diff --git a/.github/workflows/sanitizer.yml b/.github/workflows/sanitizer.yml index 57d6e78f1..b23f4520f 100644 --- a/.github/workflows/sanitizer.yml +++ b/.github/workflows/sanitizer.yml @@ -57,3 +57,13 @@ jobs: - name: test run: cd build/bin && ./unit shell: bash + + job3: + name: printf-sanitizer + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: run scan_printf script + run: ./scripts/scan_printf.sh + shell: bash diff --git a/Readme.md b/Readme.md index 917b8e8aa..1af71fad8 100644 --- a/Readme.md +++ b/Readme.md @@ -14,7 +14,6 @@ A library to import and export various 3d-model-formats including scene-post-pro [![Join the chat at https://gitter.im/assimp/assimp](https://badges.gitter.im/assimp/assimp.svg)](https://gitter.im/assimp/assimp?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Average time to resolve an issue](http://isitmaintained.com/badge/resolution/assimp/assimp.svg)](http://isitmaintained.com/project/assimp/assimp "Average time to resolve an issue") [![Percentage of issues still open](http://isitmaintained.com/badge/open/assimp/assimp.svg)](http://isitmaintained.com/project/assimp/assimp "Percentage of issues still open") -[![Total alerts](https://img.shields.io/lgtm/alerts/g/assimp/assimp.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/assimp/assimp/alerts/)
APIs are provided for C and C++. There are various bindings to other languages (C#, Java, Python, Delphi, D). Assimp also runs on Android and iOS. diff --git a/code/AssetLib/Blender/BlenderScene.cpp b/code/AssetLib/Blender/BlenderScene.cpp index 3a9a02fd0..ac10d7302 100644 --- a/code/AssetLib/Blender/BlenderScene.cpp +++ b/code/AssetLib/Blender/BlenderScene.cpp @@ -569,7 +569,7 @@ void Structure ::Convert( const FileDatabase &db) const { ReadFieldArray(dest.co, "co", db); - ReadFieldArray(dest.no, "no", db); + ReadFieldArray(dest.no, "no", db); ReadField(dest.flag, "flag", db); //ReadField(dest.mat_nr,"mat_nr",db); ReadField(dest.bweight, "bweight", db); diff --git a/code/AssetLib/FBX/FBXConverter.cpp b/code/AssetLib/FBX/FBXConverter.cpp index d45919e10..f1f57c10b 100644 --- a/code/AssetLib/FBX/FBXConverter.cpp +++ b/code/AssetLib/FBX/FBXConverter.cpp @@ -873,8 +873,12 @@ void FBXConverter::SetupNodeMetadata(const Model &model, aiNode &nd) { data->Set(index++, prop.first, interpretedBool->Value()); } else if (const TypedProperty *interpretedInt = prop.second->As>()) { data->Set(index++, prop.first, interpretedInt->Value()); + } else if (const TypedProperty *interpretedUInt = prop.second->As>()) { + data->Set(index++, prop.first, interpretedUInt->Value()); } else if (const TypedProperty *interpretedUint64 = prop.second->As>()) { data->Set(index++, prop.first, interpretedUint64->Value()); + } else if (const TypedProperty *interpretedint64 = prop.second->As>()) { + data->Set(index++, prop.first, interpretedint64->Value()); } else if (const TypedProperty *interpretedFloat = prop.second->As>()) { data->Set(index++, prop.first, interpretedFloat->Value()); } else if (const TypedProperty *interpretedString = prop.second->As>()) { @@ -1176,15 +1180,23 @@ unsigned int FBXConverter::ConvertMeshSingleMaterial(const MeshGeometry &mesh, c std::vector animMeshes; for (const BlendShape *blendShape : mesh.GetBlendShapes()) { for (const BlendShapeChannel *blendShapeChannel : blendShape->BlendShapeChannels()) { - const std::vector &shapeGeometries = blendShapeChannel->GetShapeGeometries(); - for (size_t i = 0; i < shapeGeometries.size(); i++) { + const auto& shapeGeometries = blendShapeChannel->GetShapeGeometries(); + for (const ShapeGeometry *shapeGeometry : shapeGeometries) { aiAnimMesh *animMesh = aiCreateAnimMesh(out_mesh); - const ShapeGeometry *shapeGeometry = shapeGeometries.at(i); - const std::vector &curVertices = shapeGeometry->GetVertices(); - const std::vector &curNormals = shapeGeometry->GetNormals(); - const std::vector &curIndices = shapeGeometry->GetIndices(); + const auto &curVertices = shapeGeometry->GetVertices(); + const auto &curNormals = shapeGeometry->GetNormals(); + const auto &curIndices = shapeGeometry->GetIndices(); //losing channel name if using shapeGeometry->Name() - animMesh->mName.Set(FixAnimMeshName(blendShapeChannel->Name())); + // if blendShapeChannel Name is empty or don't have a ".", add geoMetryName; + auto aniName = FixAnimMeshName(blendShapeChannel->Name()); + auto geoMetryName = FixAnimMeshName(shapeGeometry->Name()); + if (aniName.empty()) { + aniName = geoMetryName; + } + else if (aniName.find('.') == aniName.npos) { + aniName += "." + geoMetryName; + } + animMesh->mName.Set(aniName); for (size_t j = 0; j < curIndices.size(); j++) { const unsigned int curIndex = curIndices.at(j); aiVector3D vertex = curVertices.at(j); @@ -1406,13 +1418,12 @@ unsigned int FBXConverter::ConvertMeshMultiMaterial(const MeshGeometry &mesh, co std::vector animMeshes; for (const BlendShape *blendShape : mesh.GetBlendShapes()) { for (const BlendShapeChannel *blendShapeChannel : blendShape->BlendShapeChannels()) { - const std::vector &shapeGeometries = blendShapeChannel->GetShapeGeometries(); - for (size_t i = 0; i < shapeGeometries.size(); i++) { + const auto& shapeGeometries = blendShapeChannel->GetShapeGeometries(); + for (const ShapeGeometry *shapeGeometry : shapeGeometries) { aiAnimMesh *animMesh = aiCreateAnimMesh(out_mesh); - const ShapeGeometry *shapeGeometry = shapeGeometries.at(i); - const std::vector &curVertices = shapeGeometry->GetVertices(); - const std::vector &curNormals = shapeGeometry->GetNormals(); - const std::vector &curIndices = shapeGeometry->GetIndices(); + const auto& curVertices = shapeGeometry->GetVertices(); + const auto& curNormals = shapeGeometry->GetNormals(); + const auto& curIndices = shapeGeometry->GetIndices(); animMesh->mName.Set(FixAnimMeshName(shapeGeometry->Name())); for (size_t j = 0; j < curIndices.size(); j++) { unsigned int curIndex = curIndices.at(j); diff --git a/code/AssetLib/FBX/FBXDeformer.cpp b/code/AssetLib/FBX/FBXDeformer.cpp index df134a401..1aab55ea9 100644 --- a/code/AssetLib/FBX/FBXDeformer.cpp +++ b/code/AssetLib/FBX/FBXDeformer.cpp @@ -154,8 +154,10 @@ BlendShape::BlendShape(uint64_t id, const Element& element, const Document& doc, for (const Connection* con : conns) { const BlendShapeChannel* const bspc = ProcessSimpleConnection(*con, false, "BlendShapeChannel -> BlendShape", element); if (bspc) { - blendShapeChannels.push_back(bspc); - continue; + auto pr = blendShapeChannels.insert(bspc); + if (!pr.second) { + FBXImporter::LogWarn("there is the same blendShapeChannel id ", bspc->ID()); + } } } } @@ -179,8 +181,10 @@ BlendShapeChannel::BlendShapeChannel(uint64_t id, const Element& element, const for (const Connection* con : conns) { const ShapeGeometry* const sg = ProcessSimpleConnection(*con, false, "Shape -> BlendShapeChannel", element); if (sg) { - shapeGeometries.push_back(sg); - continue; + auto pr = shapeGeometries.insert(sg); + if (!pr.second) { + FBXImporter::LogWarn("there is the same shapeGeometrie id ", sg->ID()); + } } } } diff --git a/code/AssetLib/FBX/FBXDocument.h b/code/AssetLib/FBX/FBXDocument.h index 8873d65fd..821d4d5cb 100644 --- a/code/AssetLib/FBX/FBXDocument.h +++ b/code/AssetLib/FBX/FBXDocument.h @@ -46,6 +46,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #define INCLUDED_AI_FBX_DOCUMENT_H #include +#include #include #include #include "FBXProperties.h" @@ -855,14 +856,14 @@ public: return fullWeights; } - const std::vector& GetShapeGeometries() const { + const std::unordered_set& GetShapeGeometries() const { return shapeGeometries; } private: float percent; WeightArray fullWeights; - std::vector shapeGeometries; + std::unordered_set shapeGeometries; }; /** DOM class for BlendShape deformers */ @@ -872,12 +873,12 @@ public: virtual ~BlendShape(); - const std::vector& BlendShapeChannels() const { + const std::unordered_set& BlendShapeChannels() const { return blendShapeChannels; } private: - std::vector blendShapeChannels; + std::unordered_set blendShapeChannels; }; /** DOM class for skin deformer clusters (aka sub-deformers) */ diff --git a/code/AssetLib/FBX/FBXMeshGeometry.cpp b/code/AssetLib/FBX/FBXMeshGeometry.cpp index ace4ad749..fcbaac169 100644 --- a/code/AssetLib/FBX/FBXMeshGeometry.cpp +++ b/code/AssetLib/FBX/FBXMeshGeometry.cpp @@ -69,13 +69,16 @@ Geometry::Geometry(uint64_t id, const Element& element, const std::string& name, } const BlendShape* const bsp = ProcessSimpleConnection(*con, false, "BlendShape -> Geometry", element); if (bsp) { - blendShapes.push_back(bsp); + auto pr = blendShapes.insert(bsp); + if (!pr.second) { + FBXImporter::LogWarn("there is the same blendShape id ", bsp->ID()); + } } } } // ------------------------------------------------------------------------------------------------ -const std::vector& Geometry::GetBlendShapes() const { +const std::unordered_set& Geometry::GetBlendShapes() const { return blendShapes; } diff --git a/code/AssetLib/FBX/FBXMeshGeometry.h b/code/AssetLib/FBX/FBXMeshGeometry.h index f4a1a2673..3d67ec567 100644 --- a/code/AssetLib/FBX/FBXMeshGeometry.h +++ b/code/AssetLib/FBX/FBXMeshGeometry.h @@ -62,7 +62,7 @@ public: /// @param name The name instance /// @param doc The document instance Geometry( uint64_t id, const Element& element, const std::string& name, const Document& doc ); - + /// @brief The class destructor, default. virtual ~Geometry() = default; @@ -72,11 +72,12 @@ public: /// @brief Get the BlendShape attached to this geometry or nullptr /// @return The blendshape arrays. - const std::vector& GetBlendShapes() const; + const std::unordered_set& GetBlendShapes() const; private: const Skin* skin; - std::vector blendShapes; + std::unordered_set blendShapes; + }; typedef std::vector MatIndexArray; @@ -112,7 +113,7 @@ public: /// @return The binomal vector. const std::vector& GetBinormals() const; - /// @brief Return list of faces - each entry denotes a face and specifies how many vertices it has. + /// @brief Return list of faces - each entry denotes a face and specifies how many vertices it has. /// Vertices are taken from the vertex data arrays in sequential order. /// @return The face indices vector. const std::vector& GetFaceIndexCounts() const; diff --git a/code/AssetLib/MDL/HalfLife/HL1MDLLoader.cpp b/code/AssetLib/MDL/HalfLife/HL1MDLLoader.cpp index 93d37536c..a8141fcc1 100644 --- a/code/AssetLib/MDL/HalfLife/HL1MDLLoader.cpp +++ b/code/AssetLib/MDL/HalfLife/HL1MDLLoader.cpp @@ -470,14 +470,16 @@ void HL1MDLLoader::read_bones() { temp_bones_.resize(header_->numbones); + // Create the main 'bones' node that will contain all MDL root bones. aiNode *bones_node = new aiNode(AI_MDL_HL1_NODE_BONES); rootnode_children_.push_back(bones_node); - bones_node->mNumChildren = static_cast(header_->numbones); - bones_node->mChildren = new aiNode *[bones_node->mNumChildren]; + + // Store roots bones IDs temporarily. + std::vector roots; // Create bone matrices in local space. for (int i = 0; i < header_->numbones; ++i) { - aiNode *bone_node = temp_bones_[i].node = bones_node->mChildren[i] = new aiNode(unique_bones_names[i]); + aiNode *bone_node = temp_bones_[i].node = new aiNode(unique_bones_names[i]); aiVector3D angles(pbone[i].value[3], pbone[i].value[4], pbone[i].value[5]); temp_bones_[i].absolute_transform = bone_node->mTransformation = @@ -485,9 +487,11 @@ void HL1MDLLoader::read_bones() { aiVector3D(pbone[i].value[0], pbone[i].value[1], pbone[i].value[2])); if (pbone[i].parent == -1) { - bone_node->mParent = scene_->mRootNode; + bone_node->mParent = bones_node; + roots.push_back(i); // This bone has no parent. Add it to the roots list. } else { - bone_node->mParent = bones_node->mChildren[pbone[i].parent]; + bone_node->mParent = temp_bones_[pbone[i].parent].node; + temp_bones_[pbone[i].parent].children.push_back(i); // Add this bone to the parent bone's children list. temp_bones_[i].absolute_transform = temp_bones_[pbone[i].parent].absolute_transform * bone_node->mTransformation; @@ -496,6 +500,36 @@ void HL1MDLLoader::read_bones() { temp_bones_[i].offset_matrix = temp_bones_[i].absolute_transform; temp_bones_[i].offset_matrix.Inverse(); } + + // Allocate memory for each MDL root bone. + bones_node->mNumChildren = static_cast(roots.size()); + bones_node->mChildren = new aiNode *[bones_node->mNumChildren]; + + // Build all bones children hierarchy starting from each MDL root bone. + for (size_t i = 0; i < roots.size(); ++i) + { + const TempBone &root_bone = temp_bones_[roots[i]]; + bones_node->mChildren[i] = root_bone.node; + build_bone_children_hierarchy(root_bone); + } +} + +void HL1MDLLoader::build_bone_children_hierarchy(const TempBone &bone) +{ + if (bone.children.empty()) + return; + + aiNode* bone_node = bone.node; + bone_node->mNumChildren = static_cast(bone.children.size()); + bone_node->mChildren = new aiNode *[bone_node->mNumChildren]; + + // Build each child bone's hierarchy recursively. + for (size_t i = 0; i < bone.children.size(); ++i) + { + const TempBone &child_bone = temp_bones_[bone.children[i]]; + bone_node->mChildren[i] = child_bone.node; + build_bone_children_hierarchy(child_bone); + } } // ------------------------------------------------------------------------------------------------ diff --git a/code/AssetLib/MDL/HalfLife/HL1MDLLoader.h b/code/AssetLib/MDL/HalfLife/HL1MDLLoader.h index 0dba5099d..286b6e64c 100644 --- a/code/AssetLib/MDL/HalfLife/HL1MDLLoader.h +++ b/code/AssetLib/MDL/HalfLife/HL1MDLLoader.h @@ -143,6 +143,14 @@ private: */ static bool get_num_blend_controllers(const int num_blend_animations, int &num_blend_controllers); + /** + * \brief Build a bone's node children hierarchy. + * + * \param[in] bone The bone for which we must build all children hierarchy. + */ + struct TempBone; + void build_bone_children_hierarchy(const TempBone& bone); + /** Output scene to be filled */ aiScene *scene_; @@ -198,11 +206,13 @@ private: TempBone() : node(nullptr), absolute_transform(), - offset_matrix() {} + offset_matrix(), + children() {} aiNode *node; aiMatrix4x4 absolute_transform; aiMatrix4x4 offset_matrix; + std::vector children; // Bone children }; std::vector temp_bones_; diff --git a/code/AssetLib/Ogre/OgreXmlSerializer.cpp b/code/AssetLib/Ogre/OgreXmlSerializer.cpp index cd9d6dcc2..a8faaec34 100644 --- a/code/AssetLib/Ogre/OgreXmlSerializer.cpp +++ b/code/AssetLib/Ogre/OgreXmlSerializer.cpp @@ -490,7 +490,7 @@ bool OgreXmlSerializer::ImportSkeleton(Assimp::IOSystem *pIOHandler, MeshXml *me OgreXmlSerializer serializer(xmlParser.get()); XmlNode root = xmlParser->getRootNode(); if (std::string(root.name()) != nnSkeleton) { - printf("\nSkeleton is not a valid root: %s\n", root.name()); + ASSIMP_LOG_VERBOSE_DEBUG("nSkeleton is not a valid root: ", root.name(), "."); for (auto &a : root.children()) { if (std::string(a.name()) == nnSkeleton) { root = a; diff --git a/code/CApi/AssimpCExport.cpp b/code/CApi/AssimpCExport.cpp index 5e43958d0..21e40205c 100644 --- a/code/CApi/AssimpCExport.cpp +++ b/code/CApi/AssimpCExport.cpp @@ -5,8 +5,6 @@ Open Asset Import Library (assimp) Copyright (c) 2006-2022, assimp team - - All rights reserved. Redistribution and use of this software in source and binary forms, diff --git a/code/CApi/CInterfaceIOWrapper.cpp b/code/CApi/CInterfaceIOWrapper.cpp index 579545ecc..f0e46cd08 100644 --- a/code/CApi/CInterfaceIOWrapper.cpp +++ b/code/CApi/CInterfaceIOWrapper.cpp @@ -5,8 +5,6 @@ Open Asset Import Library (assimp) Copyright (c) 2006-2022, assimp team - - All rights reserved. Redistribution and use of this software in source and binary forms, @@ -47,14 +45,16 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. namespace Assimp { +// ------------------------------------------------------------------------------------------------ CIOStreamWrapper::~CIOStreamWrapper() { - /* Various places depend on this destructor to close the file */ - if (mFile) { + // Various places depend on this destructor to close the file + if (mFile != nullptr) { + mIO->mFileSystem->CloseProc(mIO->mFileSystem, mFile); } } -// ................................................................... +// ------------------------------------------------------------------------------------------------ size_t CIOStreamWrapper::Read(void *pvBuffer, size_t pSize, size_t pCount) { @@ -62,7 +62,7 @@ size_t CIOStreamWrapper::Read(void *pvBuffer, return mFile->ReadProc(mFile, (char *)pvBuffer, pSize, pCount); } -// ................................................................... +// ------------------------------------------------------------------------------------------------ size_t CIOStreamWrapper::Write(const void *pvBuffer, size_t pSize, size_t pCount) { @@ -70,23 +70,23 @@ size_t CIOStreamWrapper::Write(const void *pvBuffer, return mFile->WriteProc(mFile, (const char *)pvBuffer, pSize, pCount); } -// ................................................................... +// ------------------------------------------------------------------------------------------------ aiReturn CIOStreamWrapper::Seek(size_t pOffset, aiOrigin pOrigin) { return mFile->SeekProc(mFile, pOffset, pOrigin); } -// ................................................................... +// ------------------------------------------------------------------------------------------------ size_t CIOStreamWrapper::Tell() const { return mFile->TellProc(mFile); } -// ................................................................... +// ------------------------------------------------------------------------------------------------ size_t CIOStreamWrapper::FileSize() const { return mFile->FileSizeProc(mFile); } -// ................................................................... +// ------------------------------------------------------------------------------------------------ void CIOStreamWrapper::Flush() { return mFile->FlushProc(mFile); } diff --git a/code/CApi/CInterfaceIOWrapper.h b/code/CApi/CInterfaceIOWrapper.h index 768be3746..28d4c3e75 100644 --- a/code/CApi/CInterfaceIOWrapper.h +++ b/code/CApi/CInterfaceIOWrapper.h @@ -47,48 +47,59 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include #include #include +#include namespace Assimp { class CIOSystemWrapper; // ------------------------------------------------------------------------------------------------ -// Custom IOStream implementation for the C-API -class CIOStreamWrapper : public IOStream { +/// @brief Custom IOStream implementation for the C-API- +// ------------------------------------------------------------------------------------------------ +class CIOStreamWrapper final : public IOStream { public: - explicit CIOStreamWrapper(aiFile *pFile, CIOSystemWrapper *io) : - mFile(pFile), - mIO(io) {} - ~CIOStreamWrapper(void); - - size_t Read(void *pvBuffer, size_t pSize, size_t pCount); - size_t Write(const void *pvBuffer, size_t pSize, size_t pCount); - aiReturn Seek(size_t pOffset, aiOrigin pOrigin); - size_t Tell(void) const; - size_t FileSize() const; - void Flush(); + explicit CIOStreamWrapper(aiFile *pFile, CIOSystemWrapper *io); + ~CIOStreamWrapper() override; + size_t Read(void *pvBuffer, size_t pSize, size_t pCount) override; + size_t Write(const void *pvBuffer, size_t pSize, size_t pCount) override; + aiReturn Seek(size_t pOffset, aiOrigin pOrigin) override; + size_t Tell(void) const override; + size_t FileSize() const override; + void Flush() override; private: aiFile *mFile; CIOSystemWrapper *mIO; }; -class CIOSystemWrapper : public IOSystem { +inline CIOStreamWrapper::CIOStreamWrapper(aiFile *pFile, CIOSystemWrapper *io) : + mFile(pFile), + mIO(io) { + ai_assert(io != nullptr); +} + +// ------------------------------------------------------------------------------------------------ +/// @brief Custom IO-System wrapper implementation for the C-API. +// ------------------------------------------------------------------------------------------------ +class CIOSystemWrapper final : public IOSystem { friend class CIOStreamWrapper; public: - explicit CIOSystemWrapper(aiFileIO *pFile) : - mFileSystem(pFile) {} - - bool Exists(const char *pFile) const; - char getOsSeparator() const; - IOStream *Open(const char *pFile, const char *pMode = "rb"); - void Close(IOStream *pFile); + explicit CIOSystemWrapper(aiFileIO *pFile); + ~CIOSystemWrapper() override = default; + bool Exists(const char *pFile) const override; + char getOsSeparator() const override; + IOStream *Open(const char *pFile, const char *pMode = "rb") override; + void Close(IOStream *pFile) override; private: aiFileIO *mFileSystem; }; +inline CIOSystemWrapper::CIOSystemWrapper(aiFileIO *pFile) : mFileSystem(pFile) { + ai_assert(pFile != nullptr); +} + } // namespace Assimp -#endif +#endif // AI_CIOSYSTEM_H_INCLUDED diff --git a/code/CMakeLists.txt b/code/CMakeLists.txt index a098f3e85..ba5415fe0 100644 --- a/code/CMakeLists.txt +++ b/code/CMakeLists.txt @@ -218,6 +218,12 @@ SET( CApi_SRCS ) SOURCE_GROUP(CApi FILES ${CApi_SRCS}) +SET(Geometry_SRCS + Geometry/GeometryUtils.h + Geometry/GeometryUtils.cpp +) +SOURCE_GROUP(Geometry FILES ${Geometry_SRCS}) + SET( STEPParser_SRCS AssetLib/STEPParser/STEPFileReader.h AssetLib/STEPParser/STEPFileReader.cpp @@ -1129,6 +1135,7 @@ SET( assimp_src ${Core_SRCS} ${CApi_SRCS} ${Common_SRCS} + ${Geometry_SRCS} ${Logging_SRCS} ${Exporter_SRCS} ${PostProcessing_SRCS} diff --git a/code/Common/PolyTools.h b/code/Common/PolyTools.h index 9837a2991..a5bd1090b 100644 --- a/code/Common/PolyTools.h +++ b/code/Common/PolyTools.h @@ -74,26 +74,8 @@ inline bool OnLeftSideOfLine2D(const T& p0, const T& p1,const T& p2) { * both aiVector3D and aiVector2D, but generally ignores the third coordinate.*/ template inline bool PointInTriangle2D(const T& p0, const T& p1,const T& p2, const T& pp) { - // Point in triangle test using baryzentric coordinates - const aiVector2D v0 = p1 - p0; - const aiVector2D v1 = p2 - p0; - const aiVector2D v2 = pp - p0; - - double dot00 = v0 * v0; - double dot11 = v1 * v1; - const double dot01 = v0 * v1; - const double dot02 = v0 * v2; - const double dot12 = v1 * v2; - const double denom = dot00 * dot11 - dot01 * dot01; - if (denom == 0.0) { - return false; - } - - const double invDenom = 1.0 / denom; - dot11 = (dot11 * dot02 - dot01 * dot12) * invDenom; - dot00 = (dot00 * dot12 - dot01 * dot02) * invDenom; - - return (dot11 > 0) && (dot00 > 0) && (dot11 + dot00 < 1); + // pp should be left side of the three triangle side, by ccw arrow + return OnLeftSideOfLine2D(p0, p1, pp) && OnLeftSideOfLine2D(p1, p2, pp) && OnLeftSideOfLine2D(p2, p0, pp); } diff --git a/code/Common/Subdivision.cpp b/code/Common/Subdivision.cpp index 705ea3fb3..3aea5d4c5 100644 --- a/code/Common/Subdivision.cpp +++ b/code/Common/Subdivision.cpp @@ -50,7 +50,10 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include +#include + using namespace Assimp; + void mydummy() {} #ifdef _MSC_VER @@ -78,7 +81,7 @@ public: }; typedef std::vector UIntVector; - typedef std::map EdgeMap; + typedef std::unordered_map EdgeMap; // --------------------------------------------------------------------------- // Hashing function to derive an index into an #EdgeMap from two given diff --git a/samples/SharedCode/UTFConverter.h b/code/Geometry/GeometryUtils.cpp similarity index 54% rename from samples/SharedCode/UTFConverter.h rename to code/Geometry/GeometryUtils.cpp index 17e89ee4d..ab735aa6e 100644 --- a/samples/SharedCode/UTFConverter.h +++ b/code/Geometry/GeometryUtils.cpp @@ -1,17 +1,14 @@ /* ---------------------------------------------------------------------------- Open Asset Import Library (assimp) ---------------------------------------------------------------------------- - -Copyright (c) 2006-2020, assimp team - +---------------------------------------------------------------------- +Copyright (c) 2006-2022, assimp team All rights reserved. Redistribution and use of this software in source and binary forms, -with or without modification, are permitted provided that the following -conditions are met: +with or without modification, are permitted provided that the +following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the @@ -38,55 +35,45 @@ DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ---------------------------------------------------------------------------- + +---------------------------------------------------------------------- */ -#ifndef ASSIMP_SAMPLES_SHARED_CODE_UTFCONVERTER_H -#define ASSIMP_SAMPLES_SHARED_CODE_UTFCONVERTER_H +#include "GeometryUtils.h" -#include -#include -#include +#include -namespace AssimpSamples { -namespace SharedCode { +namespace Assimp { -// Used to convert between multibyte and unicode strings. -class UTFConverter { - using UTFConverterImpl = std::wstring_convert, wchar_t>; -public: - UTFConverter(const char* s) : - s_(s), - ws_(impl_.from_bytes(s)) { - } - UTFConverter(const wchar_t* s) : - s_(impl_.to_bytes(s)), - ws_(s) { - } - UTFConverter(const std::string& s) : - s_(s), - ws_(impl_.from_bytes(s)) { - } - UTFConverter(const std::wstring& s) : - s_(impl_.to_bytes(s)), - ws_(s) { - } - inline const char* c_str() const { - return s_.c_str(); - } - inline const std::string& str() const { - return s_; - } - inline const wchar_t* c_wstr() const { - return ws_.c_str(); - } -private: - static UTFConverterImpl impl_; - std::string s_; - std::wstring ws_; -}; - -} +ai_real GeometryUtils::heron( ai_real a, ai_real b, ai_real c ) { + ai_real s = (a + b + c) / 2; + ai_real area = pow((s * ( s - a ) * ( s - b ) * ( s - c ) ), (ai_real)0.5 ); + return area; } -#endif // ASSIMP_SAMPLES_SHARED_CODE_UTFCONVERTER_H +ai_real GeometryUtils::distance3D( const aiVector3D &vA, aiVector3D &vB ) { + const ai_real lx = ( vB.x - vA.x ); + const ai_real ly = ( vB.y - vA.y ); + const ai_real lz = ( vB.z - vA.z ); + ai_real a = lx*lx + ly*ly + lz*lz; + ai_real d = pow( a, (ai_real)0.5 ); + + return d; +} + +ai_real GeometryUtils::calculateAreaOfTriangle( const aiFace& face, aiMesh* mesh ) { + ai_real area = 0; + + aiVector3D vA( mesh->mVertices[ face.mIndices[ 0 ] ] ); + aiVector3D vB( mesh->mVertices[ face.mIndices[ 1 ] ] ); + aiVector3D vC( mesh->mVertices[ face.mIndices[ 2 ] ] ); + + ai_real a( distance3D( vA, vB ) ); + ai_real b( distance3D( vB, vC ) ); + ai_real c( distance3D( vC, vA ) ); + area = heron( a, b, c ); + + return area; +} + +} // namespace Assimp diff --git a/samples/SharedCode/UTFConverter.cpp b/code/Geometry/GeometryUtils.h similarity index 60% rename from samples/SharedCode/UTFConverter.cpp rename to code/Geometry/GeometryUtils.h index a1bff7e4b..ab49380de 100644 --- a/samples/SharedCode/UTFConverter.cpp +++ b/code/Geometry/GeometryUtils.h @@ -1,17 +1,14 @@ /* ---------------------------------------------------------------------------- Open Asset Import Library (assimp) ---------------------------------------------------------------------------- - -Copyright (c) 2006-2020, assimp team - +---------------------------------------------------------------------- +Copyright (c) 2006-2022, assimp team All rights reserved. Redistribution and use of this software in source and binary forms, -with or without modification, are permitted provided that the following -conditions are met: +with or without modification, are permitted provided that the +following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the @@ -38,15 +35,33 @@ DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ---------------------------------------------------------------------------- + +---------------------------------------------------------------------- */ -#include "UTFConverter.h" +#include +#include -namespace AssimpSamples { -namespace SharedCode { +namespace Assimp { -typename UTFConverter::UTFConverterImpl UTFConverter::impl_; +// --------------------------------------------------------------------------- +/// @brief This helper class supports some basic geometry algorithms. +// --------------------------------------------------------------------------- +class GeometryUtils { +public: + static ai_real heron( ai_real a, ai_real b, ai_real c ); + + /// @brief Will compute the distance between 2 3D-vectors + /// @param vA Vector a. + /// @param vB Vector b. + /// @return The distance. + static ai_real distance3D( const aiVector3D &vA, aiVector3D &vB ); -} -} + /// @brief Will calculate the area of a triangle described by a aiFace. + /// @param face The face + /// @param mesh The mesh containing the face + /// @return The area. + static ai_real calculateAreaOfTriangle( const aiFace& face, aiMesh* mesh ); +}; + +} // namespace Assimp diff --git a/code/PostProcessing/CalcTangentsProcess.cpp b/code/PostProcessing/CalcTangentsProcess.cpp index efc457766..a23ac856b 100644 --- a/code/PostProcessing/CalcTangentsProcess.cpp +++ b/code/PostProcessing/CalcTangentsProcess.cpp @@ -60,10 +60,6 @@ CalcTangentsProcess::CalcTangentsProcess() : // nothing to do here } -// ------------------------------------------------------------------------------------------------ -// Destructor, private as well -CalcTangentsProcess::~CalcTangentsProcess() = default; - // ------------------------------------------------------------------------------------------------ // Returns whether the processing step is present in the given flag field. bool CalcTangentsProcess::IsActive(unsigned int pFlags) const { diff --git a/code/PostProcessing/CalcTangentsProcess.h b/code/PostProcessing/CalcTangentsProcess.h index 018789bae..aaccb5307 100644 --- a/code/PostProcessing/CalcTangentsProcess.h +++ b/code/PostProcessing/CalcTangentsProcess.h @@ -59,14 +59,11 @@ namespace Assimp * because the joining of vertices also considers tangents and bitangents for * uniqueness. */ -class ASSIMP_API_WINONLY CalcTangentsProcess : public BaseProcess -{ +class ASSIMP_API_WINONLY CalcTangentsProcess : public BaseProcess { public: - CalcTangentsProcess(); - ~CalcTangentsProcess(); + ~CalcTangentsProcess() override = default; -public: // ------------------------------------------------------------------- /** Returns whether the processing step is present in the given flag. * @param pFlags The processing flags the importer was called with. @@ -74,24 +71,21 @@ public: * @return true if the process is present in this flag fields, * false if not. */ - bool IsActive( unsigned int pFlags) const; + bool IsActive( unsigned int pFlags) const override; // ------------------------------------------------------------------- /** Called prior to ExecuteOnScene(). * The function is a request to the process to update its configuration * basing on the Importer's configuration property list. */ - void SetupProperties(const Importer* pImp); - + void SetupProperties(const Importer* pImp) override; // setter for configMaxAngle - inline void SetMaxSmoothAngle(float f) - { + void SetMaxSmoothAngle(float f) { configMaxAngle =f; } protected: - // ------------------------------------------------------------------- /** Calculates tangents and bitangents for a specific mesh. * @param pMesh The mesh to process. @@ -103,10 +97,9 @@ protected: /** Executes the post processing step on the given imported data. * @param pScene The imported data to work at. */ - void Execute( aiScene* pScene); + void Execute( aiScene* pScene) override; private: - /** Configuration option: maximum smoothing angle, in radians*/ float configMaxAngle; unsigned int configSourceUV; diff --git a/code/PostProcessing/ComputeUVMappingProcess.cpp b/code/PostProcessing/ComputeUVMappingProcess.cpp index 237409f02..a5472668b 100644 --- a/code/PostProcessing/ComputeUVMappingProcess.cpp +++ b/code/PostProcessing/ComputeUVMappingProcess.cpp @@ -57,14 +57,6 @@ namespace { const static ai_real angle_epsilon = ai_real( 0.95 ); } -// ------------------------------------------------------------------------------------------------ -// Constructor to be privately used by Importer -ComputeUVMappingProcess::ComputeUVMappingProcess() = default; - -// ------------------------------------------------------------------------------------------------ -// Destructor, private as well -ComputeUVMappingProcess::~ComputeUVMappingProcess() = default; - // ------------------------------------------------------------------------------------------------ // Returns whether the processing step is present in the given flag field. bool ComputeUVMappingProcess::IsActive( unsigned int pFlags) const diff --git a/code/PostProcessing/ComputeUVMappingProcess.h b/code/PostProcessing/ComputeUVMappingProcess.h index 74744be7f..c4158f402 100644 --- a/code/PostProcessing/ComputeUVMappingProcess.h +++ b/code/PostProcessing/ComputeUVMappingProcess.h @@ -59,13 +59,10 @@ namespace Assimp { /** ComputeUVMappingProcess - converts special mappings, such as spherical, * cylindrical or boxed to proper UV coordinates for rendering. */ -class ComputeUVMappingProcess : public BaseProcess -{ -public: - ComputeUVMappingProcess(); - ~ComputeUVMappingProcess(); - +class ComputeUVMappingProcess : public BaseProcess { public: + ComputeUVMappingProcess() = default; + ~ComputeUVMappingProcess() override = default; // ------------------------------------------------------------------- /** Returns whether the processing step is present in the given flag field. @@ -73,14 +70,14 @@ public: * combination of #aiPostProcessSteps. * @return true if the process is present in this flag fields, false if not. */ - bool IsActive( unsigned int pFlags) const; + bool IsActive( unsigned int pFlags) const override; // ------------------------------------------------------------------- /** Executes the post processing step on the given imported data. * At the moment a process is not supposed to fail. * @param pScene The imported data to work at. */ - void Execute( aiScene* pScene); + void Execute( aiScene* pScene) override; protected: @@ -125,8 +122,7 @@ protected: private: // temporary structure to describe a mapping - struct MappingInfo - { + struct MappingInfo { explicit MappingInfo(aiTextureMapping _type) : type (_type) , axis (0.f,1.f,0.f) @@ -137,8 +133,7 @@ private: aiVector3D axis; unsigned int uv; - bool operator== (const MappingInfo& other) - { + bool operator== (const MappingInfo& other) { return type == other.type && axis == other.axis; } }; diff --git a/code/PostProcessing/ConvertToLHProcess.cpp b/code/PostProcessing/ConvertToLHProcess.cpp index 359c5a284..08e3fe48a 100644 --- a/code/PostProcessing/ConvertToLHProcess.cpp +++ b/code/PostProcessing/ConvertToLHProcess.cpp @@ -79,14 +79,6 @@ void flipUVs(aiMeshType *pMesh) { } // namespace -// ------------------------------------------------------------------------------------------------ -// Constructor to be privately used by Importer -MakeLeftHandedProcess::MakeLeftHandedProcess() = default; - -// ------------------------------------------------------------------------------------------------ -// Destructor, private as well -MakeLeftHandedProcess::~MakeLeftHandedProcess() = default; - // ------------------------------------------------------------------------------------------------ // Returns whether the processing step is present in the given flag field. bool MakeLeftHandedProcess::IsActive(unsigned int pFlags) const { @@ -305,14 +297,6 @@ void FlipUVsProcess::ProcessMesh(aiMesh *pMesh) { #ifndef ASSIMP_BUILD_NO_FLIPWINDING_PROCESS // # FlipWindingOrderProcess -// ------------------------------------------------------------------------------------------------ -// Constructor to be privately used by Importer -FlipWindingOrderProcess::FlipWindingOrderProcess() = default; - -// ------------------------------------------------------------------------------------------------ -// Destructor, private as well -FlipWindingOrderProcess::~FlipWindingOrderProcess() = default; - // ------------------------------------------------------------------------------------------------ // Returns whether the processing step is present in the given flag field. bool FlipWindingOrderProcess::IsActive(unsigned int pFlags) const { diff --git a/code/PostProcessing/ConvertToLHProcess.h b/code/PostProcessing/ConvertToLHProcess.h index 474056c3a..d0532277d 100644 --- a/code/PostProcessing/ConvertToLHProcess.h +++ b/code/PostProcessing/ConvertToLHProcess.h @@ -4,7 +4,6 @@ Open Asset Import Library (assimp) Copyright (c) 2006-2022, assimp team - All rights reserved. Redistribution and use of this software in source and binary forms, @@ -72,22 +71,18 @@ namespace Assimp { * * @note RH-LH and LH-RH is the same, so this class can be used for both */ -class MakeLeftHandedProcess : public BaseProcess -{ - - +class MakeLeftHandedProcess : public BaseProcess { public: - MakeLeftHandedProcess(); - ~MakeLeftHandedProcess(); + MakeLeftHandedProcess() = default; + ~MakeLeftHandedProcess() override = default; // ------------------------------------------------------------------- - bool IsActive( unsigned int pFlags) const; + bool IsActive( unsigned int pFlags) const override; // ------------------------------------------------------------------- - void Execute( aiScene* pScene); + void Execute( aiScene* pScene) override; protected: - // ------------------------------------------------------------------- /** Recursively converts a node and all of its children */ @@ -120,24 +115,22 @@ protected: // --------------------------------------------------------------------------- /** Postprocessing step to flip the face order of the imported data */ -class FlipWindingOrderProcess : public BaseProcess -{ +class FlipWindingOrderProcess : public BaseProcess { friend class Importer; public: /** Constructor to be privately used by Importer */ - FlipWindingOrderProcess(); + FlipWindingOrderProcess() = default; /** Destructor, private as well */ - ~FlipWindingOrderProcess(); + ~FlipWindingOrderProcess() override = default; // ------------------------------------------------------------------- - bool IsActive( unsigned int pFlags) const; + bool IsActive( unsigned int pFlags) const override; // ------------------------------------------------------------------- - void Execute( aiScene* pScene); + void Execute( aiScene* pScene) override; -public: /** Some other types of post-processing require winding order flips */ static void ProcessMesh( aiMesh* pMesh); }; diff --git a/code/PostProcessing/DeboneProcess.cpp b/code/PostProcessing/DeboneProcess.cpp index 22a4397bf..2a8499dc5 100644 --- a/code/PostProcessing/DeboneProcess.cpp +++ b/code/PostProcessing/DeboneProcess.cpp @@ -43,42 +43,26 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. /// @file DeboneProcess.cpp /** Implementation of the DeboneProcess post processing step */ - - // internal headers of the post-processing framework #include "ProcessHelper.h" #include "DeboneProcess.h" #include - using namespace Assimp; // ------------------------------------------------------------------------------------------------ // Constructor to be privately used by Importer -DeboneProcess::DeboneProcess() -{ - mNumBones = 0; - mNumBonesCanDoWithout = 0; - - mThreshold = AI_DEBONE_THRESHOLD; - mAllOrNone = false; -} - -// ------------------------------------------------------------------------------------------------ -// Destructor, private as well -DeboneProcess::~DeboneProcess() = default; +DeboneProcess::DeboneProcess() : mNumBones(0), mNumBonesCanDoWithout(0), mThreshold(AI_DEBONE_THRESHOLD), mAllOrNone(false) {} // ------------------------------------------------------------------------------------------------ // Returns whether the processing step is present in the given flag field. -bool DeboneProcess::IsActive( unsigned int pFlags) const -{ +bool DeboneProcess::IsActive( unsigned int pFlags) const { return (pFlags & aiProcess_Debone) != 0; } // ------------------------------------------------------------------------------------------------ // Executes the post processing step on the given imported data. -void DeboneProcess::SetupProperties(const Importer* pImp) -{ +void DeboneProcess::SetupProperties(const Importer* pImp) { // get the current value of the property mAllOrNone = pImp->GetPropertyInteger(AI_CONFIG_PP_DB_ALL_OR_NONE,0)?true:false; mThreshold = pImp->GetPropertyFloat(AI_CONFIG_PP_DB_THRESHOLD,AI_DEBONE_THRESHOLD); @@ -86,8 +70,7 @@ void DeboneProcess::SetupProperties(const Importer* pImp) // ------------------------------------------------------------------------------------------------ // Executes the post processing step on the given imported data. -void DeboneProcess::Execute( aiScene* pScene) -{ +void DeboneProcess::Execute( aiScene* pScene) { ASSIMP_LOG_DEBUG("DeboneProcess begin"); if(!pScene->mNumMeshes) { @@ -117,10 +100,8 @@ void DeboneProcess::Execute( aiScene* pScene) // build a new array of meshes for the scene std::vector meshes; - for(unsigned int a=0;amNumMeshes;a++) - { + for (unsigned int a=0;amNumMeshes; ++a) { aiMesh* srcMesh = pScene->mMeshes[a]; - std::vector > newMeshes; if(splitList[a]) { @@ -150,8 +131,7 @@ void DeboneProcess::Execute( aiScene* pScene) // and destroy the source mesh. It should be completely contained inside the new submeshes delete srcMesh; - } - else { + } else { // Mesh is kept unchanged - store it's new place in the mesh array mSubMeshIndices[a].emplace_back(static_cast(meshes.size()), (aiNode *)nullptr); meshes.push_back(srcMesh); @@ -173,8 +153,7 @@ void DeboneProcess::Execute( aiScene* pScene) // ------------------------------------------------------------------------------------------------ // Counts bones total/removable in a given mesh. -bool DeboneProcess::ConsiderMesh(const aiMesh* pMesh) -{ +bool DeboneProcess::ConsiderMesh(const aiMesh* pMesh) { if(!pMesh->HasBones()) { return false; } @@ -193,25 +172,23 @@ bool DeboneProcess::ConsiderMesh(const aiMesh* pMesh) for(unsigned int i=0;imNumBones;i++) { for(unsigned int j=0;jmBones[i]->mNumWeights;j++) { float w = pMesh->mBones[i]->mWeights[j].mWeight; - - if(w==0.0f) { + if (w == 0.0f) { continue; } unsigned int vid = pMesh->mBones[i]->mWeights[j].mVertexId; - if(w>=mThreshold) { - - if(vertexBones[vid]!=cUnowned) { - if(vertexBones[vid]==i) //double entry - { + if (w >= mThreshold) { + if (vertexBones[vid] != cUnowned) { + //double entry + if(vertexBones[vid]==i) { ASSIMP_LOG_WARN("Encountered double entry in bone weights"); - } - else //TODO: track attraction in order to break tie - { + } else { + //TODO: track attraction in order to break tie vertexBones[vid] = cCoowned; } - } - else vertexBones[vid] = i; + } else { + vertexBones[vid] = i; + } } if(!isBoneNecessary[i]) { @@ -227,13 +204,16 @@ bool DeboneProcess::ConsiderMesh(const aiMesh* pMesh) if(isInterstitialRequired) { for(unsigned int i=0;imNumFaces;i++) { unsigned int v = vertexBones[pMesh->mFaces[i].mIndices[0]]; - - for(unsigned int j=1;jmFaces[i].mNumIndices;j++) { + for (unsigned int j=1;jmFaces[i].mNumIndices;j++) { unsigned int w = vertexBones[pMesh->mFaces[i].mIndices[j]]; - if(v!=w) { - if(vmNumBones) isBoneNecessary[v] = true; - if(wmNumBones) isBoneNecessary[w] = true; + if (v != w) { + if(vmNumBones) { + isBoneNecessary[v] = true; + } + if (wmNumBones) { + isBoneNecessary[w] = true; + } } } } @@ -252,8 +232,7 @@ bool DeboneProcess::ConsiderMesh(const aiMesh* pMesh) // ------------------------------------------------------------------------------------------------ // Splits the given mesh by bone count. -void DeboneProcess::SplitMesh( const aiMesh* pMesh, std::vector< std::pair< aiMesh*,const aiBone* > >& poNewMeshes) const -{ +void DeboneProcess::SplitMesh( const aiMesh* pMesh, std::vector< std::pair< aiMesh*,const aiBone* > >& poNewMeshes) const { // same deal here as ConsiderMesh basically std::vector isBoneNecessary(pMesh->mNumBones,false); @@ -371,8 +350,7 @@ void DeboneProcess::SplitMesh( const aiMesh* pMesh, std::vector< std::pair< aiMe // ------------------------------------------------------------------------------------------------ // Recursively updates the node's mesh list to account for the changed mesh list -void DeboneProcess::UpdateNode(aiNode* pNode) const -{ +void DeboneProcess::UpdateNode(aiNode* pNode) const { // rebuild the node's mesh index list std::vector newMeshList; @@ -430,8 +408,7 @@ void DeboneProcess::UpdateNode(aiNode* pNode) const // ------------------------------------------------------------------------------------------------ // Apply the node transformation to a mesh -void DeboneProcess::ApplyTransform(aiMesh* mesh, const aiMatrix4x4& mat)const -{ +void DeboneProcess::ApplyTransform(aiMesh* mesh, const aiMatrix4x4& mat)const { // Check whether we need to transform the coordinates at all if (!mat.IsIdentity()) { diff --git a/code/PostProcessing/DeboneProcess.h b/code/PostProcessing/DeboneProcess.h index cb072b7eb..ae4448e0e 100644 --- a/code/PostProcessing/DeboneProcess.h +++ b/code/PostProcessing/DeboneProcess.h @@ -70,7 +70,7 @@ namespace Assimp { class DeboneProcess : public BaseProcess { public: DeboneProcess(); - ~DeboneProcess(); + ~DeboneProcess() override = default; // ------------------------------------------------------------------- /** Returns whether the processing step is present in the given flag. @@ -79,14 +79,14 @@ public: * @return true if the process is present in this flag fields, * false if not. */ - bool IsActive( unsigned int pFlags) const; + bool IsActive( unsigned int pFlags) const override; // ------------------------------------------------------------------- /** Called prior to ExecuteOnScene(). * The function is a request to the process to update its configuration * basing on the Importer's configuration property list. */ - void SetupProperties(const Importer* pImp); + void SetupProperties(const Importer* pImp) override; protected: // ------------------------------------------------------------------- @@ -94,7 +94,7 @@ protected: * At the moment a process is not supposed to fail. * @param pScene The imported data to work at. */ - void Execute( aiScene* pScene); + void Execute( aiScene* pScene) override; // ------------------------------------------------------------------- /** Counts bones total/removable in a given mesh. diff --git a/code/PostProcessing/DropFaceNormalsProcess.cpp b/code/PostProcessing/DropFaceNormalsProcess.cpp index f85daa588..223482374 100644 --- a/code/PostProcessing/DropFaceNormalsProcess.cpp +++ b/code/PostProcessing/DropFaceNormalsProcess.cpp @@ -54,14 +54,6 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. using namespace Assimp; -// ------------------------------------------------------------------------------------------------ -// Constructor to be privately used by Importer -DropFaceNormalsProcess::DropFaceNormalsProcess() = default; - -// ------------------------------------------------------------------------------------------------ -// Destructor, private as well -DropFaceNormalsProcess::~DropFaceNormalsProcess() = default; - // ------------------------------------------------------------------------------------------------ // Returns whether the processing step is present in the given flag field. bool DropFaceNormalsProcess::IsActive( unsigned int pFlags) const { diff --git a/code/PostProcessing/DropFaceNormalsProcess.h b/code/PostProcessing/DropFaceNormalsProcess.h index 50abdc727..df542f2ba 100644 --- a/code/PostProcessing/DropFaceNormalsProcess.h +++ b/code/PostProcessing/DropFaceNormalsProcess.h @@ -55,8 +55,8 @@ namespace Assimp { */ class ASSIMP_API_WINONLY DropFaceNormalsProcess : public BaseProcess { public: - DropFaceNormalsProcess(); - ~DropFaceNormalsProcess(); + DropFaceNormalsProcess() = default; + ~DropFaceNormalsProcess() override = default; // ------------------------------------------------------------------- /** Returns whether the processing step is present in the given flag field. @@ -64,15 +64,14 @@ public: * combination of #aiPostProcessSteps. * @return true if the process is present in this flag fields, false if not. */ - bool IsActive( unsigned int pFlags) const; + bool IsActive( unsigned int pFlags) const override; // ------------------------------------------------------------------- /** Executes the post processing step on the given imported data. * At the moment a process is not supposed to fail. * @param pScene The imported data to work at. */ - void Execute( aiScene* pScene); - + void Execute( aiScene* pScene) override; private: bool DropMeshFaceNormals(aiMesh* pcMesh); diff --git a/code/PostProcessing/EmbedTexturesProcess.cpp b/code/PostProcessing/EmbedTexturesProcess.cpp index dc7e54ac1..d5d2ef872 100644 --- a/code/PostProcessing/EmbedTexturesProcess.cpp +++ b/code/PostProcessing/EmbedTexturesProcess.cpp @@ -49,10 +49,6 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. using namespace Assimp; -EmbedTexturesProcess::EmbedTexturesProcess() = default; - -EmbedTexturesProcess::~EmbedTexturesProcess() = default; - bool EmbedTexturesProcess::IsActive(unsigned int pFlags) const { return (pFlags & aiProcess_EmbedTextures) != 0; } diff --git a/code/PostProcessing/EmbedTexturesProcess.h b/code/PostProcessing/EmbedTexturesProcess.h index c3e63612c..77d4d9c72 100644 --- a/code/PostProcessing/EmbedTexturesProcess.h +++ b/code/PostProcessing/EmbedTexturesProcess.h @@ -62,19 +62,19 @@ namespace Assimp { class ASSIMP_API EmbedTexturesProcess : public BaseProcess { public: /// The default class constructor. - EmbedTexturesProcess(); + EmbedTexturesProcess() = default; /// The class destructor. - virtual ~EmbedTexturesProcess(); + ~EmbedTexturesProcess() override = default; /// Overwritten, @see BaseProcess - virtual bool IsActive(unsigned int pFlags) const; + bool IsActive(unsigned int pFlags) const override; /// Overwritten, @see BaseProcess - virtual void SetupProperties(const Importer* pImp); + void SetupProperties(const Importer* pImp) override; /// Overwritten, @see BaseProcess - virtual void Execute(aiScene* pScene); + virtual void Execute(aiScene* pScene) override; private: // Resolve the path and add the file content to the scene as a texture. diff --git a/code/PostProcessing/FindDegenerates.cpp b/code/PostProcessing/FindDegenerates.cpp index 344979949..5874c17d2 100644 --- a/code/PostProcessing/FindDegenerates.cpp +++ b/code/PostProcessing/FindDegenerates.cpp @@ -45,6 +45,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "ProcessHelper.h" #include "FindDegenerates.h" +#include "Geometry/GeometryUtils.h" #include @@ -63,10 +64,6 @@ FindDegeneratesProcess::FindDegeneratesProcess() : // empty } -// ------------------------------------------------------------------------------------------------ -// Destructor, private as well -FindDegeneratesProcess::~FindDegeneratesProcess() = default; - // ------------------------------------------------------------------------------------------------ // Returns whether the processing step is present in the given flag field. bool FindDegeneratesProcess::IsActive( unsigned int pFlags) const { @@ -132,37 +129,6 @@ static void updateSceneGraph(aiNode* pNode, const std::unordered_mapmVertices[ face.mIndices[ 0 ] ] ); - aiVector3D vB( mesh->mVertices[ face.mIndices[ 1 ] ] ); - aiVector3D vC( mesh->mVertices[ face.mIndices[ 2 ] ] ); - - ai_real a( distance3D( vA, vB ) ); - ai_real b( distance3D( vB, vC ) ); - ai_real c( distance3D( vC, vA ) ); - area = heron( a, b, c ); - - return area; -} - // ------------------------------------------------------------------------------------------------ // Executes the post processing step on the given imported mesh bool FindDegeneratesProcess::ExecuteOnMesh( aiMesh* mesh) { @@ -218,7 +184,7 @@ bool FindDegeneratesProcess::ExecuteOnMesh( aiMesh* mesh) { if ( mConfigCheckAreaOfTriangle ) { if ( face.mNumIndices == 3 ) { - ai_real area = calculateAreaOfTriangle( face, mesh ); + ai_real area = GeometryUtils::calculateAreaOfTriangle( face, mesh ); if (area < ai_epsilon) { if ( mConfigRemoveDegenerates ) { remove_me[ a ] = true; diff --git a/code/PostProcessing/FindDegenerates.h b/code/PostProcessing/FindDegenerates.h index 6fe1e929b..6b37a47cf 100644 --- a/code/PostProcessing/FindDegenerates.h +++ b/code/PostProcessing/FindDegenerates.h @@ -59,19 +59,19 @@ namespace Assimp { class ASSIMP_API FindDegeneratesProcess : public BaseProcess { public: FindDegeneratesProcess(); - ~FindDegeneratesProcess(); + ~FindDegeneratesProcess() override = default; // ------------------------------------------------------------------- // Check whether step is active - bool IsActive( unsigned int pFlags) const; + bool IsActive( unsigned int pFlags) const override; // ------------------------------------------------------------------- // Execute step on a given scene - void Execute( aiScene* pScene); + void Execute( aiScene* pScene) override; // ------------------------------------------------------------------- // Setup import settings - void SetupProperties(const Importer* pImp); + void SetupProperties(const Importer* pImp) override; // ------------------------------------------------------------------- // Execute step on a given mesh @@ -105,23 +105,19 @@ private: bool mConfigCheckAreaOfTriangle; }; -inline -void FindDegeneratesProcess::EnableInstantRemoval(bool enabled) { +inline void FindDegeneratesProcess::EnableInstantRemoval(bool enabled) { mConfigRemoveDegenerates = enabled; } -inline -bool FindDegeneratesProcess::IsInstantRemoval() const { +inline bool FindDegeneratesProcess::IsInstantRemoval() const { return mConfigRemoveDegenerates; } -inline -void FindDegeneratesProcess::EnableAreaCheck( bool enabled ) { +inline void FindDegeneratesProcess::EnableAreaCheck( bool enabled ) { mConfigCheckAreaOfTriangle = enabled; } -inline -bool FindDegeneratesProcess::isAreaCheckEnabled() const { +inline bool FindDegeneratesProcess::isAreaCheckEnabled() const { return mConfigCheckAreaOfTriangle; } diff --git a/code/PostProcessing/FindInstancesProcess.cpp b/code/PostProcessing/FindInstancesProcess.cpp index 07a0f66db..55974b1c3 100644 --- a/code/PostProcessing/FindInstancesProcess.cpp +++ b/code/PostProcessing/FindInstancesProcess.cpp @@ -58,10 +58,6 @@ FindInstancesProcess::FindInstancesProcess() : configSpeedFlag (false) {} -// ------------------------------------------------------------------------------------------------ -// Destructor, private as well -FindInstancesProcess::~FindInstancesProcess() = default; - // ------------------------------------------------------------------------------------------------ // Returns whether the processing step is present in the given flag field. bool FindInstancesProcess::IsActive( unsigned int pFlags) const diff --git a/code/PostProcessing/FindInstancesProcess.h b/code/PostProcessing/FindInstancesProcess.h index b501d88d5..6927301ca 100644 --- a/code/PostProcessing/FindInstancesProcess.h +++ b/code/PostProcessing/FindInstancesProcess.h @@ -50,7 +50,8 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "PostProcessing/ProcessHelper.h" class FindInstancesProcessTest; -namespace Assimp { + +namespace Assimp { // ------------------------------------------------------------------------------- /** @brief Get a pseudo(!)-hash representing a mesh. @@ -60,8 +61,7 @@ namespace Assimp { * @param in Input mesh * @return Hash. */ -inline -uint64_t GetMeshHash(aiMesh* in) { +inline uint64_t GetMeshHash(aiMesh* in) { ai_assert(nullptr != in); // ... get an unique value representing the vertex format of the mesh @@ -83,8 +83,7 @@ uint64_t GetMeshHash(aiMesh* in) { * @param e Epsilon * @return true if the arrays are identical */ -inline -bool CompareArrays(const aiVector3D* first, const aiVector3D* second, +inline bool CompareArrays(const aiVector3D* first, const aiVector3D* second, unsigned int size, float e) { for (const aiVector3D* end = first+size; first != end; ++first,++second) { if ( (*first - *second).SquareLength() >= e) @@ -107,31 +106,27 @@ inline bool CompareArrays(const aiColor4D* first, const aiColor4D* second, // --------------------------------------------------------------------------- /** @brief A post-processing steps to search for instanced meshes */ -class FindInstancesProcess : public BaseProcess -{ +class FindInstancesProcess : public BaseProcess { public: - FindInstancesProcess(); - ~FindInstancesProcess(); + ~FindInstancesProcess() override = default; -public: // ------------------------------------------------------------------- // Check whether step is active in given flags combination - bool IsActive( unsigned int pFlags) const; + bool IsActive( unsigned int pFlags) const override; // ------------------------------------------------------------------- // Execute step on a given scene - void Execute( aiScene* pScene); + void Execute( aiScene* pScene) override; // ------------------------------------------------------------------- // Setup properties prior to executing the process - void SetupProperties(const Importer* pImp); + void SetupProperties(const Importer* pImp) override; private: - bool configSpeedFlag; - }; // ! end class FindInstancesProcess + } // ! end namespace Assimp #endif // !! AI_FINDINSTANCES_H_INC diff --git a/code/PostProcessing/FindInvalidDataProcess.cpp b/code/PostProcessing/FindInvalidDataProcess.cpp index c65208cbd..bb8e365a1 100644 --- a/code/PostProcessing/FindInvalidDataProcess.cpp +++ b/code/PostProcessing/FindInvalidDataProcess.cpp @@ -60,10 +60,6 @@ FindInvalidDataProcess::FindInvalidDataProcess() : // nothing to do here } -// ------------------------------------------------------------------------------------------------ -// Destructor, private as well -FindInvalidDataProcess::~FindInvalidDataProcess() = default; - // ------------------------------------------------------------------------------------------------ // Returns whether the processing step is present in the given flag field. bool FindInvalidDataProcess::IsActive(unsigned int pFlags) const { diff --git a/code/PostProcessing/FindInvalidDataProcess.h b/code/PostProcessing/FindInvalidDataProcess.h index 5ea895c59..024eb9b1e 100644 --- a/code/PostProcessing/FindInvalidDataProcess.h +++ b/code/PostProcessing/FindInvalidDataProcess.h @@ -64,35 +64,37 @@ namespace Assimp { * which have zero normal vectors. */ class ASSIMP_API FindInvalidDataProcess : public BaseProcess { public: + // ------------------------------------------------------------------- + /// The default class constructor / destructor. FindInvalidDataProcess(); - ~FindInvalidDataProcess(); + ~FindInvalidDataProcess() override = default; // ------------------------------------------------------------------- - // - bool IsActive(unsigned int pFlags) const; + /// Returns active state. + bool IsActive(unsigned int pFlags) const override; // ------------------------------------------------------------------- - // Setup import settings - void SetupProperties(const Importer *pImp); + /// Setup import settings + void SetupProperties(const Importer *pImp) override; // ------------------------------------------------------------------- - // Run the step - void Execute(aiScene *pScene); + /// Run the step + void Execute(aiScene *pScene) override; // ------------------------------------------------------------------- - /** Executes the post-processing step on the given mesh - * @param pMesh The mesh to process. - * @return 0 - nothing, 1 - removed sth, 2 - please delete me */ + /// Executes the post-processing step on the given mesh + /// @param pMesh The mesh to process. + /// @return 0 - nothing, 1 - removed sth, 2 - please delete me */ int ProcessMesh(aiMesh *pMesh); // ------------------------------------------------------------------- - /** Executes the post-processing step on the given animation - * @param anim The animation to process. */ + /// Executes the post-processing step on the given animation + /// @param anim The animation to process. */ void ProcessAnimation(aiAnimation *anim); // ------------------------------------------------------------------- - /** Executes the post-processing step on the given anim channel - * @param anim The animation channel to process.*/ + /// Executes the post-processing step on the given anim channel + /// @param anim The animation channel to process.*/ void ProcessAnimationChannel(aiNodeAnim *anim); private: diff --git a/code/PostProcessing/FixNormalsStep.cpp b/code/PostProcessing/FixNormalsStep.cpp index 3791bd35a..54ac05cc8 100644 --- a/code/PostProcessing/FixNormalsStep.cpp +++ b/code/PostProcessing/FixNormalsStep.cpp @@ -56,26 +56,15 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. using namespace Assimp; - -// ------------------------------------------------------------------------------------------------ -// Constructor to be privately used by Importer -FixInfacingNormalsProcess::FixInfacingNormalsProcess() = default; - -// ------------------------------------------------------------------------------------------------ -// Destructor, private as well -FixInfacingNormalsProcess::~FixInfacingNormalsProcess() = default; - // ------------------------------------------------------------------------------------------------ // Returns whether the processing step is present in the given flag field. -bool FixInfacingNormalsProcess::IsActive( unsigned int pFlags) const -{ +bool FixInfacingNormalsProcess::IsActive( unsigned int pFlags) const { return (pFlags & aiProcess_FixInfacingNormals) != 0; } // ------------------------------------------------------------------------------------------------ // Executes the post processing step on the given imported data. -void FixInfacingNormalsProcess::Execute( aiScene* pScene) -{ +void FixInfacingNormalsProcess::Execute( aiScene* pScene) { ASSIMP_LOG_DEBUG("FixInfacingNormalsProcess begin"); bool bHas( false ); diff --git a/code/PostProcessing/FixNormalsStep.h b/code/PostProcessing/FixNormalsStep.h index b7d3ba386..20be1958b 100644 --- a/code/PostProcessing/FixNormalsStep.h +++ b/code/PostProcessing/FixNormalsStep.h @@ -49,8 +49,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. struct aiMesh; -namespace Assimp -{ +namespace Assimp { // --------------------------------------------------------------------------- /** The FixInfacingNormalsProcess tries to determine whether the normal @@ -59,8 +58,10 @@ namespace Assimp */ class FixInfacingNormalsProcess : public BaseProcess { public: - FixInfacingNormalsProcess(); - ~FixInfacingNormalsProcess(); + // ------------------------------------------------------------------- + /// The default class constructor / destructor. + FixInfacingNormalsProcess() = default; + ~FixInfacingNormalsProcess() override = default; // ------------------------------------------------------------------- /** Returns whether the processing step is present in the given flag field. @@ -68,14 +69,14 @@ public: * combination of #aiPostProcessSteps. * @return true if the process is present in this flag fields, false if not. */ - bool IsActive( unsigned int pFlags) const; + bool IsActive( unsigned int pFlags) const override; // ------------------------------------------------------------------- /** Executes the post processing step on the given imported data. * At the moment a process is not supposed to fail. * @param pScene The imported data to work at. */ - void Execute( aiScene* pScene); + void Execute( aiScene* pScene) override; protected: diff --git a/code/PostProcessing/GenBoundingBoxesProcess.cpp b/code/PostProcessing/GenBoundingBoxesProcess.cpp index 52a0861e5..ca8e4d6d0 100644 --- a/code/PostProcessing/GenBoundingBoxesProcess.cpp +++ b/code/PostProcessing/GenBoundingBoxesProcess.cpp @@ -48,10 +48,6 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. namespace Assimp { -GenBoundingBoxesProcess::GenBoundingBoxesProcess() = default; - -GenBoundingBoxesProcess::~GenBoundingBoxesProcess() = default; - bool GenBoundingBoxesProcess::IsActive(unsigned int pFlags) const { return 0 != ( pFlags & aiProcess_GenBoundingBoxes ); } diff --git a/code/PostProcessing/GenBoundingBoxesProcess.h b/code/PostProcessing/GenBoundingBoxesProcess.h index 0b7591b6d..0cf8514f4 100644 --- a/code/PostProcessing/GenBoundingBoxesProcess.h +++ b/code/PostProcessing/GenBoundingBoxesProcess.h @@ -19,7 +19,7 @@ conditions are met: copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. - +s * Neither the name of the assimp team, nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior @@ -54,18 +54,23 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. namespace Assimp { -/** Post-processing process to find axis-aligned bounding volumes for amm meshes - * used in a scene +/** + * @brief Post-processing process to find axis-aligned bounding volumes for amm meshes + * used in a scene. */ class ASSIMP_API GenBoundingBoxesProcess : public BaseProcess { public: - /// The class constructor. - GenBoundingBoxesProcess(); - /// The class destructor. - ~GenBoundingBoxesProcess(); - /// Will return true, if aiProcess_GenBoundingBoxes is defined. + // ------------------------------------------------------------------- + /// The default class constructor / destructor. + GenBoundingBoxesProcess() = default; + ~GenBoundingBoxesProcess() override = default; + + // ------------------------------------------------------------------- + /// @brief Will return true, if aiProcess_GenBoundingBoxes is defined. bool IsActive(unsigned int pFlags) const override; - /// The execution callback. + + // ------------------------------------------------------------------- + /// @brief The execution callback. void Execute(aiScene* pScene) override; }; diff --git a/code/PostProcessing/GenFaceNormalsProcess.cpp b/code/PostProcessing/GenFaceNormalsProcess.cpp index d3520d4b2..1d259ce22 100644 --- a/code/PostProcessing/GenFaceNormalsProcess.cpp +++ b/code/PostProcessing/GenFaceNormalsProcess.cpp @@ -54,14 +54,6 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. using namespace Assimp; -// ------------------------------------------------------------------------------------------------ -// Constructor to be privately used by Importer -GenFaceNormalsProcess::GenFaceNormalsProcess() = default; - -// ------------------------------------------------------------------------------------------------ -// Destructor, private as well -GenFaceNormalsProcess::~GenFaceNormalsProcess() = default; - // ------------------------------------------------------------------------------------------------ // Returns whether the processing step is present in the given flag field. bool GenFaceNormalsProcess::IsActive(unsigned int pFlags) const { diff --git a/code/PostProcessing/GenFaceNormalsProcess.h b/code/PostProcessing/GenFaceNormalsProcess.h index c2f157e20..94794631e 100644 --- a/code/PostProcessing/GenFaceNormalsProcess.h +++ b/code/PostProcessing/GenFaceNormalsProcess.h @@ -47,35 +47,33 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "Common/BaseProcess.h" #include -namespace Assimp -{ +namespace Assimp { // --------------------------------------------------------------------------- -/** The GenFaceNormalsProcess computes face normals for all faces of all meshes -*/ -class ASSIMP_API_WINONLY GenFaceNormalsProcess : public BaseProcess -{ +/** + * @brief The GenFaceNormalsProcess computes face normals for all faces of all meshes + */ +class ASSIMP_API_WINONLY GenFaceNormalsProcess : public BaseProcess { public: + // ------------------------------------------------------------------- + /// The default class constructor / destructor. + GenFaceNormalsProcess() = default; + ~GenFaceNormalsProcess() override = default; - GenFaceNormalsProcess(); - ~GenFaceNormalsProcess(); - -public: // ------------------------------------------------------------------- /** Returns whether the processing step is present in the given flag field. * @param pFlags The processing flags the importer was called with. A bitwise * combination of #aiPostProcessSteps. * @return true if the process is present in this flag fields, false if not. */ - bool IsActive( unsigned int pFlags) const; + bool IsActive( unsigned int pFlags) const override; // ------------------------------------------------------------------- /** Executes the post processing step on the given imported data. * At the moment a process is not supposed to fail. * @param pScene The imported data to work at. */ - void Execute( aiScene* pScene); - + void Execute( aiScene* pScene) override; private: bool GenMeshFaceNormals(aiMesh* pcMesh); diff --git a/code/PostProcessing/GenVertexNormalsProcess.cpp b/code/PostProcessing/GenVertexNormalsProcess.cpp index 5b9033383..c8afac297 100644 --- a/code/PostProcessing/GenVertexNormalsProcess.cpp +++ b/code/PostProcessing/GenVertexNormalsProcess.cpp @@ -60,10 +60,6 @@ GenVertexNormalsProcess::GenVertexNormalsProcess() : // empty } -// ------------------------------------------------------------------------------------------------ -// Destructor, private as well -GenVertexNormalsProcess::~GenVertexNormalsProcess() = default; - // ------------------------------------------------------------------------------------------------ // Returns whether the processing step is present in the given flag field. bool GenVertexNormalsProcess::IsActive(unsigned int pFlags) const { @@ -109,10 +105,10 @@ void GenVertexNormalsProcess::Execute(aiScene *pScene) { // Executes the post processing step on the given imported data. bool GenVertexNormalsProcess::GenMeshVertexNormals(aiMesh *pMesh, unsigned int meshIndex) { if (nullptr != pMesh->mNormals) { - if (force_) - delete[] pMesh->mNormals; - else + if (!force_) { return false; + } + delete[] pMesh->mNormals; } // If the mesh consists of lines and/or points but not of @@ -144,8 +140,9 @@ bool GenVertexNormalsProcess::GenMeshVertexNormals(aiMesh *pMesh, unsigned int m const aiVector3D *pV3 = &pMesh->mVertices[face.mIndices[face.mNumIndices - 1]]; // Boolean XOR - if either but not both of these flags is set, then the winding order has // changed and the cross product to calculate the normal needs to be reversed - if (flippedWindingOrder_ != leftHanded_) + if (flippedWindingOrder_ != leftHanded_) { std::swap(pV2, pV3); + } const aiVector3D vNor = ((*pV2 - *pV1) ^ (*pV3 - *pV1)).NormalizeSafe(); for (unsigned int i = 0; i < face.mNumIndices; ++i) { diff --git a/code/PostProcessing/GenVertexNormalsProcess.h b/code/PostProcessing/GenVertexNormalsProcess.h index 370bf42b1..b7db9c4f2 100644 --- a/code/PostProcessing/GenVertexNormalsProcess.h +++ b/code/PostProcessing/GenVertexNormalsProcess.h @@ -60,8 +60,10 @@ namespace Assimp { */ class ASSIMP_API GenVertexNormalsProcess : public BaseProcess { public: + // ------------------------------------------------------------------- + /// The default class constructor / destructor. GenVertexNormalsProcess(); - ~GenVertexNormalsProcess(); + ~GenVertexNormalsProcess() override = default; // ------------------------------------------------------------------- /** Returns whether the processing step is present in the given flag. @@ -70,22 +72,21 @@ public: * @return true if the process is present in this flag fields, * false if not. */ - bool IsActive( unsigned int pFlags) const; + bool IsActive( unsigned int pFlags) const override; // ------------------------------------------------------------------- /** Called prior to ExecuteOnScene(). * The function is a request to the process to update its configuration * basing on the Importer's configuration property list. */ - void SetupProperties(const Importer* pImp); + void SetupProperties(const Importer* pImp) override; // ------------------------------------------------------------------- /** Executes the post processing step on the given imported data. * At the moment a process is not supposed to fail. * @param pScene The imported data to work at. */ - void Execute( aiScene* pScene); - + void Execute( aiScene* pScene) override; // setter for configMaxAngle inline void SetMaxSmoothAngle(ai_real f) { diff --git a/code/PostProcessing/ImproveCacheLocality.cpp b/code/PostProcessing/ImproveCacheLocality.cpp index 197856171..9336d6b17 100644 --- a/code/PostProcessing/ImproveCacheLocality.cpp +++ b/code/PostProcessing/ImproveCacheLocality.cpp @@ -68,10 +68,6 @@ ImproveCacheLocalityProcess::ImproveCacheLocalityProcess() // empty } -// ------------------------------------------------------------------------------------------------ -// Destructor, private as well -ImproveCacheLocalityProcess::~ImproveCacheLocalityProcess() = default; - // ------------------------------------------------------------------------------------------------ // Returns whether the processing step is present in the given flag field. bool ImproveCacheLocalityProcess::IsActive( unsigned int pFlags) const { diff --git a/code/PostProcessing/ImproveCacheLocality.h b/code/PostProcessing/ImproveCacheLocality.h index b2074a17c..6f4d55719 100644 --- a/code/PostProcessing/ImproveCacheLocality.h +++ b/code/PostProcessing/ImproveCacheLocality.h @@ -51,8 +51,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. struct aiMesh; -namespace Assimp -{ +namespace Assimp { // --------------------------------------------------------------------------- /** The ImproveCacheLocalityProcess reorders all faces for improved vertex @@ -61,26 +60,24 @@ namespace Assimp * * @note This step expects triagulated input data. */ -class ImproveCacheLocalityProcess : public BaseProcess -{ +class ImproveCacheLocalityProcess : public BaseProcess { public: - + // ------------------------------------------------------------------- + /// The default class constructor / destructor. ImproveCacheLocalityProcess(); - ~ImproveCacheLocalityProcess(); - -public: + ~ImproveCacheLocalityProcess() override = default; // ------------------------------------------------------------------- // Check whether the pp step is active - bool IsActive( unsigned int pFlags) const; + bool IsActive( unsigned int pFlags) const override; // ------------------------------------------------------------------- // Executes the pp step on a given scene - void Execute( aiScene* pScene); + void Execute( aiScene* pScene) override; // ------------------------------------------------------------------- // Configures the pp step - void SetupProperties(const Importer* pImp); + void SetupProperties(const Importer* pImp) override; protected: // ------------------------------------------------------------------- diff --git a/code/PostProcessing/JoinVerticesProcess.h b/code/PostProcessing/JoinVerticesProcess.h index b05d74ef5..aa8dc5794 100644 --- a/code/PostProcessing/JoinVerticesProcess.h +++ b/code/PostProcessing/JoinVerticesProcess.h @@ -51,8 +51,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. struct aiMesh; -namespace Assimp -{ +namespace Assimp { // --------------------------------------------------------------------------- /** The JoinVerticesProcess unites identical vertices in all imported meshes. @@ -65,12 +64,9 @@ namespace Assimp class ASSIMP_API JoinVerticesProcess : public BaseProcess { public: // ------------------------------------------------------------------- - /// @brief The default class constructor. - JoinVerticesProcess() = default; - - // ------------------------------------------------------------------- - /// @brief The default class destructor. - ~JoinVerticesProcess() = default; + /// The default class constructor / destructor. + JoinVerticesProcess() = default; + ~JoinVerticesProcess() override = default; // ------------------------------------------------------------------- /** Returns whether the processing step is present in the given flag field. @@ -78,14 +74,14 @@ public: * combination of #aiPostProcessSteps. * @return true if the process is present in this flag fields, false if not. */ - bool IsActive( unsigned int pFlags) const; + bool IsActive( unsigned int pFlags) const override; // ------------------------------------------------------------------- /** Executes the post processing step on the given imported data. * At the moment a process is not supposed to fail. * @param pScene The imported data to work at. */ - void Execute( aiScene* pScene); + void Execute( aiScene* pScene) override; // ------------------------------------------------------------------- /** Unites identical vertices in the given mesh. diff --git a/code/PostProcessing/LimitBoneWeightsProcess.cpp b/code/PostProcessing/LimitBoneWeightsProcess.cpp index 51fb43dfc..7047ec0f1 100644 --- a/code/PostProcessing/LimitBoneWeightsProcess.cpp +++ b/code/PostProcessing/LimitBoneWeightsProcess.cpp @@ -53,11 +53,9 @@ namespace Assimp { // ------------------------------------------------------------------------------------------------ // Constructor to be privately used by Importer -LimitBoneWeightsProcess::LimitBoneWeightsProcess() : mMaxWeights(AI_LMW_MAX_WEIGHTS) {} - -// ------------------------------------------------------------------------------------------------ -// Destructor, private as well -LimitBoneWeightsProcess::~LimitBoneWeightsProcess() = default; +LimitBoneWeightsProcess::LimitBoneWeightsProcess() : mMaxWeights(AI_LMW_MAX_WEIGHTS) { + // empty +} // ------------------------------------------------------------------------------------------------ // Returns whether the processing step is present in the given flag field. diff --git a/code/PostProcessing/LimitBoneWeightsProcess.h b/code/PostProcessing/LimitBoneWeightsProcess.h index 22d286b68..b19d536cf 100644 --- a/code/PostProcessing/LimitBoneWeightsProcess.h +++ b/code/PostProcessing/LimitBoneWeightsProcess.h @@ -74,8 +74,10 @@ namespace Assimp { */ class ASSIMP_API LimitBoneWeightsProcess : public BaseProcess { public: + // ------------------------------------------------------------------- + /// The default class constructor / destructor. LimitBoneWeightsProcess(); - ~LimitBoneWeightsProcess(); + ~LimitBoneWeightsProcess() override = default; // ------------------------------------------------------------------- /** Returns whether the processing step is present in the given flag. @@ -84,27 +86,27 @@ public: * @return true if the process is present in this flag fields, * false if not. */ - bool IsActive( unsigned int pFlags) const; + bool IsActive( unsigned int pFlags) const override; // ------------------------------------------------------------------- /** Called prior to ExecuteOnScene(). * The function is a request to the process to update its configuration * basing on the Importer's configuration property list. */ - void SetupProperties(const Importer* pImp); - - // ------------------------------------------------------------------- - /** Limits the bone weight count for all vertices in the given mesh. - * @param pMesh The mesh to process. - */ - void ProcessMesh( aiMesh* pMesh); + void SetupProperties(const Importer* pImp) override; // ------------------------------------------------------------------- /** Executes the post processing step on the given imported data. * At the moment a process is not supposed to fail. * @param pScene The imported data to work at. */ - void Execute( aiScene* pScene); + void Execute( aiScene* pScene) override; + + // ------------------------------------------------------------------- + /** Limits the bone weight count for all vertices in the given mesh. + * @param pMesh The mesh to process. + */ + void ProcessMesh( aiMesh* pMesh); // ------------------------------------------------------------------- /** Describes a bone weight on a vertex */ diff --git a/code/PostProcessing/MakeVerboseFormat.cpp b/code/PostProcessing/MakeVerboseFormat.cpp index 0f5276cf3..1cc2fdc02 100644 --- a/code/PostProcessing/MakeVerboseFormat.cpp +++ b/code/PostProcessing/MakeVerboseFormat.cpp @@ -49,10 +49,6 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. using namespace Assimp; -// ------------------------------------------------------------------------------------------------ -MakeVerboseFormatProcess::MakeVerboseFormatProcess() = default; -// ------------------------------------------------------------------------------------------------ -MakeVerboseFormatProcess::~MakeVerboseFormatProcess() = default; // ------------------------------------------------------------------------------------------------ // Executes the post processing step on the given imported data. void MakeVerboseFormatProcess::Execute(aiScene *pScene) { diff --git a/code/PostProcessing/MakeVerboseFormat.h b/code/PostProcessing/MakeVerboseFormat.h index 6b81da622..f21f5919e 100644 --- a/code/PostProcessing/MakeVerboseFormat.h +++ b/code/PostProcessing/MakeVerboseFormat.h @@ -66,22 +66,19 @@ namespace Assimp { * The step has been added because it was required by the viewer, however * it has been moved to the main library since others might find it * useful, too. */ -class ASSIMP_API_WINONLY MakeVerboseFormatProcess : public BaseProcess -{ -public: - - - MakeVerboseFormatProcess(); - ~MakeVerboseFormatProcess(); - +class ASSIMP_API_WINONLY MakeVerboseFormatProcess : public BaseProcess { public: + // ------------------------------------------------------------------- + /// The default class constructor / destructor. + MakeVerboseFormatProcess() = default; + ~MakeVerboseFormatProcess() override = default; // ------------------------------------------------------------------- /** Returns whether the processing step is present in the given flag field. * @param pFlags The processing flags the importer was called with. A bitwise * combination of #aiPostProcessSteps. * @return true if the process is present in this flag fields, false if not */ - bool IsActive( unsigned int /*pFlags*/ ) const + bool IsActive( unsigned int /*pFlags*/ ) const override { // NOTE: There is no direct flag that corresponds to // this postprocess step. @@ -92,7 +89,7 @@ public: /** Executes the post processing step on the given imported data. * At the moment a process is not supposed to fail. * @param pScene The imported data to work at. */ - void Execute( aiScene* pScene); + void Execute( aiScene* pScene) override; public: diff --git a/code/PostProcessing/OptimizeGraph.cpp b/code/PostProcessing/OptimizeGraph.cpp index 26b06e9b6..bcd654634 100644 --- a/code/PostProcessing/OptimizeGraph.cpp +++ b/code/PostProcessing/OptimizeGraph.cpp @@ -78,10 +78,6 @@ OptimizeGraphProcess::OptimizeGraphProcess() : // empty } -// ------------------------------------------------------------------------------------------------ -// Destructor, private as well -OptimizeGraphProcess::~OptimizeGraphProcess() = default; - // ------------------------------------------------------------------------------------------------ // Returns whether the processing step is present in the given flag field. bool OptimizeGraphProcess::IsActive(unsigned int pFlags) const { diff --git a/code/PostProcessing/OptimizeGraph.h b/code/PostProcessing/OptimizeGraph.h index f5caa139c..23e59e67d 100644 --- a/code/PostProcessing/OptimizeGraph.h +++ b/code/PostProcessing/OptimizeGraph.h @@ -71,8 +71,10 @@ namespace Assimp { */ class OptimizeGraphProcess : public BaseProcess { public: + // ------------------------------------------------------------------- + /// The default class constructor / destructor. OptimizeGraphProcess(); - ~OptimizeGraphProcess(); + ~OptimizeGraphProcess() override = default; // ------------------------------------------------------------------- bool IsActive( unsigned int pFlags) const override; diff --git a/code/PostProcessing/OptimizeMeshes.cpp b/code/PostProcessing/OptimizeMeshes.cpp index a8c01e2d7..0fd597808 100644 --- a/code/PostProcessing/OptimizeMeshes.cpp +++ b/code/PostProcessing/OptimizeMeshes.cpp @@ -69,10 +69,6 @@ OptimizeMeshesProcess::OptimizeMeshesProcess() // empty } -// ------------------------------------------------------------------------------------------------ -// Destructor, private as well -OptimizeMeshesProcess::~OptimizeMeshesProcess() = default; - // ------------------------------------------------------------------------------------------------ // Returns whether the processing step is present in the given flag field. bool OptimizeMeshesProcess::IsActive( unsigned int pFlags) const diff --git a/code/PostProcessing/OptimizeMeshes.h b/code/PostProcessing/OptimizeMeshes.h index b80f98d5d..0b062959a 100644 --- a/code/PostProcessing/OptimizeMeshes.h +++ b/code/PostProcessing/OptimizeMeshes.h @@ -68,11 +68,10 @@ namespace Assimp { */ class OptimizeMeshesProcess : public BaseProcess { public: - /// @brief The class constructor. + // ------------------------------------------------------------------- + /// The default class constructor / destructor. OptimizeMeshesProcess(); - - /// @brief The class destructor. - ~OptimizeMeshesProcess(); + ~OptimizeMeshesProcess() override = default; /** @brief Internal utility to store additional mesh info */ @@ -94,16 +93,14 @@ public: unsigned int output_id; }; -public: // ------------------------------------------------------------------- - bool IsActive( unsigned int pFlags) const; + bool IsActive( unsigned int pFlags) const override; // ------------------------------------------------------------------- - void Execute( aiScene* pScene); + void Execute( aiScene* pScene) override; // ------------------------------------------------------------------- - void SetupProperties(const Importer* pImp); - + void SetupProperties(const Importer* pImp) override; // ------------------------------------------------------------------- /** @brief Specify whether you want meshes with different diff --git a/code/PostProcessing/PretransformVertices.cpp b/code/PostProcessing/PretransformVertices.cpp index 9ac90d277..b6bb6155e 100644 --- a/code/PostProcessing/PretransformVertices.cpp +++ b/code/PostProcessing/PretransformVertices.cpp @@ -68,10 +68,6 @@ PretransformVertices::PretransformVertices() : // empty } -// ------------------------------------------------------------------------------------------------ -// Destructor, private as well -PretransformVertices::~PretransformVertices() = default; - // ------------------------------------------------------------------------------------------------ // Returns whether the processing step is present in the given flag field. bool PretransformVertices::IsActive(unsigned int pFlags) const { diff --git a/code/PostProcessing/PretransformVertices.h b/code/PostProcessing/PretransformVertices.h index 14e5139ec..7c2b5e99e 100644 --- a/code/PostProcessing/PretransformVertices.h +++ b/code/PostProcessing/PretransformVertices.h @@ -68,8 +68,10 @@ namespace Assimp { */ class ASSIMP_API PretransformVertices : public BaseProcess { public: + // ------------------------------------------------------------------- + /// The default class constructor / destructor. PretransformVertices(); - ~PretransformVertices(); + ~PretransformVertices() override = default; // ------------------------------------------------------------------- // Check whether step is active diff --git a/code/PostProcessing/RemoveRedundantMaterials.cpp b/code/PostProcessing/RemoveRedundantMaterials.cpp index 3c3cd59e0..dbc3c8822 100644 --- a/code/PostProcessing/RemoveRedundantMaterials.cpp +++ b/code/PostProcessing/RemoveRedundantMaterials.cpp @@ -62,10 +62,6 @@ RemoveRedundantMatsProcess::RemoveRedundantMatsProcess() // nothing to do here } -// ------------------------------------------------------------------------------------------------ -// Destructor, private as well -RemoveRedundantMatsProcess::~RemoveRedundantMatsProcess() = default; - // ------------------------------------------------------------------------------------------------ // Returns whether the processing step is present in the given flag field. bool RemoveRedundantMatsProcess::IsActive( unsigned int pFlags) const diff --git a/code/PostProcessing/RemoveRedundantMaterials.h b/code/PostProcessing/RemoveRedundantMaterials.h index e8c1478fd..1b42bea55 100644 --- a/code/PostProcessing/RemoveRedundantMaterials.h +++ b/code/PostProcessing/RemoveRedundantMaterials.h @@ -59,23 +59,22 @@ namespace Assimp { */ class ASSIMP_API RemoveRedundantMatsProcess : public BaseProcess { public: - /// The default class constructor. + // ------------------------------------------------------------------- + /// The default class constructor / destructor. RemoveRedundantMatsProcess(); - - /// The class destructor. - ~RemoveRedundantMatsProcess(); + ~RemoveRedundantMatsProcess() override = default; // ------------------------------------------------------------------- // Check whether step is active - bool IsActive( unsigned int pFlags) const; + bool IsActive( unsigned int pFlags) const override; // ------------------------------------------------------------------- // Execute step on a given scene - void Execute( aiScene* pScene); + void Execute( aiScene* pScene) override; // ------------------------------------------------------------------- // Setup import settings - void SetupProperties(const Importer* pImp); + void SetupProperties(const Importer* pImp) override; // ------------------------------------------------------------------- /** @brief Set list of fixed (inmutable) materials diff --git a/code/PostProcessing/RemoveVCProcess.cpp b/code/PostProcessing/RemoveVCProcess.cpp index 8bbe791f6..35047dc0a 100644 --- a/code/PostProcessing/RemoveVCProcess.cpp +++ b/code/PostProcessing/RemoveVCProcess.cpp @@ -56,10 +56,6 @@ using namespace Assimp; RemoveVCProcess::RemoveVCProcess() : configDeleteFlags(), mScene() {} -// ------------------------------------------------------------------------------------------------ -// Destructor, private as well -RemoveVCProcess::~RemoveVCProcess() = default; - // ------------------------------------------------------------------------------------------------ // Returns whether the processing step is present in the given flag field. bool RemoveVCProcess::IsActive(unsigned int pFlags) const { diff --git a/code/PostProcessing/RemoveVCProcess.h b/code/PostProcessing/RemoveVCProcess.h index cf1086882..45c0b3a71 100644 --- a/code/PostProcessing/RemoveVCProcess.h +++ b/code/PostProcessing/RemoveVCProcess.h @@ -58,11 +58,10 @@ namespace Assimp { */ class ASSIMP_API RemoveVCProcess : public BaseProcess { public: - /// The default class constructor. + // ------------------------------------------------------------------- + /// The default class constructor / destructor. RemoveVCProcess(); - - /// The class destructor. - ~RemoveVCProcess(); + ~RemoveVCProcess() override = default; // ------------------------------------------------------------------- /** Returns whether the processing step is present in the given flag field. @@ -70,37 +69,35 @@ public: * combination of #aiPostProcessSteps. * @return true if the process is present in this flag fields, false if not. */ - bool IsActive( unsigned int pFlags) const; + bool IsActive( unsigned int pFlags) const override; // ------------------------------------------------------------------- /** Executes the post processing step on the given imported data. * At the moment a process is not supposed to fail. * @param pScene The imported data to work at. */ - void Execute( aiScene* pScene); + void Execute( aiScene* pScene) override; // ------------------------------------------------------------------- /** Called prior to ExecuteOnScene(). * The function is a request to the process to update its configuration * basing on the Importer's configuration property list. */ - virtual void SetupProperties(const Importer* pImp); + virtual void SetupProperties(const Importer* pImp) override; // ------------------------------------------------------------------- /** Manually setup the configuration flags for the step * * @param Bitwise combination of the #aiComponent enumerated values. */ - void SetDeleteFlags(unsigned int f) - { + void SetDeleteFlags(unsigned int f) { configDeleteFlags = f; } // ------------------------------------------------------------------- /** Query the current configuration. */ - unsigned int GetDeleteFlags() const - { + unsigned int GetDeleteFlags() const { return configDeleteFlags; } diff --git a/code/PostProcessing/ScaleProcess.cpp b/code/PostProcessing/ScaleProcess.cpp index 34f68539a..665f28a7e 100644 --- a/code/PostProcessing/ScaleProcess.cpp +++ b/code/PostProcessing/ScaleProcess.cpp @@ -47,25 +47,27 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. namespace Assimp { -ScaleProcess::ScaleProcess() -: BaseProcess() -, mScale( AI_CONFIG_GLOBAL_SCALE_FACTOR_DEFAULT ) { +// ------------------------------------------------------------------------------------------------ +ScaleProcess::ScaleProcess() : BaseProcess(), mScale( AI_CONFIG_GLOBAL_SCALE_FACTOR_DEFAULT ) { + // empty } -ScaleProcess::~ScaleProcess() = default; - +// ------------------------------------------------------------------------------------------------ void ScaleProcess::setScale( ai_real scale ) { mScale = scale; } +// ------------------------------------------------------------------------------------------------ ai_real ScaleProcess::getScale() const { return mScale; } +// ------------------------------------------------------------------------------------------------ bool ScaleProcess::IsActive( unsigned int pFlags ) const { return ( pFlags & aiProcess_GlobalScale ) != 0; } +// ------------------------------------------------------------------------------------------------ void ScaleProcess::SetupProperties( const Importer* pImp ) { // User scaling mScale = pImp->GetPropertyFloat( AI_CONFIG_GLOBAL_SCALE_FACTOR_KEY, 1.0f ); @@ -78,6 +80,7 @@ void ScaleProcess::SetupProperties( const Importer* pImp ) { mScale *= importerScale; } +// ------------------------------------------------------------------------------------------------ void ScaleProcess::Execute( aiScene* pScene ) { if(mScale == 1.0f) { return; // nothing to scale @@ -96,37 +99,30 @@ void ScaleProcess::Execute( aiScene* pScene ) { } // Process animations and update position transform to new unit system - for( unsigned int animationID = 0; animationID < pScene->mNumAnimations; animationID++ ) - { + for( unsigned int animationID = 0; animationID < pScene->mNumAnimations; animationID++ ) { aiAnimation* animation = pScene->mAnimations[animationID]; - for( unsigned int animationChannel = 0; animationChannel < animation->mNumChannels; animationChannel++) - { + for( unsigned int animationChannel = 0; animationChannel < animation->mNumChannels; animationChannel++) { aiNodeAnim* anim = animation->mChannels[animationChannel]; - for( unsigned int posKey = 0; posKey < anim->mNumPositionKeys; posKey++) - { + for( unsigned int posKey = 0; posKey < anim->mNumPositionKeys; posKey++) { aiVectorKey& vectorKey = anim->mPositionKeys[posKey]; vectorKey.mValue *= mScale; } } } - for( unsigned int meshID = 0; meshID < pScene->mNumMeshes; meshID++) - { + for( unsigned int meshID = 0; meshID < pScene->mNumMeshes; meshID++) { aiMesh *mesh = pScene->mMeshes[meshID]; // Reconstruct mesh vertices to the new unit system - for( unsigned int vertexID = 0; vertexID < mesh->mNumVertices; vertexID++) - { + for( unsigned int vertexID = 0; vertexID < mesh->mNumVertices; vertexID++) { aiVector3D& vertex = mesh->mVertices[vertexID]; vertex *= mScale; } - // bone placement / scaling - for( unsigned int boneID = 0; boneID < mesh->mNumBones; boneID++) - { + for( unsigned int boneID = 0; boneID < mesh->mNumBones; boneID++) { // Reconstruct matrix by transform rather than by scale // This prevent scale values being changed which can // be meaningful in some cases @@ -152,12 +148,10 @@ void ScaleProcess::Execute( aiScene* pScene ) { // animation mesh processing // convert by position rather than scale. - for( unsigned int animMeshID = 0; animMeshID < mesh->mNumAnimMeshes; animMeshID++) - { + for( unsigned int animMeshID = 0; animMeshID < mesh->mNumAnimMeshes; animMeshID++) { aiAnimMesh * animMesh = mesh->mAnimMeshes[animMeshID]; - for( unsigned int vertexID = 0; vertexID < animMesh->mNumVertices; vertexID++) - { + for( unsigned int vertexID = 0; vertexID < animMesh->mNumVertices; vertexID++) { aiVector3D& vertex = animMesh->mVertices[vertexID]; vertex *= mScale; } @@ -167,16 +161,17 @@ void ScaleProcess::Execute( aiScene* pScene ) { traverseNodes( pScene->mRootNode ); } +// ------------------------------------------------------------------------------------------------ void ScaleProcess::traverseNodes( aiNode *node, unsigned int nested_node_id ) { applyScaling( node ); - for( size_t i = 0; i < node->mNumChildren; i++) - { + for( size_t i = 0; i < node->mNumChildren; i++) { // recurse into the tree until we are done! traverseNodes( node->mChildren[i], nested_node_id+1 ); } } +// ------------------------------------------------------------------------------------------------ void ScaleProcess::applyScaling( aiNode *currentNode ) { if ( nullptr != currentNode ) { // Reconstruct matrix by transform rather than by scale diff --git a/code/PostProcessing/ScaleProcess.h b/code/PostProcessing/ScaleProcess.h index b6eb75de7..ae1c3ed00 100644 --- a/code/PostProcessing/ScaleProcess.h +++ b/code/PostProcessing/ScaleProcess.h @@ -62,11 +62,10 @@ namespace Assimp { */ class ASSIMP_API ScaleProcess : public BaseProcess { public: - /// The default class constructor. + // ------------------------------------------------------------------- + /// The default class constructor / destructor. ScaleProcess(); - - /// The class destructor. - virtual ~ScaleProcess(); + ~ScaleProcess() override = default; /// Will set the scale manually. void setScale( ai_real scale ); @@ -75,13 +74,13 @@ public: ai_real getScale() const; /// Overwritten, @see BaseProcess - virtual bool IsActive( unsigned int pFlags ) const; + virtual bool IsActive( unsigned int pFlags ) const override; /// Overwritten, @see BaseProcess - virtual void SetupProperties( const Importer* pImp ); + virtual void SetupProperties( const Importer* pImp ) override; /// Overwritten, @see BaseProcess - virtual void Execute( aiScene* pScene ); + virtual void Execute( aiScene* pScene ) override; private: void traverseNodes( aiNode *currentNode, unsigned int nested_node_id = 0 ); diff --git a/code/PostProcessing/SortByPTypeProcess.cpp b/code/PostProcessing/SortByPTypeProcess.cpp index 6312fa173..1be75fc48 100644 --- a/code/PostProcessing/SortByPTypeProcess.cpp +++ b/code/PostProcessing/SortByPTypeProcess.cpp @@ -59,10 +59,6 @@ SortByPTypeProcess::SortByPTypeProcess() : // empty } -// ------------------------------------------------------------------------------------------------ -// Destructor, private as well -SortByPTypeProcess::~SortByPTypeProcess() = default; - // ------------------------------------------------------------------------------------------------ // Returns whether the processing step is present in the given flag field. bool SortByPTypeProcess::IsActive(unsigned int pFlags) const { diff --git a/code/PostProcessing/SortByPTypeProcess.h b/code/PostProcessing/SortByPTypeProcess.h index e30342a86..ce4f7da62 100644 --- a/code/PostProcessing/SortByPTypeProcess.h +++ b/code/PostProcessing/SortByPTypeProcess.h @@ -60,17 +60,19 @@ namespace Assimp { */ class ASSIMP_API SortByPTypeProcess : public BaseProcess { public: + // ------------------------------------------------------------------- + /// The default class constructor / destructor. SortByPTypeProcess(); - ~SortByPTypeProcess(); + ~SortByPTypeProcess() override = default; // ------------------------------------------------------------------- - bool IsActive( unsigned int pFlags) const; + bool IsActive( unsigned int pFlags) const override; // ------------------------------------------------------------------- - void Execute( aiScene* pScene); + void Execute( aiScene* pScene) override; // ------------------------------------------------------------------- - void SetupProperties(const Importer* pImp); + void SetupProperties(const Importer* pImp) override; private: int mConfigRemoveMeshes; diff --git a/code/PostProcessing/SplitByBoneCountProcess.cpp b/code/PostProcessing/SplitByBoneCountProcess.cpp index a501d3bd6..5324160d4 100644 --- a/code/PostProcessing/SplitByBoneCountProcess.cpp +++ b/code/PostProcessing/SplitByBoneCountProcess.cpp @@ -40,7 +40,6 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ---------------------------------------------------------------------- */ - /// @file SplitByBoneCountProcess.cpp /// Implementation of the SplitByBoneCount postprocessing step @@ -59,47 +58,36 @@ using namespace Assimp::Formatter; // ------------------------------------------------------------------------------------------------ // Constructor -SplitByBoneCountProcess::SplitByBoneCountProcess() -{ - // set default, might be overridden by importer config - mMaxBoneCount = AI_SBBC_DEFAULT_MAX_BONES; +SplitByBoneCountProcess::SplitByBoneCountProcess() : mMaxBoneCount(AI_SBBC_DEFAULT_MAX_BONES) { + // empty } -// ------------------------------------------------------------------------------------------------ -// Destructor -SplitByBoneCountProcess::~SplitByBoneCountProcess() = default; - // ------------------------------------------------------------------------------------------------ // Returns whether the processing step is present in the given flag. -bool SplitByBoneCountProcess::IsActive( unsigned int pFlags) const -{ +bool SplitByBoneCountProcess::IsActive( unsigned int pFlags) const { return !!(pFlags & aiProcess_SplitByBoneCount); } // ------------------------------------------------------------------------------------------------ // Updates internal properties -void SplitByBoneCountProcess::SetupProperties(const Importer* pImp) -{ +void SplitByBoneCountProcess::SetupProperties(const Importer* pImp) { mMaxBoneCount = pImp->GetPropertyInteger(AI_CONFIG_PP_SBBC_MAX_BONES,AI_SBBC_DEFAULT_MAX_BONES); } // ------------------------------------------------------------------------------------------------ // Executes the post processing step on the given imported data. -void SplitByBoneCountProcess::Execute( aiScene* pScene) -{ +void SplitByBoneCountProcess::Execute( aiScene* pScene) { ASSIMP_LOG_DEBUG("SplitByBoneCountProcess begin"); // early out bool isNecessary = false; for( unsigned int a = 0; a < pScene->mNumMeshes; ++a) - if( pScene->mMeshes[a]->mNumBones > mMaxBoneCount ) - { + if( pScene->mMeshes[a]->mNumBones > mMaxBoneCount ) { isNecessary = true; break; } - if( !isNecessary ) - { + if( !isNecessary ) { ASSIMP_LOG_DEBUG("SplitByBoneCountProcess early-out: no meshes with more than ", mMaxBoneCount, " bones." ); return; } @@ -111,28 +99,23 @@ void SplitByBoneCountProcess::Execute( aiScene* pScene) // build a new array of meshes for the scene std::vector meshes; - for( unsigned int a = 0; a < pScene->mNumMeshes; ++a) - { + for( unsigned int a = 0; a < pScene->mNumMeshes; ++a) { aiMesh* srcMesh = pScene->mMeshes[a]; std::vector newMeshes; SplitMesh( pScene->mMeshes[a], newMeshes); // mesh was split - if( !newMeshes.empty() ) - { + if( !newMeshes.empty() ) { // store new meshes and indices of the new meshes - for( unsigned int b = 0; b < newMeshes.size(); ++b) - { + for( unsigned int b = 0; b < newMeshes.size(); ++b) { mSubMeshIndices[a].push_back( static_cast(meshes.size())); meshes.push_back( newMeshes[b]); } // and destroy the source mesh. It should be completely contained inside the new submeshes delete srcMesh; - } - else - { + } else { // Mesh is kept unchanged - store it's new place in the mesh array mSubMeshIndices[a].push_back( static_cast(meshes.size())); meshes.push_back( srcMesh); @@ -153,11 +136,9 @@ void SplitByBoneCountProcess::Execute( aiScene* pScene) // ------------------------------------------------------------------------------------------------ // Splits the given mesh by bone count. -void SplitByBoneCountProcess::SplitMesh( const aiMesh* pMesh, std::vector& poNewMeshes) const -{ +void SplitByBoneCountProcess::SplitMesh( const aiMesh* pMesh, std::vector& poNewMeshes) const { // skip if not necessary - if( pMesh->mNumBones <= mMaxBoneCount ) - { + if( pMesh->mNumBones <= mMaxBoneCount ) { return; } @@ -165,27 +146,22 @@ void SplitByBoneCountProcess::SplitMesh( const aiMesh* pMesh, std::vector BoneWeight; std::vector< std::vector > vertexBones( pMesh->mNumVertices); - for( unsigned int a = 0; a < pMesh->mNumBones; ++a) - { + for( unsigned int a = 0; a < pMesh->mNumBones; ++a) { const aiBone* bone = pMesh->mBones[a]; - for( unsigned int b = 0; b < bone->mNumWeights; ++b) - { - if (bone->mWeights[b].mWeight > 0.0f) - { - int vertexId = bone->mWeights[b].mVertexId; - vertexBones[vertexId].emplace_back(a, bone->mWeights[b].mWeight); - if (vertexBones[vertexId].size() > mMaxBoneCount) - { - throw DeadlyImportError("SplitByBoneCountProcess: Single face requires more bones than specified max bone count!"); + for( unsigned int b = 0; b < bone->mNumWeights; ++b) { + if (bone->mWeights[b].mWeight > 0.0f) { + int vertexId = bone->mWeights[b].mVertexId; + vertexBones[vertexId].emplace_back(a, bone->mWeights[b].mWeight); + if (vertexBones[vertexId].size() > mMaxBoneCount) { + throw DeadlyImportError("SplitByBoneCountProcess: Single face requires more bones than specified max bone count!"); + } } - } } } unsigned int numFacesHandled = 0; std::vector isFaceHandled( pMesh->mNumFaces, false); - while( numFacesHandled < pMesh->mNumFaces ) - { + while( numFacesHandled < pMesh->mNumFaces ) { // which bones are used in the current submesh unsigned int numBones = 0; std::vector isBoneUsed( pMesh->mNumBones, false); @@ -196,11 +172,9 @@ void SplitByBoneCountProcess::SplitMesh( const aiMesh* pMesh, std::vectormNumFaces; ++a) - { + for( unsigned int a = 0; a < pMesh->mNumFaces; ++a) { // skip if the face is already stored in a submesh - if( isFaceHandled[a] ) - { + if( isFaceHandled[a] ) { continue; } // a small local set of new bones for the current face. State of all used bones for that face @@ -209,33 +183,27 @@ void SplitByBoneCountProcess::SplitMesh( const aiMesh* pMesh, std::vectormFaces[a]; // check every vertex if its bones would still fit into the current submesh - for( unsigned int b = 0; b < face.mNumIndices; ++b ) - { - const std::vector& vb = vertexBones[face.mIndices[b]]; - for( unsigned int c = 0; c < vb.size(); ++c) - { - unsigned int boneIndex = vb[c].first; - if( !isBoneUsed[boneIndex] ) - { - newBonesAtCurrentFace.insert(boneIndex); + for( unsigned int b = 0; b < face.mNumIndices; ++b ) { + const std::vector& vb = vertexBones[face.mIndices[b]]; + for( unsigned int c = 0; c < vb.size(); ++c) { + unsigned int boneIndex = vb[c].first; + if( !isBoneUsed[boneIndex] ) { + newBonesAtCurrentFace.insert(boneIndex); + } } - } } // leave out the face if the new bones required for this face don't fit the bone count limit anymore - if( numBones + newBonesAtCurrentFace.size() > mMaxBoneCount ) - { + if( numBones + newBonesAtCurrentFace.size() > mMaxBoneCount ) { continue; } // mark all new bones as necessary - for (std::set::iterator it = newBonesAtCurrentFace.begin(); it != newBonesAtCurrentFace.end(); ++it) - { - if (!isBoneUsed[*it]) - { - isBoneUsed[*it] = true; - numBones++; - } + for (std::set::iterator it = newBonesAtCurrentFace.begin(); it != newBonesAtCurrentFace.end(); ++it) { + if (!isBoneUsed[*it]) { + isBoneUsed[*it] = true; + numBones++; + } } // store the face index and the vertex count @@ -261,27 +229,21 @@ void SplitByBoneCountProcess::SplitMesh( const aiMesh* pMesh, std::vectormNumVertices = numSubMeshVertices; newMesh->mNumFaces = static_cast(subMeshFaces.size()); newMesh->mVertices = new aiVector3D[newMesh->mNumVertices]; - if( pMesh->HasNormals() ) - { + if( pMesh->HasNormals() ) { newMesh->mNormals = new aiVector3D[newMesh->mNumVertices]; } - if( pMesh->HasTangentsAndBitangents() ) - { + if( pMesh->HasTangentsAndBitangents() ) { newMesh->mTangents = new aiVector3D[newMesh->mNumVertices]; newMesh->mBitangents = new aiVector3D[newMesh->mNumVertices]; } - for( unsigned int a = 0; a < AI_MAX_NUMBER_OF_TEXTURECOORDS; ++a ) - { - if( pMesh->HasTextureCoords( a) ) - { + for( unsigned int a = 0; a < AI_MAX_NUMBER_OF_TEXTURECOORDS; ++a ) { + if( pMesh->HasTextureCoords( a) ) { newMesh->mTextureCoords[a] = new aiVector3D[newMesh->mNumVertices]; } newMesh->mNumUVComponents[a] = pMesh->mNumUVComponents[a]; } - for( unsigned int a = 0; a < AI_MAX_NUMBER_OF_COLOR_SETS; ++a ) - { - if( pMesh->HasVertexColors( a) ) - { + for( unsigned int a = 0; a < AI_MAX_NUMBER_OF_COLOR_SETS; ++a ) { + if( pMesh->HasVertexColors( a) ) { newMesh->mColors[a] = new aiColor4D[newMesh->mNumVertices]; } } @@ -290,41 +252,33 @@ void SplitByBoneCountProcess::SplitMesh( const aiMesh* pMesh, std::vectormFaces = new aiFace[subMeshFaces.size()]; unsigned int nvi = 0; // next vertex index std::vector previousVertexIndices( numSubMeshVertices, std::numeric_limits::max()); // per new vertex: its index in the source mesh - for( unsigned int a = 0; a < subMeshFaces.size(); ++a ) - { + for( unsigned int a = 0; a < subMeshFaces.size(); ++a ) { const aiFace& srcFace = pMesh->mFaces[subMeshFaces[a]]; aiFace& dstFace = newMesh->mFaces[a]; dstFace.mNumIndices = srcFace.mNumIndices; dstFace.mIndices = new unsigned int[dstFace.mNumIndices]; // accumulate linearly all the vertices of the source face - for( unsigned int b = 0; b < dstFace.mNumIndices; ++b ) - { + for( unsigned int b = 0; b < dstFace.mNumIndices; ++b ) { unsigned int srcIndex = srcFace.mIndices[b]; dstFace.mIndices[b] = nvi; previousVertexIndices[nvi] = srcIndex; newMesh->mVertices[nvi] = pMesh->mVertices[srcIndex]; - if( pMesh->HasNormals() ) - { + if( pMesh->HasNormals() ) { newMesh->mNormals[nvi] = pMesh->mNormals[srcIndex]; } - if( pMesh->HasTangentsAndBitangents() ) - { + if( pMesh->HasTangentsAndBitangents() ) { newMesh->mTangents[nvi] = pMesh->mTangents[srcIndex]; newMesh->mBitangents[nvi] = pMesh->mBitangents[srcIndex]; } - for( unsigned int c = 0; c < AI_MAX_NUMBER_OF_TEXTURECOORDS; ++c ) - { - if( pMesh->HasTextureCoords( c) ) - { + for( unsigned int c = 0; c < AI_MAX_NUMBER_OF_TEXTURECOORDS; ++c ) { + if( pMesh->HasTextureCoords( c) ) { newMesh->mTextureCoords[c][nvi] = pMesh->mTextureCoords[c][srcIndex]; } } - for( unsigned int c = 0; c < AI_MAX_NUMBER_OF_COLOR_SETS; ++c ) - { - if( pMesh->HasVertexColors( c) ) - { + for( unsigned int c = 0; c < AI_MAX_NUMBER_OF_COLOR_SETS; ++c ) { + if( pMesh->HasVertexColors( c) ) { newMesh->mColors[c][nvi] = pMesh->mColors[c][srcIndex]; } } @@ -340,10 +294,8 @@ void SplitByBoneCountProcess::SplitMesh( const aiMesh* pMesh, std::vectormBones = new aiBone*[numBones]; std::vector mappedBoneIndex( pMesh->mNumBones, std::numeric_limits::max()); - for( unsigned int a = 0; a < pMesh->mNumBones; ++a ) - { - if( !isBoneUsed[a] ) - { + for( unsigned int a = 0; a < pMesh->mNumBones; ++a ) { + if( !isBoneUsed[a] ) { continue; } @@ -360,24 +312,20 @@ void SplitByBoneCountProcess::SplitMesh( const aiMesh* pMesh, std::vectormNumBones == numBones ); // iterate over all new vertices and count which bones affected its old vertex in the source mesh - for( unsigned int a = 0; a < numSubMeshVertices; ++a ) - { + for( unsigned int a = 0; a < numSubMeshVertices; ++a ) { unsigned int oldIndex = previousVertexIndices[a]; const std::vector& bonesOnThisVertex = vertexBones[oldIndex]; - for( unsigned int b = 0; b < bonesOnThisVertex.size(); ++b ) - { + for( unsigned int b = 0; b < bonesOnThisVertex.size(); ++b ) { unsigned int newBoneIndex = mappedBoneIndex[ bonesOnThisVertex[b].first ]; - if( newBoneIndex != std::numeric_limits::max() ) - { + if( newBoneIndex != std::numeric_limits::max() ) { newMesh->mBones[newBoneIndex]->mNumWeights++; } } } // allocate all bone weight arrays accordingly - for( unsigned int a = 0; a < newMesh->mNumBones; ++a ) - { + for( unsigned int a = 0; a < newMesh->mNumBones; ++a ) { aiBone* bone = newMesh->mBones[a]; ai_assert( bone->mNumWeights > 0 ); bone->mWeights = new aiVertexWeight[bone->mNumWeights]; @@ -385,16 +333,14 @@ void SplitByBoneCountProcess::SplitMesh( const aiMesh* pMesh, std::vector& bonesOnThisVertex = vertexBones[previousIndex]; // all of the bones affecting it should be present in the new submesh, or else // the face it comprises shouldn't be present - for( unsigned int b = 0; b < bonesOnThisVertex.size(); ++b) - { + for( unsigned int b = 0; b < bonesOnThisVertex.size(); ++b) { unsigned int newBoneIndex = mappedBoneIndex[ bonesOnThisVertex[b].first ]; ai_assert( newBoneIndex != std::numeric_limits::max() ); aiVertexWeight* dstWeight = newMesh->mBones[newBoneIndex]->mWeights + newMesh->mBones[newBoneIndex]->mNumWeights; @@ -450,14 +396,11 @@ void SplitByBoneCountProcess::SplitMesh( const aiMesh* pMesh, std::vectormNumMeshes > 0 ) - { + if( pNode->mNumMeshes == 0 ) { std::vector newMeshList; - for( unsigned int a = 0; a < pNode->mNumMeshes; ++a) - { + for( unsigned int a = 0; a < pNode->mNumMeshes; ++a) { unsigned int srcIndex = pNode->mMeshes[a]; const std::vector& replaceMeshes = mSubMeshIndices[srcIndex]; newMeshList.insert( newMeshList.end(), replaceMeshes.begin(), replaceMeshes.end()); @@ -470,8 +413,7 @@ void SplitByBoneCountProcess::UpdateNode( aiNode* pNode) const } // do that also recursively for all children - for( unsigned int a = 0; a < pNode->mNumChildren; ++a ) - { + for( unsigned int a = 0; a < pNode->mNumChildren; ++a ) { UpdateNode( pNode->mChildren[a]); } } diff --git a/code/PostProcessing/SplitByBoneCountProcess.h b/code/PostProcessing/SplitByBoneCountProcess.h index 938b00c7f..625019e0c 100644 --- a/code/PostProcessing/SplitByBoneCountProcess.h +++ b/code/PostProcessing/SplitByBoneCountProcess.h @@ -4,7 +4,6 @@ Open Asset Import Library (assimp) Copyright (c) 2006-2022, assimp team - All rights reserved. Redistribution and use of this software in source and binary forms, @@ -51,9 +50,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include #include -namespace Assimp -{ - +namespace Assimp { /** Postprocessing filter to split meshes with many bones into submeshes * so that each submesh has a certain max bone count. @@ -61,34 +58,29 @@ namespace Assimp * Applied BEFORE the JoinVertices-Step occurs. * Returns NON-UNIQUE vertices, splits by bone count. */ -class SplitByBoneCountProcess : public BaseProcess -{ +class SplitByBoneCountProcess : public BaseProcess { public: - + // ------------------------------------------------------------------- + /// The default class constructor / destructor. SplitByBoneCountProcess(); - ~SplitByBoneCountProcess(); + ~SplitByBoneCountProcess() override = default; -public: - /** Returns whether the processing step is present in the given flag. - * @param pFlags The processing flags the importer was called with. A - * bitwise combination of #aiPostProcessSteps. - * @return true if the process is present in this flag fields, - * false if not. - */ - bool IsActive( unsigned int pFlags) const; + /// @brief Returns whether the processing step is present in the given flag. + /// @param pFlags The processing flags the importer was called with. A + /// bitwise combination of #aiPostProcessSteps. + /// @return true if the process is present in this flag fields, false if not. + bool IsActive( unsigned int pFlags) const override; - /** Called prior to ExecuteOnScene(). - * The function is a request to the process to update its configuration - * basing on the Importer's configuration property list. - */ - virtual void SetupProperties(const Importer* pImp); + /// @brief Called prior to ExecuteOnScene(). + /// The function is a request to the process to update its configuration + /// basing on the Importer's configuration property list. + virtual void SetupProperties(const Importer* pImp) override; protected: - /** Executes the post processing step on the given imported data. - * At the moment a process is not supposed to fail. - * @param pScene The imported data to work at. - */ - void Execute( aiScene* pScene); + /// Executes the post processing step on the given imported data. + /// At the moment a process is not supposed to fail. + /// @param pScene The imported data to work at. + void Execute( aiScene* pScene) override; /// Splits the given mesh by bone count. /// @param pMesh the Mesh to split. Is not changed at all, but might be superfluous in case it was split. diff --git a/code/PostProcessing/SplitLargeMeshes.cpp b/code/PostProcessing/SplitLargeMeshes.cpp index 151ac4991..73e0cc5d8 100644 --- a/code/PostProcessing/SplitLargeMeshes.cpp +++ b/code/PostProcessing/SplitLargeMeshes.cpp @@ -55,9 +55,6 @@ SplitLargeMeshesProcess_Triangle::SplitLargeMeshesProcess_Triangle() { LIMIT = AI_SLM_DEFAULT_MAX_TRIANGLES; } -// ------------------------------------------------------------------------------------------------ -SplitLargeMeshesProcess_Triangle::~SplitLargeMeshesProcess_Triangle() = default; - // ------------------------------------------------------------------------------------------------ // Returns whether the processing step is present in the given flag field. bool SplitLargeMeshesProcess_Triangle::IsActive( unsigned int pFlags) const { @@ -329,9 +326,6 @@ SplitLargeMeshesProcess_Vertex::SplitLargeMeshesProcess_Vertex() { LIMIT = AI_SLM_DEFAULT_MAX_VERTICES; } -// ------------------------------------------------------------------------------------------------ -SplitLargeMeshesProcess_Vertex::~SplitLargeMeshesProcess_Vertex() = default; - // ------------------------------------------------------------------------------------------------ // Returns whether the processing step is present in the given flag field. bool SplitLargeMeshesProcess_Vertex::IsActive( unsigned int pFlags) const { diff --git a/code/PostProcessing/SplitLargeMeshes.h b/code/PostProcessing/SplitLargeMeshes.h index e5a8d4c1b..4e0d764c1 100644 --- a/code/PostProcessing/SplitLargeMeshes.h +++ b/code/PostProcessing/SplitLargeMeshes.h @@ -83,16 +83,15 @@ class SplitLargeMeshesProcess_Vertex; * Applied BEFORE the JoinVertices-Step occurs. * Returns NON-UNIQUE vertices, splits by triangle number. */ -class ASSIMP_API SplitLargeMeshesProcess_Triangle : public BaseProcess -{ +class ASSIMP_API SplitLargeMeshesProcess_Triangle : public BaseProcess { friend class SplitLargeMeshesProcess_Vertex; public: - + // ------------------------------------------------------------------- + /// The default class constructor / destructor. SplitLargeMeshesProcess_Triangle(); - ~SplitLargeMeshesProcess_Triangle(); + ~SplitLargeMeshesProcess_Triangle() override = default; -public: // ------------------------------------------------------------------- /** Returns whether the processing step is present in the given flag. * @param pFlags The processing flags the importer was called with. A @@ -100,16 +99,14 @@ public: * @return true if the process is present in this flag fields, * false if not. */ - bool IsActive( unsigned int pFlags) const; - + bool IsActive( unsigned int pFlags) const override; // ------------------------------------------------------------------- /** Called prior to ExecuteOnScene(). * The function is a request to the process to update its configuration * basing on the Importer's configuration property list. */ - virtual void SetupProperties(const Importer* pImp); - + void SetupProperties(const Importer* pImp) override; //! Set the split limit - needed for unit testing inline void SetLimit(unsigned int l) @@ -119,14 +116,12 @@ public: inline unsigned int GetLimit() const {return LIMIT;} -public: - // ------------------------------------------------------------------- /** Executes the post processing step on the given imported data. * At the moment a process is not supposed to fail. * @param pScene The imported data to work at. */ - void Execute( aiScene* pScene); + void Execute( aiScene* pScene) override; // ------------------------------------------------------------------- //! Apply the algorithm to a given mesh @@ -144,36 +139,31 @@ public: unsigned int LIMIT; }; - // --------------------------------------------------------------------------- /** Post-processing filter to split large meshes into sub-meshes * * Applied AFTER the JoinVertices-Step occurs. * Returns UNIQUE vertices, splits by vertex number. */ -class ASSIMP_API SplitLargeMeshesProcess_Vertex : public BaseProcess -{ +class ASSIMP_API SplitLargeMeshesProcess_Vertex : public BaseProcess { public: - SplitLargeMeshesProcess_Vertex(); - ~SplitLargeMeshesProcess_Vertex(); + ~SplitLargeMeshesProcess_Vertex() override = default; -public: // ------------------------------------------------------------------- /** Returns whether the processing step is present in the given flag field. * @param pFlags The processing flags the importer was called with. A bitwise * combination of #aiPostProcessSteps. * @return true if the process is present in this flag fields, false if not. */ - bool IsActive( unsigned int pFlags) const; + bool IsActive( unsigned int pFlags) const override; // ------------------------------------------------------------------- /** Called prior to ExecuteOnScene(). * The function is a request to the process to update its configuration * basing on the Importer's configuration property list. */ - virtual void SetupProperties(const Importer* pImp); - + void SetupProperties(const Importer* pImp) override; //! Set the split limit - needed for unit testing inline void SetLimit(unsigned int l) @@ -183,14 +173,12 @@ public: inline unsigned int GetLimit() const {return LIMIT;} -public: - // ------------------------------------------------------------------- /** Executes the post processing step on the given imported data. * At the moment a process is not supposed to fail. * @param pScene The imported data to work at. */ - void Execute( aiScene* pScene); + void Execute( aiScene* pScene) override; // ------------------------------------------------------------------- //! Apply the algorithm to a given mesh diff --git a/code/PostProcessing/TextureTransform.cpp b/code/PostProcessing/TextureTransform.cpp index efbf4d2c6..2ed17f390 100644 --- a/code/PostProcessing/TextureTransform.cpp +++ b/code/PostProcessing/TextureTransform.cpp @@ -56,33 +56,24 @@ using namespace Assimp; // ------------------------------------------------------------------------------------------------ // Constructor to be privately used by Importer -TextureTransformStep::TextureTransformStep() : - configFlags() -{ +TextureTransformStep::TextureTransformStep() : configFlags() { // nothing to do here } -// ------------------------------------------------------------------------------------------------ -// Destructor, private as well -TextureTransformStep::~TextureTransformStep() = default; - // ------------------------------------------------------------------------------------------------ // Returns whether the processing step is present in the given flag field. -bool TextureTransformStep::IsActive( unsigned int pFlags) const -{ +bool TextureTransformStep::IsActive( unsigned int pFlags) const { return (pFlags & aiProcess_TransformUVCoords) != 0; } // ------------------------------------------------------------------------------------------------ // Setup properties -void TextureTransformStep::SetupProperties(const Importer* pImp) -{ +void TextureTransformStep::SetupProperties(const Importer* pImp) { configFlags = pImp->GetPropertyInteger(AI_CONFIG_PP_TUV_EVALUATE,AI_UVTRAFO_ALL); } // ------------------------------------------------------------------------------------------------ -void TextureTransformStep::PreProcessUVTransform(STransformVecInfo& info) -{ +void TextureTransformStep::PreProcessUVTransform(STransformVecInfo& info) { /* This function tries to simplify the input UV transformation. * That's very important as it allows us to reduce the number * of output UV channels. The order in which the transformations @@ -90,7 +81,7 @@ void TextureTransformStep::PreProcessUVTransform(STransformVecInfo& info) */ int rounded; - char szTemp[512]; + char szTemp[512] = {}; /* Optimize the rotation angle. That's slightly difficult as * we have an inprecise floating-point number (when comparing @@ -98,12 +89,10 @@ void TextureTransformStep::PreProcessUVTransform(STransformVecInfo& info) * an epsilon of 5 degrees). If there is a rotation value, we can't * perform any further optimizations. */ - if (info.mRotation) - { + if (info.mRotation) { float out = info.mRotation; rounded = static_cast((info.mRotation / static_cast(AI_MATH_TWO_PI))); - if (rounded) - { + if (rounded) { out -= rounded * static_cast(AI_MATH_PI); ASSIMP_LOG_INFO("Texture coordinate rotation ", info.mRotation, " can be simplified to ", out); } @@ -187,8 +176,7 @@ void TextureTransformStep::PreProcessUVTransform(STransformVecInfo& info) } // ------------------------------------------------------------------------------------------------ -void UpdateUVIndex(const std::list& l, unsigned int n) -{ +void UpdateUVIndex(const std::list& l, unsigned int n) { // Don't set if == 0 && wasn't set before for (std::list::const_iterator it = l.begin();it != l.end(); ++it) { const TTUpdateInfo& info = *it; @@ -203,8 +191,7 @@ void UpdateUVIndex(const std::list& l, unsigned int n) } // ------------------------------------------------------------------------------------------------ -inline const char* MappingModeToChar(aiTextureMapMode map) -{ +inline static const char* MappingModeToChar(aiTextureMapMode map) { if (aiTextureMapMode_Wrap == map) return "-w"; @@ -215,8 +202,7 @@ inline const char* MappingModeToChar(aiTextureMapMode map) } // ------------------------------------------------------------------------------------------------ -void TextureTransformStep::Execute( aiScene* pScene) -{ +void TextureTransformStep::Execute( aiScene* pScene) { ASSIMP_LOG_DEBUG("TransformUVCoordsProcess begin"); diff --git a/code/PostProcessing/TextureTransform.h b/code/PostProcessing/TextureTransform.h index c1cccf8ef..c9f0480ba 100644 --- a/code/PostProcessing/TextureTransform.h +++ b/code/PostProcessing/TextureTransform.h @@ -193,28 +193,23 @@ struct STransformVecInfo : public aiUVTransform { /** Helper step to compute final UV coordinate sets if there are scalings * or rotations in the original data read from the file. */ -class TextureTransformStep : public BaseProcess -{ +class TextureTransformStep : public BaseProcess { public: - + // ------------------------------------------------------------------- + /// The default class constructor / destructor. TextureTransformStep(); - ~TextureTransformStep(); - -public: + ~TextureTransformStep() override = default; // ------------------------------------------------------------------- - bool IsActive( unsigned int pFlags) const; + bool IsActive( unsigned int pFlags) const override; // ------------------------------------------------------------------- - void Execute( aiScene* pScene); + void Execute( aiScene* pScene) override; // ------------------------------------------------------------------- - void SetupProperties(const Importer* pImp); - + void SetupProperties(const Importer* pImp) override; protected: - - // ------------------------------------------------------------------- /** Preprocess a specific UV transformation setup * @@ -223,10 +218,9 @@ protected: void PreProcessUVTransform(STransformVecInfo& info); private: - unsigned int configFlags; }; - -} + +} // namespace Assimp #endif //! AI_TEXTURE_TRANSFORM_H_INCLUDED diff --git a/code/PostProcessing/TriangulateProcess.cpp b/code/PostProcessing/TriangulateProcess.cpp index 52e760361..52cfa66bf 100644 --- a/code/PostProcessing/TriangulateProcess.cpp +++ b/code/PostProcessing/TriangulateProcess.cpp @@ -156,15 +156,6 @@ namespace { } - -// ------------------------------------------------------------------------------------------------ -// Constructor to be privately used by Importer -TriangulateProcess::TriangulateProcess() = default; - -// ------------------------------------------------------------------------------------------------ -// Destructor, private as well -TriangulateProcess::~TriangulateProcess() = default; - // ------------------------------------------------------------------------------------------------ // Returns whether the processing step is present in the given flag field. bool TriangulateProcess::IsActive( unsigned int pFlags) const @@ -468,6 +459,21 @@ bool TriangulateProcess::TriangulateMesh( aiMesh* pMesh) continue; } + // Skip when three point is in a line + aiVector2D left = *pnt0 - *pnt1; + aiVector2D right = *pnt2 - *pnt1; + + left.Normalize(); + right.Normalize(); + auto mul = left * right; + + // if the angle is 0 or 180 + if (std::abs(mul - 1.f) < ai_epsilon || std::abs(mul + 1.f) < ai_epsilon) { + // skip this ear + ASSIMP_LOG_WARN("Skip a ear, due to its angle is near 0 or 180."); + continue; + } + // and no other point may be contained in this triangle for ( tmp = 0; tmp < max; ++tmp) { diff --git a/code/PostProcessing/TriangulateProcess.h b/code/PostProcessing/TriangulateProcess.h index ed5f4a587..ac31e4377 100644 --- a/code/PostProcessing/TriangulateProcess.h +++ b/code/PostProcessing/TriangulateProcess.h @@ -61,8 +61,10 @@ namespace Assimp { */ class ASSIMP_API TriangulateProcess : public BaseProcess { public: - TriangulateProcess(); - ~TriangulateProcess(); + // ------------------------------------------------------------------- + /// The default class constructor / destructor. + TriangulateProcess() = default; + ~TriangulateProcess() override = default; // ------------------------------------------------------------------- /** Returns whether the processing step is present in the given flag field. @@ -70,14 +72,14 @@ public: * combination of #aiPostProcessSteps. * @return true if the process is present in this flag fields, false if not. */ - bool IsActive( unsigned int pFlags) const; + bool IsActive( unsigned int pFlags) const override; // ------------------------------------------------------------------- /** Executes the post processing step on the given imported data. * At the moment a process is not supposed to fail. * @param pScene The imported data to work at. */ - void Execute( aiScene* pScene); + void Execute( aiScene* pScene) override; // ------------------------------------------------------------------- /** Triangulates the given mesh. diff --git a/code/PostProcessing/ValidateDataStructure.cpp b/code/PostProcessing/ValidateDataStructure.cpp index d234e220b..e31054972 100644 --- a/code/PostProcessing/ValidateDataStructure.cpp +++ b/code/PostProcessing/ValidateDataStructure.cpp @@ -60,12 +60,7 @@ using namespace Assimp; // ------------------------------------------------------------------------------------------------ // Constructor to be privately used by Importer -ValidateDSProcess::ValidateDSProcess() : - mScene() {} - -// ------------------------------------------------------------------------------------------------ -// Destructor, private as well -ValidateDSProcess::~ValidateDSProcess() = default; +ValidateDSProcess::ValidateDSProcess() : mScene(nullptr) {} // ------------------------------------------------------------------------------------------------ // Returns whether the processing step is present in the given flag field. @@ -916,7 +911,12 @@ void ValidateDSProcess::Validate(const aiNode *pNode) { nodeName, pNode->mNumChildren); } for (unsigned int i = 0; i < pNode->mNumChildren; ++i) { - Validate(pNode->mChildren[i]); + const aiNode *pChild = pNode->mChildren[i]; + Validate(pChild); + if (pChild->mParent != pNode) { + const char *parentName = (pChild->mParent != nullptr) ? pChild->mParent->mName.C_Str() : "null"; + ReportError("aiNode \"%s\" child %i \"%s\" parent is someone else: \"%s\"", pNode->mName.C_Str(), i, pChild->mName.C_Str(), parentName); + } } } } diff --git a/code/PostProcessing/ValidateDataStructure.h b/code/PostProcessing/ValidateDataStructure.h index 077a47b70..9cfd4ced1 100644 --- a/code/PostProcessing/ValidateDataStructure.h +++ b/code/PostProcessing/ValidateDataStructure.h @@ -69,22 +69,20 @@ namespace Assimp { /** Validates the whole ASSIMP scene data structure for correctness. * ImportErrorException is thrown of the scene is corrupt.*/ // -------------------------------------------------------------------------------------- -class ValidateDSProcess : public BaseProcess -{ +class ValidateDSProcess : public BaseProcess { public: - + // ------------------------------------------------------------------- + /// The default class constructor / destructor. ValidateDSProcess(); - ~ValidateDSProcess(); - -public: - // ------------------------------------------------------------------- - bool IsActive( unsigned int pFlags) const; + ~ValidateDSProcess() override = default; // ------------------------------------------------------------------- - void Execute( aiScene* pScene); + bool IsActive( unsigned int pFlags) const override; + + // ------------------------------------------------------------------- + void Execute( aiScene* pScene) override; protected: - // ------------------------------------------------------------------- /** Report a validation error. This will throw an exception, * control won't return. diff --git a/contrib/draco/.cmake-format.py b/contrib/draco/.cmake-format.py index 64f2495b4..5b36f67aa 100644 --- a/contrib/draco/.cmake-format.py +++ b/contrib/draco/.cmake-format.py @@ -1,102 +1,137 @@ -# Generated with cmake-format 0.5.1 -# How wide to allow formatted cmake files -line_width = 80 - -# How many spaces to tab for indent -tab_size = 2 - -# If arglists are longer than this, break them always -max_subargs_per_line = 10 - -# If true, separate flow control names from their parentheses with a space -separate_ctrl_name_with_space = False - -# If true, separate function names from parentheses with a space -separate_fn_name_with_space = False - -# If a statement is wrapped to more than one line, than dangle the closing -# parenthesis on its own line -dangle_parens = False - -# What character to use for bulleted lists -bullet_char = '*' - -# What character to use as punctuation after numerals in an enumerated list -enum_char = '.' - -# What style line endings to use in the output. -line_ending = u'unix' - -# Format command names consistently as 'lower' or 'upper' case -command_case = u'lower' - -# Format keywords consistently as 'lower' or 'upper' case -keyword_case = u'unchanged' - -# Specify structure for custom cmake functions -additional_commands = { - "foo": { - "flags": [ - "BAR", - "BAZ" - ], - "kwargs": { - "HEADERS": "*", - "DEPENDS": "*", - "SOURCES": "*" - } +with section('parse'): + # Specify structure for custom cmake functions + additional_commands = { + 'draco_add_emscripten_executable': { + 'kwargs': { + 'NAME': '*', + 'SOURCES': '*', + 'OUTPUT_NAME': '*', + 'DEFINES': '*', + 'INCLUDES': '*', + 'COMPILE_FLAGS': '*', + 'LINK_FLAGS': '*', + 'OBJLIB_DEPS': '*', + 'LIB_DEPS': '*', + 'GLUE_PATH': '*', + 'PRE_LINK_JS_SOURCES': '*', + 'POST_LINK_JS_SOURCES': '*', + 'FEATURES': '*', + }, + 'pargs': 0, + }, + 'draco_add_executable': { + 'kwargs': { + 'NAME': '*', + 'SOURCES': '*', + 'OUTPUT_NAME': '*', + 'TEST': 0, + 'DEFINES': '*', + 'INCLUDES': '*', + 'COMPILE_FLAGS': '*', + 'LINK_FLAGS': '*', + 'OBJLIB_DEPS': '*', + 'LIB_DEPS': '*', + }, + 'pargs': 0, + }, + 'draco_add_library': { + 'kwargs': { + 'NAME': '*', + 'TYPE': '*', + 'SOURCES': '*', + 'TEST': 0, + 'OUTPUT_NAME': '*', + 'DEFINES': '*', + 'INCLUDES': '*', + 'COMPILE_FLAGS': '*', + 'LINK_FLAGS': '*', + 'OBJLIB_DEPS': '*', + 'LIB_DEPS': '*', + 'PUBLIC_INCLUDES': '*', + }, + 'pargs': 0, + }, + 'draco_generate_emscripten_glue': { + 'kwargs': { + 'INPUT_IDL': '*', + 'OUTPUT_PATH': '*', + }, + 'pargs': 0, + }, + 'draco_get_required_emscripten_flags': { + 'kwargs': { + 'FLAG_LIST_VAR_COMPILER': '*', + 'FLAG_LIST_VAR_LINKER': '*', + }, + 'pargs': 0, + }, + 'draco_option': { + 'kwargs': { + 'NAME': '*', + 'HELPSTRING': '*', + 'VALUE': '*', + }, + 'pargs': 0, + }, + # Rules for built in CMake commands and those from dependencies. + 'list': { + 'kwargs': { + 'APPEND': '*', + 'FILTER': '*', + 'FIND': '*', + 'GET': '*', + 'INSERT': '*', + 'JOIN': '*', + 'LENGTH': '*', + 'POP_BACK': '*', + 'POP_FRONT': '*', + 'PREPEND': '*', + 'REMOVE_DUPLICATES': '*', + 'REMOVE_ITEM': '*', + 'REVERSE': '*', + 'SORT': '*', + 'SUBLIST': '*', + 'TRANSFORM': '*', + }, + }, + 'protobuf_generate': { + 'kwargs': { + 'IMPORT_DIRS': '*', + 'LANGUAGE': '*', + 'OUT_VAR': '*', + 'PROTOC_OUT_DIR': '*', + 'PROTOS': '*', + }, + }, } -} -# A list of command names which should always be wrapped -always_wrap = [] +with section('format'): + # Formatting options. -# Specify the order of wrapping algorithms during successive reflow attempts -algorithm_order = [0, 1, 2, 3, 4] + # How wide to allow formatted cmake files + line_width = 80 -# If true, the argument lists which are known to be sortable will be sorted -# lexicographicall -autosort = False + # How many spaces to tab for indent + tab_size = 2 -# enable comment markup parsing and reflow -enable_markup = True + # If true, separate flow control names from their parentheses with a space + separate_ctrl_name_with_space = False -# If comment markup is enabled, don't reflow the first comment block in -# eachlistfile. Use this to preserve formatting of your -# copyright/licensestatements. -first_comment_is_literal = False + # If true, separate function names from parentheses with a space + separate_fn_name_with_space = False -# If comment markup is enabled, don't reflow any comment block which matchesthis -# (regex) pattern. Default is `None` (disabled). -literal_comment_pattern = None + # If a statement is wrapped to more than one line, than dangle the closing + # parenthesis on its own line. + dangle_parens = False -# Regular expression to match preformat fences in comments -# default=r'^\s*([`~]{3}[`~]*)(.*)$' -fence_pattern = u'^\\s*([`~]{3}[`~]*)(.*)$' + # Do not sort argument lists. + enable_sort = False -# Regular expression to match rulers in comments -# default=r'^\s*[^\w\s]{3}.*[^\w\s]{3}$' -ruler_pattern = u'^\\s*[^\\w\\s]{3}.*[^\\w\\s]{3}$' + # What style line endings to use in the output. + line_ending = 'unix' -# If true, emit the unicode byte-order mark (BOM) at the start of the file -emit_byteorder_mark = False + # Format command names consistently as 'lower' or 'upper' case + command_case = 'canonical' -# If a comment line starts with at least this many consecutive hash characters, -# then don't lstrip() them off. This allows for lazy hash rulers where the first -# hash char is not separated by space -hashruler_min_length = 10 - -# If true, then insert a space between the first hash char and remaining hash -# chars in a hash ruler, and normalize its length to fill the column -canonicalize_hashrulers = True - -# Specify the encoding of the input file. Defaults to utf-8. -input_encoding = u'utf-8' - -# Specify the encoding of the output file. Defaults to utf-8. Note that cmake -# only claims to support utf-8 so be careful when using anything else -output_encoding = u'utf-8' - -# A dictionary containing any per-command configuration overrides. Currently -# only `command_case` is supported. -per_command = {} + # Format keywords consistently as 'lower' or 'upper' case + keyword_case = 'upper' diff --git a/contrib/draco/.gitattributes b/contrib/draco/.gitattributes new file mode 100644 index 000000000..96acfc612 --- /dev/null +++ b/contrib/draco/.gitattributes @@ -0,0 +1 @@ +*.obj eol=lf \ No newline at end of file diff --git a/contrib/draco/.gitmodules b/contrib/draco/.gitmodules new file mode 100644 index 000000000..25f0a1c03 --- /dev/null +++ b/contrib/draco/.gitmodules @@ -0,0 +1,12 @@ +[submodule "third_party/googletest"] + path = third_party/googletest + url = https://github.com/google/googletest.git +[submodule "third_party/eigen"] + path = third_party/eigen + url = https://gitlab.com/libeigen/eigen.git +[submodule "third_party/tinygltf"] + path = third_party/tinygltf + url = https://github.com/syoyo/tinygltf.git +[submodule "third_party/filesystem"] + path = third_party/filesystem + url = https://github.com/gulrak/filesystem diff --git a/contrib/draco/BUILDING.md b/contrib/draco/BUILDING.md index d33917b88..340b2b83b 100644 --- a/contrib/draco/BUILDING.md +++ b/contrib/draco/BUILDING.md @@ -4,8 +4,10 @@ _**Contents**_ * [Mac OS X](#mac-os-x) * [Windows](#windows) * [CMake Build Configuration](#cmake-build-configuration) + * [Transcoder](#transcoder) * [Debugging and Optimization](#debugging-and-optimization) * [Googletest Integration](#googletest-integration) + * [Third Party Libraries](#third-party-libraries) * [Javascript Encoder/Decoder](#javascript-encoderdecoder) * [WebAssembly Decoder](#webassembly-decoder) * [WebAssembly Mesh Only Decoder](#webassembly-mesh-only-decoder) @@ -72,6 +74,43 @@ C:\Users\nobody> cmake ../ -G "Visual Studio 16 2019" -A x64 CMake Build Configuration ------------------------- +Transcoder +---------- + +Before attempting to build Draco with transcoding support you must run an +additional Git command to obtain the submodules: + +~~~~~ bash +# Run this command from within your Draco clone. +$ git submodule update --init +# See below if you prefer to use existing versions of Draco dependencies. +~~~~~ + +In order to build the `draco_transcoder` target, the transcoding support needs +to be explicitly enabled when you run `cmake`, for example: + +~~~~~ bash +$ cmake ../ -DDRACO_TRANSCODER_SUPPORTED=ON +~~~~~ + +The above option is currently not compatible with our Javascript or WebAssembly +builds but all other use cases are supported. Note that binaries and libraries +built with the transcoder support may result in increased binary sizes of the +produced libraries and executables compared to the default CMake settings. + +The following CMake variables can be used to configure Draco to use local +copies of third party dependencies instead of git submodules. + +- `DRACO_EIGEN_PATH`: this path must contain an Eigen directory that includes + the Eigen sources. +- `DRACO_FILESYSTEM_PATH`: this path must contain the ghc directory where the + filesystem includes are located. +- `DRACO_TINYGLTF_PATH`: this path must contain tiny_gltf.h and its + dependencies. + +When not specified the Draco build requires the presence of the submodules that +are stored within `draco/third_party`. + Debugging and Optimization -------------------------- @@ -114,17 +153,52 @@ $ cmake ../ -DDRACO_SANITIZE=address Googletest Integration ---------------------- -Draco includes testing support built using Googletest. To enable Googletest unit -test support the DRACO_TESTS cmake variable must be turned on at cmake -generation time: +Draco includes testing support built using Googletest. The Googletest repository +is included as a submodule of the Draco git repository. Run the following +command to clone the Googletest repository: + +~~~~~ bash +$ git submodule update --init +~~~~~ + +To enable Googletest unit test support the DRACO_TESTS cmake variable must be +turned on at cmake generation time: ~~~~~ bash $ cmake ../ -DDRACO_TESTS=ON ~~~~~ -When cmake is used as shown in the above example the googletest directory must -be a sibling of the Draco repository root directory. To run the tests execute -`draco_tests` from your build output directory. +To run the tests execute `draco_tests` from your build output directory: + +~~~~~ bash +$ ./draco_tests +~~~~~ + +Draco can be configured to use a local Googletest installation. The +`DRACO_GOOGLETEST_PATH` variable overrides the behavior described above and +configures Draco to use the Googletest at the specified path. + +Third Party Libraries +--------------------- + +When Draco is built with transcoding and/or testing support enabled the project +has dependencies on third party libraries: + +- [Eigen](https://eigen.tuxfamily.org/) + - Provides various math utilites. +- [Googletest](https://github.com/google/googletest) + - Provides testing support. +- [Gulrak/filesystem](https://github.com/gulrak/filesystem) + - Provides C++17 std::filesystem emulation for pre-C++17 environments. +- [TinyGLTF](https://github.com/syoyo/tinygltf) + - Provides GLTF I/O support. + +These dependencies are managed as Git submodules. To obtain the dependencies +run the following command in your Draco repository: + +~~~~~ bash +$ git submodule update --init +~~~~~ WebAssembly Decoder ------------------- diff --git a/contrib/draco/CMakeLists.txt b/contrib/draco/CMakeLists.txt index 6ea9b21fd..a93267d25 100644 --- a/contrib/draco/CMakeLists.txt +++ b/contrib/draco/CMakeLists.txt @@ -1,7 +1,18 @@ -cmake_minimum_required(VERSION 3.12 FATAL_ERROR) +# Copyright 2021 The Draco Authors +# +# 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. -# Draco requires C++11. -set(CMAKE_CXX_STANDARD 11) +cmake_minimum_required(VERSION 3.12 FATAL_ERROR) project(draco C CXX) if(NOT CMAKE_BUILD_TYPE) @@ -10,21 +21,23 @@ endif() set(draco_root "${CMAKE_CURRENT_SOURCE_DIR}") set(draco_src_root "${draco_root}/src/draco") -set(draco_build "${Assimp_BINARY_DIR}") +set(draco_build "${CMAKE_BINARY_DIR}") if("${draco_root}" STREQUAL "${draco_build}") message( - FATAL_ERROR "Building from within the Draco source tree is not supported.\n" - "Hint: Run these commands\n" - "$ rm -rf CMakeCache.txt CMakeFiles\n" - "$ mkdir -p ../draco_build\n" "$ cd ../draco_build\n" - "And re-run CMake from the draco_build directory.") + FATAL_ERROR + "Building from within the Draco source tree is not supported.\n" + "Hint: Run these commands\n" + "$ rm -rf CMakeCache.txt CMakeFiles\n" + "$ mkdir -p ../draco_build\n" + "$ cd ../draco_build\n" + "And re-run CMake from the draco_build directory.") endif() -include(CMakePackageConfigHelpers) include(FindPythonInterp) include("${draco_root}/cmake/draco_build_definitions.cmake") include("${draco_root}/cmake/draco_cpu_detection.cmake") +include("${draco_root}/cmake/draco_dependencies.cmake") include("${draco_root}/cmake/draco_emscripten.cmake") include("${draco_root}/cmake/draco_flags.cmake") include("${draco_root}/cmake/draco_helpers.cmake") @@ -49,6 +62,7 @@ draco_track_configuration_variable(DRACO_GENERATED_SOURCES_DIRECTORY) # Controls use of std::mutex and absl::Mutex in ThreadPool. draco_track_configuration_variable(DRACO_THREADPOOL_USE_STD_MUTEX) + if(DRACO_VERBOSE) draco_dump_cmake_flag_variables() draco_dump_tracked_configuration_variables() @@ -68,29 +82,32 @@ draco_reset_target_lists() draco_setup_options() draco_set_build_definitions() draco_set_cxx_flags() +draco_set_exe_linker_flags() draco_generate_features_h() # Draco source file listing variables. -list(APPEND draco_attributes_sources - "${draco_src_root}/attributes/attribute_octahedron_transform.cc" - "${draco_src_root}/attributes/attribute_octahedron_transform.h" - "${draco_src_root}/attributes/attribute_quantization_transform.cc" - "${draco_src_root}/attributes/attribute_quantization_transform.h" - "${draco_src_root}/attributes/attribute_transform.cc" - "${draco_src_root}/attributes/attribute_transform.h" - "${draco_src_root}/attributes/attribute_transform_data.h" - "${draco_src_root}/attributes/attribute_transform_type.h" - "${draco_src_root}/attributes/geometry_attribute.cc" - "${draco_src_root}/attributes/geometry_attribute.h" - "${draco_src_root}/attributes/geometry_indices.h" - "${draco_src_root}/attributes/point_attribute.cc" - "${draco_src_root}/attributes/point_attribute.h") +list( + APPEND draco_attributes_sources + "${draco_src_root}/attributes/attribute_octahedron_transform.cc" + "${draco_src_root}/attributes/attribute_octahedron_transform.h" + "${draco_src_root}/attributes/attribute_quantization_transform.cc" + "${draco_src_root}/attributes/attribute_quantization_transform.h" + "${draco_src_root}/attributes/attribute_transform.cc" + "${draco_src_root}/attributes/attribute_transform.h" + "${draco_src_root}/attributes/attribute_transform_data.h" + "${draco_src_root}/attributes/attribute_transform_type.h" + "${draco_src_root}/attributes/geometry_attribute.cc" + "${draco_src_root}/attributes/geometry_attribute.h" + "${draco_src_root}/attributes/geometry_indices.h" + "${draco_src_root}/attributes/point_attribute.cc" + "${draco_src_root}/attributes/point_attribute.h") list( APPEND draco_compression_attributes_dec_sources "${draco_src_root}/compression/attributes/attributes_decoder.cc" "${draco_src_root}/compression/attributes/attributes_decoder.h" + "${draco_src_root}/compression/attributes/attributes_decoder_interface.h" "${draco_src_root}/compression/attributes/kd_tree_attributes_decoder.cc" "${draco_src_root}/compression/attributes/kd_tree_attributes_decoder.h" "${draco_src_root}/compression/attributes/kd_tree_attributes_shared.h" @@ -107,7 +124,7 @@ list( "${draco_src_root}/compression/attributes/sequential_normal_attribute_decoder.h" "${draco_src_root}/compression/attributes/sequential_quantization_attribute_decoder.cc" "${draco_src_root}/compression/attributes/sequential_quantization_attribute_decoder.h" - ) +) list( APPEND @@ -128,7 +145,7 @@ list( "${draco_src_root}/compression/attributes/sequential_normal_attribute_encoder.h" "${draco_src_root}/compression/attributes/sequential_quantization_attribute_encoder.cc" "${draco_src_root}/compression/attributes/sequential_quantization_attribute_encoder.h" - ) +) list( @@ -160,7 +177,7 @@ list( "${draco_src_root}/compression/attributes/prediction_schemes/prediction_scheme_normal_octahedron_transform_base.h" "${draco_src_root}/compression/attributes/prediction_schemes/prediction_scheme_wrap_decoding_transform.h" "${draco_src_root}/compression/attributes/prediction_schemes/prediction_scheme_wrap_transform_base.h" - ) +) list( APPEND @@ -192,7 +209,7 @@ list( "${draco_src_root}/compression/attributes/prediction_schemes/prediction_scheme_normal_octahedron_transform_base.h" "${draco_src_root}/compression/attributes/prediction_schemes/prediction_scheme_wrap_encoding_transform.h" "${draco_src_root}/compression/attributes/prediction_schemes/prediction_scheme_wrap_transform_base.h" - ) +) list( APPEND @@ -217,27 +234,34 @@ list( "${draco_src_root}/compression/bit_coders/symbol_bit_encoder.cc" "${draco_src_root}/compression/bit_coders/symbol_bit_encoder.h") -list(APPEND draco_enc_config_sources - "${draco_src_root}/compression/config/compression_shared.h" - "${draco_src_root}/compression/config/draco_options.h" - "${draco_src_root}/compression/config/encoder_options.h" - "${draco_src_root}/compression/config/encoding_features.h") +list( + APPEND draco_enc_config_sources + "${draco_src_root}/compression/config/compression_shared.h" + "${draco_src_root}/compression/config/draco_options.h" + "${draco_src_root}/compression/config/encoder_options.h" + "${draco_src_root}/compression/config/encoding_features.h") -list(APPEND draco_dec_config_sources - "${draco_src_root}/compression/config/compression_shared.h" - "${draco_src_root}/compression/config/decoder_options.h" - "${draco_src_root}/compression/config/draco_options.h") +list( + APPEND draco_dec_config_sources + "${draco_src_root}/compression/config/compression_shared.h" + "${draco_src_root}/compression/config/decoder_options.h" + "${draco_src_root}/compression/config/draco_options.h") + +list(APPEND draco_compression_options_sources + "${draco_src_root}/compression/draco_compression_options.cc" + "${draco_src_root}/compression/draco_compression_options.h") list(APPEND draco_compression_decode_sources "${draco_src_root}/compression/decode.cc" "${draco_src_root}/compression/decode.h") -list(APPEND draco_compression_encode_sources - "${draco_src_root}/compression/encode.cc" - "${draco_src_root}/compression/encode.h" - "${draco_src_root}/compression/encode_base.h" - "${draco_src_root}/compression/expert_encode.cc" - "${draco_src_root}/compression/expert_encode.h") +list( + APPEND draco_compression_encode_sources + "${draco_src_root}/compression/encode.cc" + "${draco_src_root}/compression/encode.h" + "${draco_src_root}/compression/encode_base.h" + "${draco_src_root}/compression/expert_encode.cc" + "${draco_src_root}/compression/expert_encode.h") list( APPEND @@ -291,7 +315,7 @@ list( "${draco_src_root}/compression/point_cloud/point_cloud_kd_tree_decoder.h" "${draco_src_root}/compression/point_cloud/point_cloud_sequential_decoder.cc" "${draco_src_root}/compression/point_cloud/point_cloud_sequential_decoder.h" - ) +) list( APPEND @@ -302,112 +326,126 @@ list( "${draco_src_root}/compression/point_cloud/point_cloud_kd_tree_encoder.h" "${draco_src_root}/compression/point_cloud/point_cloud_sequential_encoder.cc" "${draco_src_root}/compression/point_cloud/point_cloud_sequential_encoder.h" - ) +) -list(APPEND draco_compression_entropy_sources - "${draco_src_root}/compression/entropy/ans.h" - "${draco_src_root}/compression/entropy/rans_symbol_coding.h" - "${draco_src_root}/compression/entropy/rans_symbol_decoder.h" - "${draco_src_root}/compression/entropy/rans_symbol_encoder.h" - "${draco_src_root}/compression/entropy/shannon_entropy.cc" - "${draco_src_root}/compression/entropy/shannon_entropy.h" - "${draco_src_root}/compression/entropy/symbol_decoding.cc" - "${draco_src_root}/compression/entropy/symbol_decoding.h" - "${draco_src_root}/compression/entropy/symbol_encoding.cc" - "${draco_src_root}/compression/entropy/symbol_encoding.h") +list( + APPEND draco_compression_entropy_sources + "${draco_src_root}/compression/entropy/ans.h" + "${draco_src_root}/compression/entropy/rans_symbol_coding.h" + "${draco_src_root}/compression/entropy/rans_symbol_decoder.h" + "${draco_src_root}/compression/entropy/rans_symbol_encoder.h" + "${draco_src_root}/compression/entropy/shannon_entropy.cc" + "${draco_src_root}/compression/entropy/shannon_entropy.h" + "${draco_src_root}/compression/entropy/symbol_decoding.cc" + "${draco_src_root}/compression/entropy/symbol_decoding.h" + "${draco_src_root}/compression/entropy/symbol_encoding.cc" + "${draco_src_root}/compression/entropy/symbol_encoding.h") -list(APPEND draco_core_sources - "${draco_src_root}/core/bit_utils.cc" - "${draco_src_root}/core/bit_utils.h" - "${draco_src_root}/core/bounding_box.cc" - "${draco_src_root}/core/bounding_box.h" - "${draco_src_root}/core/cycle_timer.cc" - "${draco_src_root}/core/cycle_timer.h" - "${draco_src_root}/core/data_buffer.cc" - "${draco_src_root}/core/data_buffer.h" - "${draco_src_root}/core/decoder_buffer.cc" - "${draco_src_root}/core/decoder_buffer.h" - "${draco_src_root}/core/divide.cc" - "${draco_src_root}/core/divide.h" - "${draco_src_root}/core/draco_index_type.h" - "${draco_src_root}/core/draco_index_type_vector.h" - "${draco_src_root}/core/draco_types.cc" - "${draco_src_root}/core/draco_types.h" - "${draco_src_root}/core/encoder_buffer.cc" - "${draco_src_root}/core/encoder_buffer.h" - "${draco_src_root}/core/hash_utils.cc" - "${draco_src_root}/core/hash_utils.h" - "${draco_src_root}/core/macros.h" - "${draco_src_root}/core/math_utils.h" - "${draco_src_root}/core/options.cc" - "${draco_src_root}/core/options.h" - "${draco_src_root}/core/quantization_utils.cc" - "${draco_src_root}/core/quantization_utils.h" - "${draco_src_root}/core/status.h" - "${draco_src_root}/core/status_or.h" - "${draco_src_root}/core/varint_decoding.h" - "${draco_src_root}/core/varint_encoding.h" - "${draco_src_root}/core/vector_d.h") +list( + APPEND draco_core_sources + "${draco_src_root}/core/bit_utils.cc" + "${draco_src_root}/core/bit_utils.h" + "${draco_src_root}/core/bounding_box.cc" + "${draco_src_root}/core/bounding_box.h" + "${draco_src_root}/core/constants.h" + "${draco_src_root}/core/cycle_timer.cc" + "${draco_src_root}/core/cycle_timer.h" + "${draco_src_root}/core/data_buffer.cc" + "${draco_src_root}/core/data_buffer.h" + "${draco_src_root}/core/decoder_buffer.cc" + "${draco_src_root}/core/decoder_buffer.h" + "${draco_src_root}/core/divide.cc" + "${draco_src_root}/core/divide.h" + "${draco_src_root}/core/draco_index_type.h" + "${draco_src_root}/core/draco_index_type_vector.h" + "${draco_src_root}/core/draco_types.cc" + "${draco_src_root}/core/draco_types.h" + "${draco_src_root}/core/draco_version.h" + "${draco_src_root}/core/encoder_buffer.cc" + "${draco_src_root}/core/encoder_buffer.h" + "${draco_src_root}/core/hash_utils.cc" + "${draco_src_root}/core/hash_utils.h" + "${draco_src_root}/core/macros.h" + "${draco_src_root}/core/math_utils.h" + "${draco_src_root}/core/options.cc" + "${draco_src_root}/core/options.h" + "${draco_src_root}/core/quantization_utils.cc" + "${draco_src_root}/core/quantization_utils.h" + "${draco_src_root}/core/status.h" + "${draco_src_root}/core/status_or.h" + "${draco_src_root}/core/varint_decoding.h" + "${draco_src_root}/core/varint_encoding.h" + "${draco_src_root}/core/vector_d.h") -list(APPEND draco_io_sources - "${draco_src_root}/io/file_reader_factory.cc" - "${draco_src_root}/io/file_reader_factory.h" - "${draco_src_root}/io/file_reader_interface.h" - "${draco_src_root}/io/file_utils.cc" - "${draco_src_root}/io/file_utils.h" - "${draco_src_root}/io/file_writer_factory.cc" - "${draco_src_root}/io/file_writer_factory.h" - "${draco_src_root}/io/file_writer_interface.h" - "${draco_src_root}/io/file_writer_utils.h" - "${draco_src_root}/io/file_writer_utils.cc" - "${draco_src_root}/io/mesh_io.cc" - "${draco_src_root}/io/mesh_io.h" - "${draco_src_root}/io/obj_decoder.cc" - "${draco_src_root}/io/obj_decoder.h" - "${draco_src_root}/io/obj_encoder.cc" - "${draco_src_root}/io/obj_encoder.h" - "${draco_src_root}/io/parser_utils.cc" - "${draco_src_root}/io/parser_utils.h" - "${draco_src_root}/io/ply_decoder.cc" - "${draco_src_root}/io/ply_decoder.h" - "${draco_src_root}/io/ply_encoder.cc" - "${draco_src_root}/io/ply_encoder.h" - "${draco_src_root}/io/ply_property_reader.h" - "${draco_src_root}/io/ply_property_writer.h" - "${draco_src_root}/io/ply_reader.cc" - "${draco_src_root}/io/ply_reader.h" - "${draco_src_root}/io/point_cloud_io.cc" - "${draco_src_root}/io/point_cloud_io.h" - "${draco_src_root}/io/stdio_file_reader.cc" - "${draco_src_root}/io/stdio_file_reader.h" - "${draco_src_root}/io/stdio_file_writer.cc" - "${draco_src_root}/io/stdio_file_writer.h") +list( + APPEND draco_io_sources + "${draco_src_root}/io/file_reader_factory.cc" + "${draco_src_root}/io/file_reader_factory.h" + "${draco_src_root}/io/file_reader_interface.h" + "${draco_src_root}/io/file_utils.cc" + "${draco_src_root}/io/file_utils.h" + "${draco_src_root}/io/file_writer_factory.cc" + "${draco_src_root}/io/file_writer_factory.h" + "${draco_src_root}/io/file_writer_interface.h" + "${draco_src_root}/io/file_writer_utils.h" + "${draco_src_root}/io/file_writer_utils.cc" + "${draco_src_root}/io/mesh_io.cc" + "${draco_src_root}/io/mesh_io.h" + "${draco_src_root}/io/obj_decoder.cc" + "${draco_src_root}/io/obj_decoder.h" + "${draco_src_root}/io/obj_encoder.cc" + "${draco_src_root}/io/obj_encoder.h" + "${draco_src_root}/io/parser_utils.cc" + "${draco_src_root}/io/parser_utils.h" + "${draco_src_root}/io/ply_decoder.cc" + "${draco_src_root}/io/ply_decoder.h" + "${draco_src_root}/io/ply_encoder.cc" + "${draco_src_root}/io/ply_encoder.h" + "${draco_src_root}/io/ply_property_reader.h" + "${draco_src_root}/io/ply_property_writer.h" + "${draco_src_root}/io/ply_reader.cc" + "${draco_src_root}/io/ply_reader.h" + "${draco_src_root}/io/stl_decoder.cc" + "${draco_src_root}/io/stl_decoder.h" + "${draco_src_root}/io/stl_encoder.cc" + "${draco_src_root}/io/stl_encoder.h" + "${draco_src_root}/io/point_cloud_io.cc" + "${draco_src_root}/io/point_cloud_io.h" + "${draco_src_root}/io/stdio_file_reader.cc" + "${draco_src_root}/io/stdio_file_reader.h" + "${draco_src_root}/io/stdio_file_writer.cc" + "${draco_src_root}/io/stdio_file_writer.h") -list(APPEND draco_mesh_sources - "${draco_src_root}/mesh/corner_table.cc" - "${draco_src_root}/mesh/corner_table.h" - "${draco_src_root}/mesh/corner_table_iterators.h" - "${draco_src_root}/mesh/mesh.cc" - "${draco_src_root}/mesh/mesh.h" - "${draco_src_root}/mesh/mesh_are_equivalent.cc" - "${draco_src_root}/mesh/mesh_are_equivalent.h" - "${draco_src_root}/mesh/mesh_attribute_corner_table.cc" - "${draco_src_root}/mesh/mesh_attribute_corner_table.h" - "${draco_src_root}/mesh/mesh_cleanup.cc" - "${draco_src_root}/mesh/mesh_cleanup.h" - "${draco_src_root}/mesh/mesh_misc_functions.cc" - "${draco_src_root}/mesh/mesh_misc_functions.h" - "${draco_src_root}/mesh/mesh_stripifier.cc" - "${draco_src_root}/mesh/mesh_stripifier.h" - "${draco_src_root}/mesh/triangle_soup_mesh_builder.cc" - "${draco_src_root}/mesh/triangle_soup_mesh_builder.h" - "${draco_src_root}/mesh/valence_cache.h") +list( + APPEND draco_mesh_sources + "${draco_src_root}/mesh/corner_table.cc" + "${draco_src_root}/mesh/corner_table.h" + "${draco_src_root}/mesh/corner_table_iterators.h" + "${draco_src_root}/mesh/mesh.cc" + "${draco_src_root}/mesh/mesh.h" + "${draco_src_root}/mesh/mesh_are_equivalent.cc" + "${draco_src_root}/mesh/mesh_are_equivalent.h" + "${draco_src_root}/mesh/mesh_attribute_corner_table.cc" + "${draco_src_root}/mesh/mesh_attribute_corner_table.h" + "${draco_src_root}/mesh/mesh_cleanup.cc" + "${draco_src_root}/mesh/mesh_cleanup.h" + "${draco_src_root}/mesh/mesh_features.cc" + "${draco_src_root}/mesh/mesh_features.h" + "${draco_src_root}/mesh/mesh_indices.h" + "${draco_src_root}/mesh/mesh_misc_functions.cc" + "${draco_src_root}/mesh/mesh_misc_functions.h" + "${draco_src_root}/mesh/mesh_stripifier.cc" + "${draco_src_root}/mesh/mesh_stripifier.h" + "${draco_src_root}/mesh/triangle_soup_mesh_builder.cc" + "${draco_src_root}/mesh/triangle_soup_mesh_builder.h" + "${draco_src_root}/mesh/valence_cache.h") -list(APPEND draco_point_cloud_sources - "${draco_src_root}/point_cloud/point_cloud.cc" - "${draco_src_root}/point_cloud/point_cloud.h" - "${draco_src_root}/point_cloud/point_cloud_builder.cc" - "${draco_src_root}/point_cloud/point_cloud_builder.h") +list( + APPEND draco_point_cloud_sources + "${draco_src_root}/point_cloud/point_cloud.cc" + "${draco_src_root}/point_cloud/point_cloud.h" + "${draco_src_root}/point_cloud/point_cloud_builder.cc" + "${draco_src_root}/point_cloud/point_cloud_builder.h") list( APPEND @@ -424,7 +462,7 @@ list( "${draco_src_root}/compression/point_cloud/algorithms/dynamic_integer_points_kd_tree_decoder.h" "${draco_src_root}/compression/point_cloud/algorithms/float_points_tree_decoder.cc" "${draco_src_root}/compression/point_cloud/algorithms/float_points_tree_decoder.h" - ) +) list( APPEND @@ -433,13 +471,18 @@ list( "${draco_src_root}/compression/point_cloud/algorithms/dynamic_integer_points_kd_tree_encoder.h" "${draco_src_root}/compression/point_cloud/algorithms/float_points_tree_encoder.cc" "${draco_src_root}/compression/point_cloud/algorithms/float_points_tree_encoder.h" - ) +) -list(APPEND draco_metadata_sources - "${draco_src_root}/metadata/geometry_metadata.cc" - "${draco_src_root}/metadata/geometry_metadata.h" - "${draco_src_root}/metadata/metadata.cc" - "${draco_src_root}/metadata/metadata.h") +list( + APPEND draco_metadata_sources + "${draco_src_root}/metadata/geometry_metadata.cc" + "${draco_src_root}/metadata/geometry_metadata.h" + "${draco_src_root}/metadata/metadata.cc" + "${draco_src_root}/metadata/metadata.h" + "${draco_src_root}/metadata/property_table.cc" + "${draco_src_root}/metadata/property_table.h" + "${draco_src_root}/metadata/structural_metadata.cc" + "${draco_src_root}/metadata/structural_metadata.h") list(APPEND draco_metadata_enc_sources "${draco_src_root}/metadata/metadata_encoder.cc" @@ -465,7 +508,7 @@ list( APPEND draco_js_dec_sources "${draco_src_root}/javascript/emscripten/decoder_webidl_wrapper.cc" "${draco_src_root}/javascript/emscripten/draco_decoder_glue_wrapper.cc" - ) +) list( APPEND draco_js_enc_sources @@ -477,14 +520,14 @@ list( draco_animation_js_dec_sources "${draco_src_root}/javascript/emscripten/animation_decoder_webidl_wrapper.cc" "${draco_src_root}/javascript/emscripten/draco_animation_decoder_glue_wrapper.cc" - ) +) list( APPEND draco_animation_js_enc_sources "${draco_src_root}/javascript/emscripten/animation_encoder_webidl_wrapper.cc" "${draco_src_root}/javascript/emscripten/draco_animation_encoder_glue_wrapper.cc" - ) +) list(APPEND draco_unity_plug_sources "${draco_src_root}/unity/draco_unity_plugin.cc" @@ -494,49 +537,133 @@ list(APPEND draco_maya_plug_sources "${draco_src_root}/maya/draco_maya_plugin.cc" "${draco_src_root}/maya/draco_maya_plugin.h") +if(DRACO_TRANSCODER_SUPPORTED) + list( + APPEND draco_animation_sources + "${draco_src_root}/animation/animation.cc" + "${draco_src_root}/animation/animation.h" + "${draco_src_root}/animation/node_animation_data.h" + "${draco_src_root}/animation/skin.cc" + "${draco_src_root}/animation/skin.h") + + list( + APPEND draco_io_sources + "${draco_src_root}/io/gltf_decoder.cc" + "${draco_src_root}/io/gltf_decoder.h" + "${draco_src_root}/io/gltf_encoder.cc" + "${draco_src_root}/io/gltf_encoder.h" + "${draco_src_root}/io/gltf_utils.cc" + "${draco_src_root}/io/gltf_utils.h" + "${draco_src_root}/io/image_compression_options.h" + "${draco_src_root}/io/scene_io.cc" + "${draco_src_root}/io/scene_io.h" + "${draco_src_root}/io/texture_io.cc" + "${draco_src_root}/io/texture_io.h" + "${draco_src_root}/io/tiny_gltf_utils.cc" + "${draco_src_root}/io/tiny_gltf_utils.h") + + list( + APPEND draco_material_sources + "${draco_src_root}/material/material.cc" + "${draco_src_root}/material/material.h" + "${draco_src_root}/material/material_library.cc" + "${draco_src_root}/material/material_library.h") + + list( + APPEND draco_mesh_sources + "${draco_src_root}/mesh/mesh_connected_components.h" + "${draco_src_root}/mesh/mesh_splitter.cc" + "${draco_src_root}/mesh/mesh_splitter.h" + "${draco_src_root}/mesh/mesh_utils.cc" + "${draco_src_root}/mesh/mesh_utils.h") + + list( + APPEND draco_scene_sources + "${draco_src_root}/scene/instance_array.cc" + "${draco_src_root}/scene/instance_array.h" + "${draco_src_root}/scene/light.cc" + "${draco_src_root}/scene/light.h" + "${draco_src_root}/scene/mesh_group.h" + "${draco_src_root}/scene/scene.cc" + "${draco_src_root}/scene/scene.h" + "${draco_src_root}/scene/scene_are_equivalent.cc" + "${draco_src_root}/scene/scene_are_equivalent.h" + "${draco_src_root}/scene/scene_indices.h" + "${draco_src_root}/scene/scene_node.h" + "${draco_src_root}/scene/scene_utils.cc" + "${draco_src_root}/scene/scene_utils.h" + "${draco_src_root}/scene/trs_matrix.cc" + "${draco_src_root}/scene/trs_matrix.h") + + list( + APPEND draco_texture_sources + "${draco_src_root}/texture/source_image.cc" + "${draco_src_root}/texture/source_image.h" + "${draco_src_root}/texture/texture.h" + "${draco_src_root}/texture/texture_library.cc" + "${draco_src_root}/texture/texture_library.h" + "${draco_src_root}/texture/texture_map.cc" + "${draco_src_root}/texture/texture_map.h" + "${draco_src_root}/texture/texture_transform.cc" + "${draco_src_root}/texture/texture_transform.h" + "${draco_src_root}/texture/texture_utils.cc" + "${draco_src_root}/texture/texture_utils.h") + + +endif() + # # Draco targets. # if(EMSCRIPTEN AND DRACO_JS_GLUE) # Draco decoder and encoder "executable" targets in various flavors for - # Emsscripten. - list(APPEND draco_decoder_src - ${draco_attributes_sources} - ${draco_compression_attributes_dec_sources} - ${draco_compression_attributes_pred_schemes_dec_sources} - ${draco_compression_bit_coders_sources} - ${draco_compression_decode_sources} - ${draco_compression_entropy_sources} - ${draco_compression_mesh_traverser_sources} - ${draco_compression_mesh_dec_sources} - ${draco_compression_point_cloud_dec_sources} - ${draco_core_sources} - ${draco_dec_config_sources} - ${draco_js_dec_sources} - ${draco_mesh_sources} - ${draco_metadata_dec_sources} - ${draco_metadata_sources} - ${draco_point_cloud_sources} - ${draco_points_dec_sources}) + # Emscripten. - list(APPEND draco_encoder_src - ${draco_attributes_sources} - ${draco_compression_attributes_enc_sources} - ${draco_compression_attributes_pred_schemes_enc_sources} - ${draco_compression_bit_coders_sources} - ${draco_compression_encode_sources} - ${draco_compression_entropy_sources} - ${draco_compression_mesh_traverser_sources} - ${draco_compression_mesh_enc_sources} - ${draco_compression_point_cloud_enc_sources} - ${draco_core_sources} - ${draco_enc_config_sources} - ${draco_js_enc_sources} - ${draco_mesh_sources} - ${draco_metadata_enc_sources} - ${draco_metadata_sources} - ${draco_point_cloud_sources} - ${draco_points_enc_sources}) + if(DRACO_TRANSCODER_SUPPORTED) + message(FATAL_ERROR "The transcoder is not supported in Emscripten.") + endif() + + list( + APPEND draco_decoder_src + ${draco_attributes_sources} + ${draco_compression_attributes_dec_sources} + ${draco_compression_attributes_pred_schemes_dec_sources} + ${draco_compression_bit_coders_sources} + ${draco_compression_decode_sources} + ${draco_compression_entropy_sources} + ${draco_compression_mesh_traverser_sources} + ${draco_compression_mesh_dec_sources} + ${draco_compression_options_sources} + ${draco_compression_point_cloud_dec_sources} + ${draco_core_sources} + ${draco_dec_config_sources} + ${draco_js_dec_sources} + ${draco_mesh_sources} + ${draco_metadata_dec_sources} + ${draco_metadata_sources} + ${draco_point_cloud_sources} + ${draco_points_dec_sources}) + + list( + APPEND draco_encoder_src + ${draco_attributes_sources} + ${draco_compression_attributes_enc_sources} + ${draco_compression_attributes_pred_schemes_enc_sources} + ${draco_compression_bit_coders_sources} + ${draco_compression_encode_sources} + ${draco_compression_entropy_sources} + ${draco_compression_mesh_traverser_sources} + ${draco_compression_mesh_enc_sources} + ${draco_compression_options_sources} + ${draco_compression_point_cloud_enc_sources} + ${draco_core_sources} + ${draco_enc_config_sources} + ${draco_js_enc_sources} + ${draco_mesh_sources} + ${draco_metadata_enc_sources} + ${draco_metadata_sources} + ${draco_point_cloud_sources} + ${draco_points_enc_sources}) list(APPEND draco_js_dec_idl "${draco_src_root}/javascript/emscripten/draco_web_decoder.idl") @@ -561,10 +688,10 @@ if(EMSCRIPTEN AND DRACO_JS_GLUE) set(draco_decoder_glue_path "${draco_build}/glue_decoder") set(draco_encoder_glue_path "${draco_build}/glue_encoder") - draco_generate_emscripten_glue(INPUT_IDL ${draco_js_dec_idl} OUTPUT_PATH - ${draco_decoder_glue_path}) - draco_generate_emscripten_glue(INPUT_IDL ${draco_js_enc_idl} OUTPUT_PATH - ${draco_encoder_glue_path}) + draco_generate_emscripten_glue(INPUT_IDL ${draco_js_dec_idl} + OUTPUT_PATH ${draco_decoder_glue_path}) + draco_generate_emscripten_glue(INPUT_IDL ${draco_js_enc_idl} + OUTPUT_PATH ${draco_encoder_glue_path}) if(DRACO_DECODER_ATTRIBUTE_DEDUPLICATION) list(APPEND draco_decoder_features @@ -572,45 +699,28 @@ if(EMSCRIPTEN AND DRACO_JS_GLUE) "DRACO_ATTRIBUTE_VALUES_DEDUPLICATION_SUPPORTED") endif() - draco_add_emscripten_executable(NAME - draco_decoder - SOURCES - ${draco_decoder_src} - DEFINES - ${draco_defines} - FEATURES - ${draco_decoder_features} - INCLUDES - ${draco_include_paths} - LINK_FLAGS - "-sEXPORT_NAME=\"DracoDecoderModule\"" - GLUE_PATH - ${draco_decoder_glue_path} - PRE_LINK_JS_SOURCES - ${draco_pre_link_js_sources} - POST_LINK_JS_SOURCES - ${draco_post_link_js_decoder_sources}) + draco_add_emscripten_executable( + NAME draco_decoder + SOURCES ${draco_decoder_src} + DEFINES ${draco_defines} + FEATURES ${draco_decoder_features} + INCLUDES ${draco_include_paths} + LINK_FLAGS "-sEXPORT_NAME=\"DracoDecoderModule\"" + GLUE_PATH ${draco_decoder_glue_path} + PRE_LINK_JS_SOURCES ${draco_pre_link_js_sources} + POST_LINK_JS_SOURCES ${draco_post_link_js_decoder_sources}) draco_add_emscripten_executable( - NAME - draco_encoder - SOURCES - ${draco_encoder_src} - DEFINES - ${draco_defines} - FEATURES - DRACO_ATTRIBUTE_INDICES_DEDUPLICATION_SUPPORTED - DRACO_ATTRIBUTE_VALUES_DEDUPLICATION_SUPPORTED - INCLUDES - ${draco_include_paths} - LINK_FLAGS - "-sEXPORT_NAME=\"DracoEncoderModule\"" - GLUE_PATH - ${draco_encoder_glue_path} - PRE_LINK_JS_SOURCES - ${draco_pre_link_js_sources} - POST_LINK_JS_SOURCES - ${draco_post_link_js_sources}) + NAME draco_encoder + SOURCES ${draco_encoder_src} + DEFINES ${draco_defines} + FEATURES DRACO_ATTRIBUTE_INDICES_DEDUPLICATION_SUPPORTED + DRACO_ATTRIBUTE_VALUES_DEDUPLICATION_SUPPORTED + INCLUDES ${draco_include_paths} + LINK_FLAGS "-sEXPORT_NAME=\"DracoEncoderModule\"" + GLUE_PATH ${draco_encoder_glue_path} + PRE_LINK_JS_SOURCES ${draco_pre_link_js_sources} + POST_LINK_JS_SOURCES ${draco_post_link_js_sources}) if(DRACO_ANIMATION_ENCODING) set(draco_anim_decoder_glue_path "${draco_build}/glue_animation_decoder") @@ -622,186 +732,270 @@ if(EMSCRIPTEN AND DRACO_JS_GLUE) OUTPUT_PATH ${draco_anim_encoder_glue_path}) draco_add_emscripten_executable( - NAME - draco_animation_decoder - SOURCES - ${draco_animation_dec_sources} - ${draco_animation_js_dec_sources} - ${draco_animation_sources} - ${draco_decoder_src} - DEFINES - ${draco_defines} - INCLUDES - ${draco_include_paths} - LINK_FLAGS - "-sEXPORT_NAME=\"DracoAnimationDecoderModule\"" - GLUE_PATH - ${draco_anim_decoder_glue_path} - PRE_LINK_JS_SOURCES - ${draco_pre_link_js_sources} - POST_LINK_JS_SOURCES - ${draco_post_link_js_decoder_sources}) + NAME draco_animation_decoder + SOURCES ${draco_animation_dec_sources} ${draco_animation_js_dec_sources} + ${draco_animation_sources} ${draco_decoder_src} + DEFINES ${draco_defines} + INCLUDES ${draco_include_paths} + LINK_FLAGS "-sEXPORT_NAME=\"DracoAnimationDecoderModule\"" + GLUE_PATH ${draco_anim_decoder_glue_path} + PRE_LINK_JS_SOURCES ${draco_pre_link_js_sources} + POST_LINK_JS_SOURCES ${draco_post_link_js_decoder_sources}) draco_add_emscripten_executable( - NAME - draco_animation_encoder - SOURCES - ${draco_animation_enc_sources} - ${draco_animation_js_enc_sources} - ${draco_animation_sources} - ${draco_encoder_src} - DEFINES - ${draco_defines} - INCLUDES - ${draco_include_paths} - LINK_FLAGS - "-sEXPORT_NAME=\"DracoAnimationEncoderModule\"" - GLUE_PATH - ${draco_anim_encoder_glue_path} - PRE_LINK_JS_SOURCES - ${draco_pre_link_js_sources} - POST_LINK_JS_SOURCES - ${draco_post_link_js_sources}) + NAME draco_animation_encoder + SOURCES ${draco_animation_enc_sources} ${draco_animation_js_enc_sources} + ${draco_animation_sources} ${draco_encoder_src} + DEFINES ${draco_defines} + INCLUDES ${draco_include_paths} + LINK_FLAGS "-sEXPORT_NAME=\"DracoAnimationEncoderModule\"" + GLUE_PATH ${draco_anim_encoder_glue_path} + PRE_LINK_JS_SOURCES ${draco_pre_link_js_sources} + POST_LINK_JS_SOURCES ${draco_post_link_js_sources}) endif() else() # Standard Draco libs, encoder and decoder. Object collections that mirror the # Draco directory structure. - draco_add_library(NAME draco_attributes TYPE OBJECT SOURCES - ${draco_attributes_sources} DEFINES ${draco_defines} - INCLUDES ${draco_include_paths}) - draco_add_library(NAME - draco_compression_attributes_dec - OBJECT - ${draco_compression_attributes_dec_sources} - TYPE - OBJECT - SOURCES - ${draco_compression_attributes_dec_sources} - DEFINES - ${draco_defines} - INCLUDES - ${draco_include_paths}) - draco_add_library(NAME draco_compression_attributes_enc TYPE OBJECT SOURCES - ${draco_compression_attributes_enc_sources} DEFINES - ${draco_defines} INCLUDES ${draco_include_paths}) - draco_add_library(NAME draco_compression_attributes_pred_schemes_dec TYPE - OBJECT SOURCES - ${draco_compression_attributes_pred_schemes_dec_sources}) - draco_add_library(NAME draco_compression_attributes_pred_schemes_enc TYPE - OBJECT SOURCES - ${draco_compression_attributes_pred_schemes_enc_sources} - DEFINES ${draco_defines} INCLUDES ${draco_include_paths}) - draco_add_library(NAME draco_compression_bit_coders TYPE OBJECT SOURCES - ${draco_compression_bit_coders_sources} DEFINES - ${draco_defines} INCLUDES ${draco_include_paths}) - draco_add_library(NAME draco_enc_config TYPE OBJECT SOURCES - ${draco_enc_config_sources} DEFINES ${draco_defines} - INCLUDES ${draco_include_paths}) - draco_add_library(NAME draco_dec_config TYPE OBJECT SOURCES - ${draco_dec_config_sources} DEFINES ${draco_defines} - INCLUDES ${draco_include_paths}) - draco_add_library(NAME draco_compression_decode TYPE OBJECT SOURCES - ${draco_compression_decode_sources} DEFINES ${draco_defines} - INCLUDES ${draco_include_paths}) - draco_add_library(NAME draco_compression_encode TYPE OBJECT SOURCES - ${draco_compression_encode_sources} DEFINES ${draco_defines} - INCLUDES ${draco_include_paths}) - draco_add_library(NAME draco_compression_entropy TYPE OBJECT SOURCES - ${draco_compression_entropy_sources} DEFINES - ${draco_defines} INCLUDES ${draco_include_paths}) - draco_add_library(NAME draco_compression_mesh_traverser TYPE OBJECT SOURCES - ${draco_compression_mesh_traverser_sources} DEFINES - ${draco_defines} INCLUDES ${draco_include_paths}) - draco_add_library(NAME draco_compression_mesh_dec TYPE OBJECT SOURCES - ${draco_compression_mesh_dec_sources} DEFINES - ${draco_defines} INCLUDES ${draco_include_paths}) - draco_add_library(NAME draco_compression_mesh_enc TYPE OBJECT SOURCES - ${draco_compression_mesh_enc_sources} DEFINES - ${draco_defines} INCLUDES ${draco_include_paths}) - draco_add_library(NAME draco_compression_point_cloud_dec TYPE OBJECT SOURCES - ${draco_compression_point_cloud_dec_sources} DEFINES - ${draco_defines} INCLUDES ${draco_include_paths}) - draco_add_library(NAME draco_compression_point_cloud_enc TYPE OBJECT SOURCES - ${draco_compression_point_cloud_enc_sources} DEFINES - ${draco_defines} INCLUDES ${draco_include_paths}) - draco_add_library(NAME draco_core TYPE OBJECT SOURCES ${draco_core_sources} - DEFINES ${draco_defines} INCLUDES ${draco_include_paths}) - draco_add_library(NAME draco_io TYPE OBJECT SOURCES ${draco_io_sources} - DEFINES ${draco_defines} INCLUDES ${draco_include_paths}) - draco_add_library(NAME draco_mesh TYPE OBJECT SOURCES ${draco_mesh_sources} - DEFINES ${draco_defines} INCLUDES ${draco_include_paths}) - draco_add_library(NAME draco_metadata_dec TYPE OBJECT SOURCES - ${draco_metadata_dec_sources} DEFINES ${draco_defines} - INCLUDES ${draco_include_paths}) - draco_add_library(NAME draco_metadata_enc TYPE OBJECT SOURCES - ${draco_metadata_enc_sources} DEFINES ${draco_defines} - INCLUDES ${draco_include_paths}) - draco_add_library(NAME draco_metadata TYPE OBJECT SOURCES - ${draco_metadata_sources} DEFINES ${draco_defines} INCLUDES - ${draco_include_paths}) - draco_add_library(NAME draco_animation_dec TYPE OBJECT SOURCES - ${draco_animation_dec_sources} DEFINES ${draco_defines} - INCLUDES ${draco_include_paths}) - draco_add_library(NAME draco_animation_enc TYPE OBJECT SOURCES - ${draco_animation_enc_sources} DEFINES ${draco_defines} - INCLUDES ${draco_include_paths}) - draco_add_library(NAME draco_animation TYPE OBJECT SOURCES - ${draco_animation_sources} DEFINES ${draco_defines} INCLUDES - ${draco_include_paths}) - draco_add_library(NAME draco_point_cloud TYPE OBJECT SOURCES - ${draco_point_cloud_sources} DEFINES ${draco_defines} - INCLUDES ${draco_include_paths}) - draco_add_library(NAME - draco_points_dec - TYPE - OBJECT - SOURCES - ${draco_points_common_sources} - ${draco_points_dec_sources} - DEFINES - ${draco_defines} - INCLUDES - ${draco_include_paths}) - draco_add_library(NAME - draco_points_enc - TYPE - OBJECT - SOURCES - ${draco_points_common_sources} - ${draco_points_enc_sources} - DEFINES - ${draco_defines} - INCLUDES - ${draco_include_paths}) + draco_add_library( + NAME draco_attributes + TYPE OBJECT + SOURCES ${draco_attributes_sources} + DEFINES ${draco_defines} + INCLUDES ${draco_include_paths}) + draco_add_library( + NAME draco_compression_attributes_dec OBJECT + ${draco_compression_attributes_dec_sources} + TYPE OBJECT + SOURCES ${draco_compression_attributes_dec_sources} + DEFINES ${draco_defines} + INCLUDES ${draco_include_paths}) + draco_add_library( + NAME draco_compression_attributes_enc + TYPE OBJECT + SOURCES ${draco_compression_attributes_enc_sources} + DEFINES ${draco_defines} + INCLUDES ${draco_include_paths}) + draco_add_library( + NAME draco_compression_attributes_pred_schemes_dec + TYPE OBJECT + SOURCES ${draco_compression_attributes_pred_schemes_dec_sources}) + draco_add_library( + NAME draco_compression_attributes_pred_schemes_enc + TYPE OBJECT + SOURCES ${draco_compression_attributes_pred_schemes_enc_sources} + DEFINES ${draco_defines} + INCLUDES ${draco_include_paths}) + draco_add_library( + NAME draco_compression_bit_coders + TYPE OBJECT + SOURCES ${draco_compression_bit_coders_sources} + DEFINES ${draco_defines} + INCLUDES ${draco_include_paths}) + draco_add_library( + NAME draco_enc_config + TYPE OBJECT + SOURCES ${draco_enc_config_sources} + DEFINES ${draco_defines} + INCLUDES ${draco_include_paths}) + draco_add_library( + NAME draco_dec_config + TYPE OBJECT + SOURCES ${draco_dec_config_sources} + DEFINES ${draco_defines} + INCLUDES ${draco_include_paths}) + draco_add_library( + NAME draco_compression_decode + TYPE OBJECT + SOURCES ${draco_compression_decode_sources} + DEFINES ${draco_defines} + INCLUDES ${draco_include_paths}) + draco_add_library( + NAME draco_compression_encode + TYPE OBJECT + SOURCES ${draco_compression_encode_sources} + DEFINES ${draco_defines} + INCLUDES ${draco_include_paths}) + draco_add_library( + NAME draco_compression_entropy + TYPE OBJECT + SOURCES ${draco_compression_entropy_sources} + DEFINES ${draco_defines} + INCLUDES ${draco_include_paths}) + draco_add_library( + NAME draco_compression_mesh_traverser + TYPE OBJECT + SOURCES ${draco_compression_mesh_traverser_sources} + DEFINES ${draco_defines} + INCLUDES ${draco_include_paths}) + draco_add_library( + NAME draco_compression_mesh_dec + TYPE OBJECT + SOURCES ${draco_compression_mesh_dec_sources} + DEFINES ${draco_defines} + INCLUDES ${draco_include_paths}) + draco_add_library( + NAME draco_compression_mesh_enc + TYPE OBJECT + SOURCES ${draco_compression_mesh_enc_sources} + DEFINES ${draco_defines} + INCLUDES ${draco_include_paths}) + draco_add_library( + NAME draco_compression_options + TYPE OBJECT + SOURCES ${draco_compression_options_sources} + DEFINES ${draco_defines} + INCLUDES ${draco_include_paths}) + draco_add_library( + NAME draco_compression_point_cloud_dec + TYPE OBJECT + SOURCES ${draco_compression_point_cloud_dec_sources} + DEFINES ${draco_defines} + INCLUDES ${draco_include_paths}) + draco_add_library( + NAME draco_compression_point_cloud_enc + TYPE OBJECT + SOURCES ${draco_compression_point_cloud_enc_sources} + DEFINES ${draco_defines} + INCLUDES ${draco_include_paths}) + draco_add_library( + NAME draco_core + TYPE OBJECT + SOURCES ${draco_core_sources} + DEFINES ${draco_defines} + INCLUDES ${draco_include_paths}) + draco_add_library( + NAME draco_io + TYPE OBJECT + SOURCES ${draco_io_sources} + DEFINES ${draco_defines} + INCLUDES ${draco_include_paths}) + draco_add_library( + NAME draco_mesh + TYPE OBJECT + SOURCES ${draco_mesh_sources} + DEFINES ${draco_defines} + INCLUDES ${draco_include_paths}) + draco_add_library( + NAME draco_metadata_dec + TYPE OBJECT + SOURCES ${draco_metadata_dec_sources} + DEFINES ${draco_defines} + INCLUDES ${draco_include_paths}) + draco_add_library( + NAME draco_metadata_enc + TYPE OBJECT + SOURCES ${draco_metadata_enc_sources} + DEFINES ${draco_defines} + INCLUDES ${draco_include_paths}) + draco_add_library( + NAME draco_metadata + TYPE OBJECT + SOURCES ${draco_metadata_sources} + DEFINES ${draco_defines} + INCLUDES ${draco_include_paths}) + draco_add_library( + NAME draco_animation_dec + TYPE OBJECT + SOURCES ${draco_animation_dec_sources} + DEFINES ${draco_defines} + INCLUDES ${draco_include_paths}) + draco_add_library( + NAME draco_animation_enc + TYPE OBJECT + SOURCES ${draco_animation_enc_sources} + DEFINES ${draco_defines} + INCLUDES ${draco_include_paths}) + draco_add_library( + NAME draco_animation + TYPE OBJECT + SOURCES ${draco_animation_sources} + DEFINES ${draco_defines} + INCLUDES ${draco_include_paths}) + draco_add_library( + NAME draco_point_cloud + TYPE OBJECT + SOURCES ${draco_point_cloud_sources} + DEFINES ${draco_defines} + INCLUDES ${draco_include_paths}) + draco_add_library( + NAME draco_points_dec + TYPE OBJECT + SOURCES ${draco_points_common_sources} ${draco_points_dec_sources} + DEFINES ${draco_defines} + INCLUDES ${draco_include_paths}) + draco_add_library( + NAME draco_points_enc + TYPE OBJECT + SOURCES ${draco_points_common_sources} ${draco_points_enc_sources} + DEFINES ${draco_defines} + INCLUDES ${draco_include_paths}) - set(draco_object_library_deps - draco_attributes - draco_compression_attributes_dec - draco_compression_attributes_enc - draco_compression_attributes_pred_schemes_dec - draco_compression_attributes_pred_schemes_enc - draco_compression_bit_coders - draco_compression_decode - draco_compression_encode - draco_compression_entropy - draco_compression_mesh_dec - draco_compression_mesh_enc - draco_compression_point_cloud_dec - draco_compression_point_cloud_enc - draco_core - draco_dec_config - draco_enc_config - draco_io - draco_mesh - draco_metadata - draco_metadata_dec - draco_metadata_enc - draco_animation - draco_animation_dec - draco_animation_enc - draco_point_cloud - draco_points_dec - draco_points_enc) + if(DRACO_TRANSCODER_SUPPORTED) + if(MSVC) + # TODO(https://github.com/google/draco/issues/826) + set_source_files_properties("${draco_src_root}/io/gltf_decoder.cc" + PROPERTIES COMPILE_OPTIONS "/Od") + endif() + + draco_add_library( + NAME draco_material + TYPE OBJECT + SOURCES ${draco_material_sources} + DEFINES ${draco_defines} + INCLUDES ${draco_include_paths}) + + draco_add_library( + NAME draco_scene + TYPE OBJECT + SOURCES ${draco_scene_sources} + DEFINES ${draco_defines} + INCLUDES ${draco_include_paths}) + + draco_add_library( + NAME draco_texture + TYPE OBJECT + SOURCES ${draco_texture_sources} + DEFINES ${draco_defines} + INCLUDES ${draco_include_paths}) + + endif() + + list( + APPEND draco_object_library_deps + draco_attributes + draco_compression_attributes_dec + draco_compression_attributes_enc + draco_compression_attributes_pred_schemes_dec + draco_compression_attributes_pred_schemes_enc + draco_compression_bit_coders + draco_compression_decode + draco_compression_encode + draco_compression_entropy + draco_compression_mesh_dec + draco_compression_mesh_enc + draco_compression_options + draco_compression_point_cloud_dec + draco_compression_point_cloud_enc + draco_core + draco_dec_config + draco_enc_config + draco_io + draco_mesh + draco_metadata + draco_metadata_dec + draco_metadata_enc + draco_animation + draco_animation_dec + draco_animation_enc + draco_point_cloud + draco_points_dec + draco_points_enc) + + if(DRACO_TRANSCODER_SUPPORTED) + list(APPEND draco_object_library_deps draco_material draco_scene + draco_texture) + + endif() # Library targets that consume the object collections. if(MSVC) @@ -809,56 +1003,48 @@ else() # that the exported symbols are part of the DLL target. The unfortunate side # effect of this is that a single configuration cannot output both the # static library and the DLL: This results in an either/or situation. - # Windows users of the draco build can have a DLL and an import library, - # or they can have a static library; they cannot have both from a single + # Windows users of the draco build can have a DLL and an import library, or + # they can have a static library; they cannot have both from a single # configuration of the build. if(BUILD_SHARED_LIBS) set(draco_lib_type SHARED) else() set(draco_lib_type STATIC) endif() - draco_add_library(NAME - draco - OUTPUT_NAME - draco - TYPE - ${draco_lib_type} - DEFINES - ${draco_defines} - INCLUDES - ${draco_include_paths} - OBJLIB_DEPS - ${draco_object_library_deps}) + draco_add_library( + NAME draco + OUTPUT_NAME draco + TYPE ${draco_lib_type} + DEFINES ${draco_defines} + INCLUDES ${draco_include_paths} + OBJLIB_DEPS ${draco_object_library_deps} + LIB_DEPS ${draco_lib_deps}) + add_library(draco::draco ALIAS draco) else() - draco_add_library(NAME - draco_static - OUTPUT_NAME - draco - TYPE - STATIC - DEFINES - ${draco_defines} - INCLUDES - ${draco_include_paths} - OBJLIB_DEPS - ${draco_object_library_deps}) + draco_add_library( + NAME draco_static + OUTPUT_NAME draco + TYPE STATIC + DEFINES ${draco_defines} + INCLUDES ${draco_include_paths} + OBJLIB_DEPS ${draco_object_library_deps} + LIB_DEPS ${draco_lib_deps}) if(BUILD_SHARED_LIBS) - draco_add_library(NAME - draco_shared - SOURCES - "${draco_src_root}/core/draco_version.h" - OUTPUT_NAME - draco - TYPE - SHARED - DEFINES - ${draco_defines} - INCLUDES - ${draco_include_paths} - LIB_DEPS - draco_static) + draco_add_library( + NAME draco_shared + SOURCES "${draco_src_root}/core/draco_version.h" + OUTPUT_NAME draco + TYPE SHARED + DEFINES ${draco_defines} + INCLUDES ${draco_include_paths} + LIB_DEPS draco_static) + add_library(draco::draco ALIAS draco_shared) + set_target_properties(draco_shared PROPERTIES EXPORT_NAME draco) + else() + add_library(draco::draco ALIAS draco_static) + set_target_properties(draco_static PROPERTIES EXPORT_NAME draco) endif() endif() @@ -869,22 +1055,20 @@ else() set(unity_decoder_lib_type MODULE) endif() - draco_add_library(NAME draco_unity_plugin TYPE OBJECT SOURCES - ${draco_unity_plug_sources} DEFINES ${draco_defines} - INCLUDES ${draco_include_paths}) + draco_add_library( + NAME draco_unity_plugin + TYPE OBJECT + SOURCES ${draco_unity_plug_sources} + DEFINES ${draco_defines} + INCLUDES ${draco_include_paths}) - draco_add_library(NAME - dracodec_unity - TYPE - ${unity_decoder_lib_type} - DEFINES - ${draco_defines} - INCLUDES - ${draco_include_paths} - OBJLIB_DEPS - draco_unity_plugin - LIB_DEPS - ${draco_plugin_dependency}) + draco_add_library( + NAME dracodec_unity + TYPE ${unity_decoder_lib_type} + DEFINES ${draco_defines} + INCLUDES ${draco_include_paths} + OBJLIB_DEPS draco_unity_plugin + LIB_DEPS ${draco_plugin_dependency}) # For Mac, we need to build a .bundle for the unity plugin. if(APPLE) @@ -893,22 +1077,20 @@ else() endif() if(DRACO_MAYA_PLUGIN) - draco_add_library(NAME draco_maya_plugin TYPE OBJECT SOURCES - ${draco_maya_plug_sources} DEFINES ${draco_defines} - INCLUDES ${draco_include_paths}) + draco_add_library( + NAME draco_maya_plugin + TYPE OBJECT + SOURCES ${draco_maya_plug_sources} + DEFINES ${draco_defines} + INCLUDES ${draco_include_paths}) - draco_add_library(NAME - draco_maya_wrapper - TYPE - MODULE - DEFINES - ${draco_defines} - INCLUDES - ${draco_include_paths} - OBJLIB_DEPS - draco_maya_plugin - LIB_DEPS - ${draco_plugin_dependency}) + draco_add_library( + NAME draco_maya_wrapper + TYPE MODULE + DEFINES ${draco_defines} + INCLUDES ${draco_include_paths} + OBJLIB_DEPS draco_maya_plugin + LIB_DEPS ${draco_plugin_dependency}) # For Mac, we need to build a .bundle for the plugin. if(APPLE) @@ -917,29 +1099,44 @@ else() endif() # Draco app targets. - draco_add_executable(NAME - draco_decoder - SOURCES - "${draco_src_root}/tools/draco_decoder.cc" - ${draco_io_sources} - DEFINES - ${draco_defines} - INCLUDES - ${draco_include_paths} - LIB_DEPS - ${draco_dependency}) + draco_add_executable( + NAME draco_decoder + SOURCES "${draco_src_root}/tools/draco_decoder.cc" ${draco_io_sources} + DEFINES ${draco_defines} + INCLUDES ${draco_include_paths} + LIB_DEPS ${draco_dependency}) - draco_add_executable(NAME - draco_encoder - SOURCES - "${draco_src_root}/tools/draco_encoder.cc" - ${draco_io_sources} - DEFINES - ${draco_defines} - INCLUDES - ${draco_include_paths} - LIB_DEPS - ${draco_dependency}) + draco_add_executable( + NAME draco_encoder + SOURCES "${draco_src_root}/tools/draco_encoder.cc" ${draco_io_sources} + DEFINES ${draco_defines} + INCLUDES ${draco_include_paths} + LIB_DEPS ${draco_dependency}) + + if(DRACO_TRANSCODER_SUPPORTED) + draco_add_executable( + NAME draco_transcoder + SOURCES "${draco_src_root}/tools/draco_transcoder.cc" + "${draco_src_root}/tools/draco_transcoder_lib.cc" + "${draco_src_root}/tools/draco_transcoder_lib.h" + ${draco_io_sources} + DEFINES ${draco_defines} + INCLUDES ${draco_include_paths} + LIB_DEPS ${draco_dependency}) + + if(DRACO_SIMPLIFIER_SUPPORTED) + draco_add_executable( + NAME draco_simplifier + SOURCES ${draco_pipeline_proto_header} + "${draco_src_root}/tools/draco_simplifier.cc" + "${draco_src_root}/tools/draco_simplifier_lib.cc" + "${draco_src_root}/tools/draco_simplifier_lib.h" + ${draco_io_sources} + DEFINES ${draco_defines} + INCLUDES ${draco_include_paths} + LIB_DEPS ${draco_dependency}) + endif() + endif() draco_setup_install_target() draco_setup_test_targets() diff --git a/contrib/draco/README.md b/contrib/draco/README.md index 0d980b387..4cc717c8d 100644 --- a/contrib/draco/README.md +++ b/contrib/draco/README.md @@ -2,10 +2,93 @@

-[![Build Status](https://github.com/google/draco/workflows/Build/badge.svg)](https://github.com/google/draco/actions?query=workflow%3ABuild) +[![draco-ci](https://github.com/google/draco/workflows/draco-ci/badge.svg?branch=master)](https://github.com/google/draco/actions/workflows/ci.yml) News ======= + +Attention GStatic users: the Draco team strongly recommends using the versioned +URLs for accessing Draco GStatic content. If you are using the URLs that include +the `v1/decoders` substring within the URL, edge caching and GStatic propagation +delays can result in transient errors that can be difficult to diagnose when +new Draco releases are launched. To avoid the issue pin your sites to a +versioned release. + +### Version 1.5.6 release: +* Using the versioned www.gstatic.com WASM and Javascript decoders continues + to be recommended. To use v1.5.6, use this URL: + * https://www.gstatic.com/draco/versioned/decoders/1.5.6/* +* The CMake flag DRACO_DEBUG_MSVC_WARNINGS has been replaced with + DRACO_DEBUG_COMPILER_WARNINGS, and the behavior has changed. It is now a + boolean flag defined in draco_options.cmake. +* Bug fixes. +* Security fixes. + +### Version 1.5.5 release: +* Using the versioned www.gstatic.com WASM and Javascript decoders continues + to be recommended. To use v1.5.5, use this URL: + * https://www.gstatic.com/draco/versioned/decoders/1.5.5/* +* Bug fix: https://github.com/google/draco/issues/935 + +### Version 1.5.4 release: +* Using the versioned www.gstatic.com WASM and Javascript decoders continues + to be recommended. To use v1.5.4, use this URL: + * https://www.gstatic.com/draco/versioned/decoders/1.5.4/* +* Added partial support for glTF extensions EXT_mesh_features and + EXT_structural_metadata. +* Bug fixes. +* Security fixes. + +### Version 1.5.3 release: +* Using the versioned www.gstatic.com WASM and Javascript decoders continues + to be recommended. To use v1.5.3, use this URL: + * https://www.gstatic.com/draco/versioned/decoders/1.5.3/* +* Bug fixes. + +### Version 1.5.2 release +* This is the same as v1.5.1 with the following two bug fixes: + * Fixes DRACO_TRANSCODER_SUPPORTED enabled builds. + * ABI version updated. + +### Version 1.5.1 release +* Adds assertion enabled Emscripten builds to the release, and a subset of the + assertion enabled builds to GStatic. See the file listing below. +* Custom paths to third party dependencies are now supported. See BUILDING.md + for more information. +* The CMake configuration file draco-config.cmake is now tested and known to + work for using Draco in Linux, MacOS, and Windows CMake projects. See the + `install_test` subdirectory of `src/draco/tools` for more information. +* Bug fixes. + +### Version 1.5.0 release +* Adds the draco_transcoder tool. See the section below on the glTF transcoding + tool, and BUILDING.md for build and dependency information. +* Some changes to configuration variables have been made for this release: + - The DRACO_GLTF flag has been renamed to DRACO_GLTF_BITSTREAM to help + increase understanding of its purpose, which is to limit Draco features to + those included in the Draco glTF specification. + - Variables exported in CMake via draco-config.cmake and find-draco.cmake + (formerly FindDraco.cmake) have been renamed. It's unlikely that this + impacts any existing projects as the aforementioned files were not formed + correctly. See [PR775](https://github.com/google/draco/pull/775) for full + details of the changes. +* A CMake version file has been added. +* The CMake install target now uses absolute paths direct from CMake instead + of building them using CMAKE_INSTALL_PREFIX. This was done to make Draco + easier to use for downstream packagers and should have little to no impact on + users picking up Draco from source. +* Certain MSVC warnings have had their levels changed via compiler flag to + reduce the amount of noise output by the MSVC compilers. Set MSVC warning + level to 4, or define DRACO_DEBUG_MSVC_WARNINGS at CMake configuration time + to restore previous behavior. +* Bug fixes. + +### Version 1.4.3 release +* Using the versioned www.gstatic.com WASM and Javascript decoders continues + to be recommended. To use v1.4.3, use this URL: + * https://www.gstatic.com/draco/versioned/decoders/1.4.3/* +* Bug fixes + ### Version 1.4.1 release * Using the versioned www.gstatic.com WASM and Javascript decoders is now recommended. To use v1.4.1, use this URL: @@ -129,6 +212,7 @@ _**Contents**_ * [Encoding Tool](#encoding-tool) * [Encoding Point Clouds](#encoding-point-clouds) * [Decoding Tool](#decoding-tool) + * [glTF Transcoding Tool](#gltf-transcoding-tool) * [C++ Decoder API](#c-decoder-api) * [Javascript Encoder API](#javascript-encoder-api) * [Javascript Decoder API](#javascript-decoder-api) @@ -136,6 +220,7 @@ _**Contents**_ * [Metadata API](#metadata-api) * [NPM Package](#npm-package) * [three.js Renderer Example](#threejs-renderer-example) + * [GStatic Javascript Builds](#gstatic-javascript-builds) * [Support](#support) * [License](#license) * [References](#references) @@ -170,16 +255,18 @@ Command Line Applications ------------------------ The default target created from the build files will be the `draco_encoder` -and `draco_decoder` command line applications. For both applications, if you -run them without any arguments or `-h`, the applications will output usage and -options. +and `draco_decoder` command line applications. Additionally, `draco_transcoder` +is generated when CMake is run with the DRACO_TRANSCODER_SUPPORTED variable set +to ON (see [BUILDING](BUILDING.md#transcoder) for more details). For all +applications, if you run them without any arguments or `-h`, the applications +will output usage and options. Encoding Tool ------------- -`draco_encoder` will read OBJ or PLY files as input, and output Draco-encoded -files. We have included Stanford's [Bunny] mesh for testing. The basic command -line looks like this: +`draco_encoder` will read OBJ, STL or PLY files as input, and output +Draco-encoded files. We have included Stanford's [Bunny] mesh for testing. The +basic command line looks like this: ~~~~~ bash ./draco_encoder -i testdata/bun_zipper.ply -o out.drc @@ -232,15 +319,34 @@ and denser point clouds. Decoding Tool ------------- -`draco_decoder` will read Draco files as input, and output OBJ or PLY files. -The basic command line looks like this: +`draco_decoder` will read Draco files as input, and output OBJ, STL or PLY +files. The basic command line looks like this: ~~~~~ bash ./draco_decoder -i in.drc -o out.obj ~~~~~ +glTF Transcoding Tool +--------------------- + +`draco_transcoder` can be used to add Draco compression to glTF assets. The +basic command line looks like this: + +~~~~~ bash +./draco_transcoder -i in.glb -o out.glb +~~~~~ + +This command line will add geometry compression to all meshes in the `in.glb` +file. Quantization values for different glTF attributes can be specified +similarly to the `draco_encoder` tool. For example `-qp` can be used to define +quantization of the position attribute: + +~~~~~ bash +./draco_transcoder -i in.glb -o out.glb -qp 12 +~~~~~ + C++ Decoder API -------------- +--------------- If you'd like to add decoding to your applications you will need to include the `draco_dec` library. In order to use the Draco decoder you need to @@ -442,6 +548,30 @@ Javascript decoder using the `three.js` renderer. Please see the [javascript/example/README.md](javascript/example/README.md) file for more information. +GStatic Javascript Builds +========================= + +Prebuilt versions of the Emscripten-built Draco javascript decoders are hosted +on www.gstatic.com in version labeled directories: + +https://www.gstatic.com/draco/versioned/decoders/VERSION/* + +As of the v1.4.3 release the files available are: + +- [draco_decoder.js](https://www.gstatic.com/draco/versioned/decoders/1.4.3/draco_decoder.js) +- [draco_decoder.wasm](https://www.gstatic.com/draco/versioned/decoders/1.4.3/draco_decoder.wasm) +- [draco_decoder_gltf.js](https://www.gstatic.com/draco/versioned/decoders/1.4.3/draco_decoder_gltf.js) +- [draco_decoder_gltf.wasm](https://www.gstatic.com/draco/versioned/decoders/1.4.3/draco_decoder_gltf.wasm) +- [draco_wasm_wrapper.js](https://www.gstatic.com/draco/versioned/decoders/1.4.3/draco_wasm_wrapper.js) +- [draco_wasm_wrapper_gltf.js](https://www.gstatic.com/draco/versioned/decoders/1.4.3/draco_wasm_wrapper_gltf.js) + +Beginning with the v1.5.1 release assertion enabled builds of the following +files are available: + +- [draco_decoder.js](https://www.gstatic.com/draco/versioned/decoders/1.5.1/with_asserts/draco_decoder.js) +- [draco_decoder.wasm](https://www.gstatic.com/draco/versioned/decoders/1.5.1/with_asserts/draco_decoder.wasm) +- [draco_wasm_wrapper.js](https://www.gstatic.com/draco/versioned/decoders/1.5.1/with_asserts/draco_wasm_wrapper.js) + Support ======= diff --git a/contrib/draco/cmake/DracoConfig.cmake b/contrib/draco/cmake/DracoConfig.cmake deleted file mode 100644 index be5e1faef..000000000 --- a/contrib/draco/cmake/DracoConfig.cmake +++ /dev/null @@ -1,3 +0,0 @@ -@PACKAGE_INIT@ -set_and_check(draco_INCLUDE_DIR "@PACKAGE_draco_include_install_dir@") -set_and_check(draco_LIBRARY_DIR "@PACKAGE_draco_lib_install_dir@") diff --git a/contrib/draco/cmake/FindDraco.cmake b/contrib/draco/cmake/FindDraco.cmake deleted file mode 100644 index 0a9193065..000000000 --- a/contrib/draco/cmake/FindDraco.cmake +++ /dev/null @@ -1,56 +0,0 @@ -# Finddraco -# -# Locates draco and sets the following variables: -# -# draco_FOUND draco_INCLUDE_DIRS draco_LIBARY_DIRS draco_LIBRARIES -# draco_VERSION_STRING -# -# draco_FOUND is set to YES only when all other variables are successfully -# configured. - -unset(draco_FOUND) -unset(draco_INCLUDE_DIRS) -unset(draco_LIBRARY_DIRS) -unset(draco_LIBRARIES) -unset(draco_VERSION_STRING) - -mark_as_advanced(draco_FOUND) -mark_as_advanced(draco_INCLUDE_DIRS) -mark_as_advanced(draco_LIBRARY_DIRS) -mark_as_advanced(draco_LIBRARIES) -mark_as_advanced(draco_VERSION_STRING) - -set(draco_version_file_no_prefix "draco/src/draco/core/draco_version.h") - -# Set draco_INCLUDE_DIRS -find_path(draco_INCLUDE_DIRS NAMES "${draco_version_file_no_prefix}") - -# Extract the version string from draco_version.h. -if(draco_INCLUDE_DIRS) - set(draco_version_file - "${draco_INCLUDE_DIRS}/draco/src/draco/core/draco_version.h") - file(STRINGS "${draco_version_file}" draco_version REGEX "kdracoVersion") - list(GET draco_version 0 draco_version) - string(REPLACE "static const char kdracoVersion[] = " "" draco_version - "${draco_version}") - string(REPLACE ";" "" draco_version "${draco_version}") - string(REPLACE "\"" "" draco_version "${draco_version}") - set(draco_VERSION_STRING ${draco_version}) -endif() - -# Find the library. -if(BUILD_SHARED_LIBS) - find_library(draco_LIBRARIES NAMES draco.dll libdraco.dylib libdraco.so) -else() - find_library(draco_LIBRARIES NAMES draco.lib libdraco.a) -endif() - -# Store path to library. -get_filename_component(draco_LIBRARY_DIRS ${draco_LIBRARIES} DIRECTORY) - -if(draco_INCLUDE_DIRS - AND draco_LIBRARY_DIRS - AND draco_LIBRARIES - AND draco_VERSION_STRING) - set(draco_FOUND YES) -endif() diff --git a/contrib/draco/cmake/compiler_flags.cmake b/contrib/draco/cmake/compiler_flags.cmake deleted file mode 100644 index 8750e6f7d..000000000 --- a/contrib/draco/cmake/compiler_flags.cmake +++ /dev/null @@ -1,220 +0,0 @@ -if(DRACO_CMAKE_COMPILER_FLAGS_CMAKE_) - return() -endif() -set(DRACO_CMAKE_COMPILER_FLAGS_CMAKE_ 1) - -include(CheckCCompilerFlag) -include(CheckCXXCompilerFlag) -include("${draco_root}/cmake/compiler_tests.cmake") - -# Strings used to cache failed C/CXX flags. -set(DRACO_FAILED_C_FLAGS) -set(DRACO_FAILED_CXX_FLAGS) - -# Checks C compiler for support of $c_flag. Adds $c_flag to $CMAKE_C_FLAGS when -# the compile test passes. Caches $c_flag in $DRACO_FAILED_C_FLAGS when the test -# fails. -macro(add_c_flag_if_supported c_flag) - unset(C_FLAG_FOUND CACHE) - string(FIND "${CMAKE_C_FLAGS}" "${c_flag}" C_FLAG_FOUND) - unset(C_FLAG_FAILED CACHE) - string(FIND "${DRACO_FAILED_C_FLAGS}" "${c_flag}" C_FLAG_FAILED) - - if(${C_FLAG_FOUND} EQUAL -1 AND ${C_FLAG_FAILED} EQUAL -1) - unset(C_FLAG_SUPPORTED CACHE) - message("Checking C compiler flag support for: " ${c_flag}) - check_c_compiler_flag("${c_flag}" C_FLAG_SUPPORTED) - if(${C_FLAG_SUPPORTED}) - set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${c_flag}" CACHE STRING "") - else() - set(DRACO_FAILED_C_FLAGS - "${DRACO_FAILED_C_FLAGS} ${c_flag}" - CACHE STRING "" FORCE) - endif() - endif() -endmacro() - -# Checks C++ compiler for support of $cxx_flag. Adds $cxx_flag to -# $CMAKE_CXX_FLAGS when the compile test passes. Caches $c_flag in -# $DRACO_FAILED_CXX_FLAGS when the test fails. -macro(add_cxx_flag_if_supported cxx_flag) - unset(CXX_FLAG_FOUND CACHE) - string(FIND "${CMAKE_CXX_FLAGS}" "${cxx_flag}" CXX_FLAG_FOUND) - unset(CXX_FLAG_FAILED CACHE) - string(FIND "${DRACO_FAILED_CXX_FLAGS}" "${cxx_flag}" CXX_FLAG_FAILED) - - if(${CXX_FLAG_FOUND} EQUAL -1 AND ${CXX_FLAG_FAILED} EQUAL -1) - unset(CXX_FLAG_SUPPORTED CACHE) - message("Checking CXX compiler flag support for: " ${cxx_flag}) - check_cxx_compiler_flag("${cxx_flag}" CXX_FLAG_SUPPORTED) - if(${CXX_FLAG_SUPPORTED}) - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${cxx_flag}" CACHE STRING "") - else() - set(DRACO_FAILED_CXX_FLAGS - "${DRACO_FAILED_CXX_FLAGS} ${cxx_flag}" - CACHE STRING "" FORCE) - endif() - endif() -endmacro() - -# Convenience method for adding a flag to both the C and C++ compiler command -# lines. -macro(add_compiler_flag_if_supported flag) - add_c_flag_if_supported(${flag}) - add_cxx_flag_if_supported(${flag}) -endmacro() - -# Checks C compiler for support of $c_flag and terminates generation when -# support is not present. -macro(require_c_flag c_flag update_c_flags) - unset(C_FLAG_FOUND CACHE) - string(FIND "${CMAKE_C_FLAGS}" "${c_flag}" C_FLAG_FOUND) - - if(${C_FLAG_FOUND} EQUAL -1) - unset(HAVE_C_FLAG CACHE) - message("Checking C compiler flag support for: " ${c_flag}) - check_c_compiler_flag("${c_flag}" HAVE_C_FLAG) - if(NOT ${HAVE_C_FLAG}) - message( - FATAL_ERROR "${PROJECT_NAME} requires support for C flag: ${c_flag}.") - endif() - if(${update_c_flags}) - set(CMAKE_C_FLAGS "${c_flag} ${CMAKE_C_FLAGS}" CACHE STRING "" FORCE) - endif() - endif() -endmacro() - -# Checks CXX compiler for support of $cxx_flag and terminates generation when -# support is not present. -macro(require_cxx_flag cxx_flag update_cxx_flags) - unset(CXX_FLAG_FOUND CACHE) - string(FIND "${CMAKE_CXX_FLAGS}" "${cxx_flag}" CXX_FLAG_FOUND) - - if(${CXX_FLAG_FOUND} EQUAL -1) - unset(HAVE_CXX_FLAG CACHE) - message("Checking CXX compiler flag support for: " ${cxx_flag}) - check_cxx_compiler_flag("${cxx_flag}" HAVE_CXX_FLAG) - if(NOT ${HAVE_CXX_FLAG}) - message( - FATAL_ERROR - "${PROJECT_NAME} requires support for CXX flag: ${cxx_flag}.") - endif() - if(${update_cxx_flags}) - set(CMAKE_CXX_FLAGS - "${cxx_flag} ${CMAKE_CXX_FLAGS}" - CACHE STRING "" FORCE) - endif() - endif() -endmacro() - -# Checks for support of $flag by both the C and CXX compilers. Terminates -# generation when support is not present in both compilers. -macro(require_compiler_flag flag update_cmake_flags) - require_c_flag(${flag} ${update_cmake_flags}) - require_cxx_flag(${flag} ${update_cmake_flags}) -endmacro() - -# Checks only non-MSVC targets for support of $c_flag and terminates generation -# when support is not present. -macro(require_c_flag_nomsvc c_flag update_c_flags) - if(NOT MSVC) - require_c_flag(${c_flag} ${update_c_flags}) - endif() -endmacro() - -# Checks only non-MSVC targets for support of $cxx_flag and terminates -# generation when support is not present. -macro(require_cxx_flag_nomsvc cxx_flag update_cxx_flags) - if(NOT MSVC) - require_cxx_flag(${cxx_flag} ${update_cxx_flags}) - endif() -endmacro() - -# Checks only non-MSVC targets for support of $flag by both the C and CXX -# compilers. Terminates generation when support is not present in both -# compilers. -macro(require_compiler_flag_nomsvc flag update_cmake_flags) - require_c_flag_nomsvc(${flag} ${update_cmake_flags}) - require_cxx_flag_nomsvc(${flag} ${update_cmake_flags}) -endmacro() - -# Adds $flag to assembler command line. -macro(append_as_flag flag) - unset(AS_FLAG_FOUND CACHE) - string(FIND "${DRACO_AS_FLAGS}" "${flag}" AS_FLAG_FOUND) - - if(${AS_FLAG_FOUND} EQUAL -1) - set(DRACO_AS_FLAGS "${DRACO_AS_FLAGS} ${flag}") - endif() -endmacro() - -# Adds $flag to the C compiler command line. -macro(append_c_flag flag) - unset(C_FLAG_FOUND CACHE) - string(FIND "${CMAKE_C_FLAGS}" "${flag}" C_FLAG_FOUND) - - if(${C_FLAG_FOUND} EQUAL -1) - set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${flag}") - endif() -endmacro() - -# Adds $flag to the CXX compiler command line. -macro(append_cxx_flag flag) - unset(CXX_FLAG_FOUND CACHE) - string(FIND "${CMAKE_CXX_FLAGS}" "${flag}" CXX_FLAG_FOUND) - - if(${CXX_FLAG_FOUND} EQUAL -1) - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${flag}") - endif() -endmacro() - -# Adds $flag to the C and CXX compiler command lines. -macro(append_compiler_flag flag) - append_c_flag(${flag}) - append_cxx_flag(${flag}) -endmacro() - -# Adds $flag to the executable linker command line. -macro(append_exe_linker_flag flag) - unset(LINKER_FLAG_FOUND CACHE) - string(FIND "${CMAKE_EXE_LINKER_FLAGS}" "${flag}" LINKER_FLAG_FOUND) - - if(${LINKER_FLAG_FOUND} EQUAL -1) - set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} ${flag}") - endif() -endmacro() - -# Adds $flag to the link flags for $target. -function(append_link_flag_to_target target flags) - unset(target_link_flags) - get_target_property(target_link_flags ${target} LINK_FLAGS) - - if(target_link_flags) - unset(link_flag_found) - string(FIND "${target_link_flags}" "${flags}" link_flag_found) - - if(NOT ${link_flag_found} EQUAL -1) - return() - endif() - - set(target_link_flags "${target_link_flags} ${flags}") - else() - set(target_link_flags "${flags}") - endif() - - set_target_properties(${target} PROPERTIES LINK_FLAGS ${target_link_flags}) -endfunction() - -# Adds $flag to executable linker flags, and makes sure C/CXX builds still work. -macro(require_linker_flag flag) - append_exe_linker_flag(${flag}) - - unset(c_passed) - draco_check_c_compiles("LINKER_FLAG_C_TEST(${flag})" "" c_passed) - unset(cxx_passed) - draco_check_cxx_compiles("LINKER_FLAG_CXX_TEST(${flag})" "" cxx_passed) - - if(NOT c_passed OR NOT cxx_passed) - message(FATAL_ERROR "Linker flag test for ${flag} failed.") - endif() -endmacro() diff --git a/contrib/draco/cmake/compiler_tests.cmake b/contrib/draco/cmake/compiler_tests.cmake deleted file mode 100644 index e781a6537..000000000 --- a/contrib/draco/cmake/compiler_tests.cmake +++ /dev/null @@ -1,103 +0,0 @@ -if(DRACO_CMAKE_COMPILER_TESTS_CMAKE_) - return() -endif() -set(DRACO_CMAKE_COMPILER_TESTS_CMAKE_ 1) - -include(CheckCSourceCompiles) -include(CheckCXXSourceCompiles) - -# The basic main() macro used in all compile tests. -set(DRACO_C_MAIN "\nint main(void) { return 0; }") -set(DRACO_CXX_MAIN "\nint main() { return 0; }") - -# Strings containing the names of passed and failed tests. -set(DRACO_C_PASSED_TESTS) -set(DRACO_C_FAILED_TESTS) -set(DRACO_CXX_PASSED_TESTS) -set(DRACO_CXX_FAILED_TESTS) - -macro(draco_push_var var new_value) - set(SAVED_${var} ${var}) - set(${var} ${new_value}) -endmacro() - -macro(draco_pop_var var) - set(var ${SAVED_${var}}) - unset(SAVED_${var}) -endmacro() - -# Confirms $test_source compiles and stores $test_name in one of -# $DRACO_C_PASSED_TESTS or $DRACO_C_FAILED_TESTS depending on out come. When the -# test passes $result_var is set to 1. When it fails $result_var is unset. The -# test is not run if the test name is found in either of the passed or failed -# test variables. -macro(draco_check_c_compiles test_name test_source result_var) - unset(C_TEST_PASSED CACHE) - unset(C_TEST_FAILED CACHE) - string(FIND "${DRACO_C_PASSED_TESTS}" "${test_name}" C_TEST_PASSED) - string(FIND "${DRACO_C_FAILED_TESTS}" "${test_name}" C_TEST_FAILED) - if(${C_TEST_PASSED} EQUAL -1 AND ${C_TEST_FAILED} EQUAL -1) - unset(C_TEST_COMPILED CACHE) - message("Running C compiler test: ${test_name}") - check_c_source_compiles("${test_source} ${DRACO_C_MAIN}" C_TEST_COMPILED) - set(${result_var} ${C_TEST_COMPILED}) - - if(${C_TEST_COMPILED}) - set(DRACO_C_PASSED_TESTS "${DRACO_C_PASSED_TESTS} ${test_name}") - else() - set(DRACO_C_FAILED_TESTS "${DRACO_C_FAILED_TESTS} ${test_name}") - message("C Compiler test ${test_name} failed.") - endif() - elseif(NOT ${C_TEST_PASSED} EQUAL -1) - set(${result_var} 1) - else() # ${C_TEST_FAILED} NOT EQUAL -1 - unset(${result_var}) - endif() -endmacro() - -# Confirms $test_source compiles and stores $test_name in one of -# $DRACO_CXX_PASSED_TESTS or $DRACO_CXX_FAILED_TESTS depending on out come. When -# the test passes $result_var is set to 1. When it fails $result_var is unset. -# The test is not run if the test name is found in either of the passed or -# failed test variables. -macro(draco_check_cxx_compiles test_name test_source result_var) - unset(CXX_TEST_PASSED CACHE) - unset(CXX_TEST_FAILED CACHE) - string(FIND "${DRACO_CXX_PASSED_TESTS}" "${test_name}" CXX_TEST_PASSED) - string(FIND "${DRACO_CXX_FAILED_TESTS}" "${test_name}" CXX_TEST_FAILED) - if(${CXX_TEST_PASSED} EQUAL -1 AND ${CXX_TEST_FAILED} EQUAL -1) - unset(CXX_TEST_COMPILED CACHE) - message("Running CXX compiler test: ${test_name}") - check_cxx_source_compiles("${test_source} ${DRACO_CXX_MAIN}" - CXX_TEST_COMPILED) - set(${result_var} ${CXX_TEST_COMPILED}) - - if(${CXX_TEST_COMPILED}) - set(DRACO_CXX_PASSED_TESTS "${DRACO_CXX_PASSED_TESTS} ${test_name}") - else() - set(DRACO_CXX_FAILED_TESTS "${DRACO_CXX_FAILED_TESTS} ${test_name}") - message("CXX Compiler test ${test_name} failed.") - endif() - elseif(NOT ${CXX_TEST_PASSED} EQUAL -1) - set(${result_var} 1) - else() # ${CXX_TEST_FAILED} NOT EQUAL -1 - unset(${result_var}) - endif() -endmacro() - -# Convenience macro that confirms $test_source compiles as C and C++. -# $result_var is set to 1 when both tests are successful, and 0 when one or both -# tests fail. Note: This macro is intended to be used to write to result -# variables that are expanded via configure_file(). $result_var is set to 1 or 0 -# to allow direct usage of the value in generated source files. -macro(draco_check_source_compiles test_name test_source result_var) - unset(C_PASSED) - unset(CXX_PASSED) - draco_check_c_compiles(${test_name} ${test_source} C_PASSED) - draco_check_cxx_compiles(${test_name} ${test_source} CXX_PASSED) - if(${C_PASSED} AND ${CXX_PASSED}) - set(${result_var} 1) - else() - set(${result_var} 0) - endif() -endmacro() diff --git a/contrib/draco/cmake/draco-config.cmake.template b/contrib/draco/cmake/draco-config.cmake.template index ca4a456bf..ed86823ea 100644 --- a/contrib/draco/cmake/draco-config.cmake.template +++ b/contrib/draco/cmake/draco-config.cmake.template @@ -1,2 +1,3 @@ -set(DRACO_INCLUDE_DIRS "@DRACO_INCLUDE_DIRS@") -set(DRACO_LIBRARIES "draco") +@PACKAGE_INIT@ + +include("${CMAKE_CURRENT_LIST_DIR}/draco-targets.cmake") diff --git a/contrib/draco/cmake/draco.pc.template b/contrib/draco/cmake/draco.pc.template index b8ae48212..050219ccb 100644 --- a/contrib/draco/cmake/draco.pc.template +++ b/contrib/draco/cmake/draco.pc.template @@ -1,11 +1,6 @@ -prefix=@prefix@ -exec_prefix=@exec_prefix@ -libdir=@libdir@ -includedir=@includedir@ - Name: @PROJECT_NAME@ Description: Draco geometry de(com)pression library. Version: @DRACO_VERSION@ -Cflags: -I${includedir} -Libs: -L${libdir} -ldraco +Cflags: -I@includes_path@ +Libs: -L@libs_path@ -ldraco Libs.private: @CMAKE_THREAD_LIBS_INIT@ diff --git a/contrib/draco/cmake/draco_build_definitions.cmake b/contrib/draco/cmake/draco_build_definitions.cmake index f7354c15f..4dc232333 100644 --- a/contrib/draco/cmake/draco_build_definitions.cmake +++ b/contrib/draco/cmake/draco_build_definitions.cmake @@ -1,3 +1,17 @@ +# Copyright 2021 The Draco Authors +# +# 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. + if(DRACO_CMAKE_DRACO_BUILD_DEFINITIONS_CMAKE_) return() endif() # DRACO_CMAKE_DRACO_BUILD_DEFINITIONS_CMAKE_ @@ -17,10 +31,6 @@ macro(set_draco_target) endif() set(draco_plugin_dependency draco_static) endif() - - if(BUILD_SHARED_LIBS) - set(CMAKE_POSITION_INDEPENDENT_CODE ON) - endif() endmacro() # Configures flags and sets build system globals. @@ -36,23 +46,37 @@ macro(draco_set_build_definitions) endif() draco_load_version_info() - set(DRACO_SOVERSION 1) + + # Library version info. See the libtool docs for updating the values: + # https://www.gnu.org/software/libtool/manual/libtool.html#Updating-version-info + # + # c=, r=, a= + # + # libtool generates a .so file as .so.[c-a].a.r, while -version-info c:r:a is + # passed to libtool. + # + # We set DRACO_SOVERSION = [c-a].a.r + set(LT_CURRENT 8) + set(LT_REVISION 0) + set(LT_AGE 0) + math(EXPR DRACO_SOVERSION_MAJOR "${LT_CURRENT} - ${LT_AGE}") + set(DRACO_SOVERSION "${DRACO_SOVERSION_MAJOR}.${LT_AGE}.${LT_REVISION}") + unset(LT_CURRENT) + unset(LT_REVISION) + unset(LT_AGE) list(APPEND draco_include_paths "${draco_root}" "${draco_root}/src" "${draco_build}") - if(DRACO_ABSL) - list(APPEND draco_include_path "${draco_root}/third_party/abseil-cpp") + if(DRACO_TRANSCODER_SUPPORTED) + draco_setup_eigen() + draco_setup_filesystem() + draco_setup_tinygltf() + + endif() - list(APPEND draco_gtest_include_paths - "${draco_root}/../googletest/googlemock/include" - "${draco_root}/../googletest/googlemock" - "${draco_root}/../googletest/googletest/include" - "${draco_root}/../googletest/googletest") - list(APPEND draco_test_include_paths ${draco_include_paths} - ${draco_gtest_include_paths}) list(APPEND draco_defines "DRACO_CMAKE=1" "DRACO_FLAGS_SRCDIR=\"${draco_root}\"" "DRACO_FLAGS_TMPDIR=\"/tmp\"") @@ -63,11 +87,22 @@ macro(draco_set_build_definitions) if(BUILD_SHARED_LIBS) set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS TRUE) endif() - else() + endif() + + if(NOT MSVC) if(${CMAKE_SIZEOF_VOID_P} EQUAL 8) # Ensure 64-bit platforms can support large files. list(APPEND draco_defines "_LARGEFILE_SOURCE" "_FILE_OFFSET_BITS=64") endif() + + if(NOT DRACO_DEBUG_COMPILER_WARNINGS) + if(CMAKE_CXX_COMPILER_ID STREQUAL "Clang") + list(APPEND draco_clang_cxx_flags + "-Wno-implicit-const-int-float-conversion") + else() + list(APPEND draco_base_cxx_flags "-Wno-deprecated-declarations") + endif() + endif() endif() if(ANDROID) @@ -102,13 +137,9 @@ macro(draco_set_build_definitions) set(draco_neon_source_file_suffix "neon.cc") set(draco_sse4_source_file_suffix "sse4.cc") - if((${CMAKE_CXX_COMPILER_ID} - STREQUAL - "GNU" - AND ${CMAKE_CXX_COMPILER_VERSION} VERSION_LESS 5) - OR (${CMAKE_CXX_COMPILER_ID} - STREQUAL - "Clang" + if((${CMAKE_CXX_COMPILER_ID} STREQUAL "GNU" AND ${CMAKE_CXX_COMPILER_VERSION} + VERSION_LESS 5) + OR (${CMAKE_CXX_COMPILER_ID} STREQUAL "Clang" AND ${CMAKE_CXX_COMPILER_VERSION} VERSION_LESS 4)) message( WARNING "GNU/GCC < v5 or Clang/LLVM < v4, ENABLING COMPATIBILITY MODE.") @@ -117,7 +148,9 @@ macro(draco_set_build_definitions) if(EMSCRIPTEN) draco_check_emscripten_environment() - draco_get_required_emscripten_flags(FLAG_LIST_VAR draco_base_cxx_flags) + draco_get_required_emscripten_flags( + FLAG_LIST_VAR_COMPILER draco_base_cxx_flags + FLAG_LIST_VAR_LINKER draco_base_exe_linker_flags) endif() draco_configure_sanitizer() diff --git a/contrib/draco/cmake/draco_cpu_detection.cmake b/contrib/draco/cmake/draco_cpu_detection.cmake index 96e4a289b..c3b77b80c 100644 --- a/contrib/draco/cmake/draco_cpu_detection.cmake +++ b/contrib/draco/cmake/draco_cpu_detection.cmake @@ -1,3 +1,17 @@ +# Copyright 2021 The Draco Authors +# +# 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. + if(DRACO_CMAKE_DRACO_CPU_DETECTION_CMAKE_) return() endif() # DRACO_CMAKE_DRACO_CPU_DETECTION_CMAKE_ diff --git a/contrib/draco/cmake/draco_dependencies.cmake b/contrib/draco/cmake/draco_dependencies.cmake new file mode 100644 index 000000000..91ee0839b --- /dev/null +++ b/contrib/draco/cmake/draco_dependencies.cmake @@ -0,0 +1,136 @@ +# Copyright 2022 The Draco Authors +# +# 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. + +if(DRACO_CMAKE_DRACO_DEPENDENCIES_CMAKE) + return() +endif() +set(DRACO_CMAKE_DRACO_DEPENDENCIES_CMAKE 1) + +include("${draco_root}/cmake/draco_variables.cmake") + +# Each variable holds a user specified custom path to a local copy of the +# sources that belong to each project that Draco depends on. When paths are +# empty the build will be generated pointing to the Draco git submodules. +# Otherwise the paths specified by the user will be used in the build +# configuration. + +# Path to the Eigen. The path must contain the Eigen directory. +set(DRACO_EIGEN_PATH) +draco_track_configuration_variable(DRACO_EIGEN_PATH) + +# Path to the gulrak/filesystem installation. The path specified must contain +# the ghc subdirectory that houses the filesystem includes. +set(DRACO_FILESYSTEM_PATH) +draco_track_configuration_variable(DRACO_FILESYSTEM_PATH) + +# Path to the googletest installation. The path must be to the root of the +# Googletest project directory. +set(DRACO_GOOGLETEST_PATH) +draco_track_configuration_variable(DRACO_GOOGLETEST_PATH) + +# Path to the syoyo/tinygltf installation. The path must be to the root of the +# project directory. +set(DRACO_TINYGLTF_PATH) +draco_track_configuration_variable(DRACO_TINYGLTF_PATH) + +# Utility macro for killing the build due to a missing submodule directory. +macro(draco_die_missing_submodule dir) + message(FATAL_ERROR "${dir} missing, run git submodule update --init") +endmacro() + +# Determines the Eigen location and updates the build configuration accordingly. +macro(draco_setup_eigen) + if(DRACO_EIGEN_PATH) + set(eigen_path "${DRACO_EIGEN_PATH}") + + if(NOT IS_DIRECTORY "${eigen_path}") + message(FATAL_ERROR "DRACO_EIGEN_PATH does not exist.") + endif() + else() + set(eigen_path "${draco_root}/third_party/eigen") + + if(NOT IS_DIRECTORY "${eigen_path}") + draco_die_missing_submodule("${eigen_path}") + endif() + endif() + + set(eigen_include_path "${eigen_path}/Eigen") + + if(NOT EXISTS "${eigen_path}/Eigen") + message(FATAL_ERROR "The eigen path does not contain an Eigen directory.") + endif() + + list(APPEND draco_include_paths "${eigen_path}") +endmacro() + +# Determines the gulrak/filesystem location and updates the build configuration +# accordingly. +macro(draco_setup_filesystem) + if(DRACO_FILESYSTEM_PATH) + set(fs_path "${DRACO_FILESYSTEM_PATH}") + + if(NOT IS_DIRECTORY "${fs_path}") + message(FATAL_ERROR "DRACO_FILESYSTEM_PATH does not exist.") + endif() + else() + set(fs_path "${draco_root}/third_party/filesystem/include") + + if(NOT IS_DIRECTORY "${fs_path}") + draco_die_missing_submodule("${fs_path}") + endif() + endif() + + list(APPEND draco_include_paths "${fs_path}") +endmacro() + +# Determines the Googletest location and sets up include and source list vars +# for the draco_tests build. +macro(draco_setup_googletest) + if(DRACO_GOOGLETEST_PATH) + set(gtest_path "${DRACO_GOOGLETEST_PATH}") + if(NOT IS_DIRECTORY "${gtest_path}") + message(FATAL_ERROR "DRACO_GOOGLETEST_PATH does not exist.") + endif() + else() + set(gtest_path "${draco_root}/third_party/googletest") + endif() + + list(APPEND draco_test_include_paths ${draco_include_paths} + "${gtest_path}/include" "${gtest_path}/googlemock" + "${gtest_path}/googletest/include" "${gtest_path}/googletest") + + list(APPEND draco_gtest_all "${gtest_path}/googletest/src/gtest-all.cc") + list(APPEND draco_gtest_main "${gtest_path}/googletest/src/gtest_main.cc") +endmacro() + + +# Determines the location of TinyGLTF and updates the build configuration +# accordingly. +macro(draco_setup_tinygltf) + if(DRACO_TINYGLTF_PATH) + set(tinygltf_path "${DRACO_TINYGLTF_PATH}") + + if(NOT IS_DIRECTORY "${tinygltf_path}") + message(FATAL_ERROR "DRACO_TINYGLTF_PATH does not exist.") + endif() + else() + set(tinygltf_path "${draco_root}/third_party/tinygltf") + + if(NOT IS_DIRECTORY "${tinygltf_path}") + draco_die_missing_submodule("${tinygltf_path}") + endif() + endif() + + list(APPEND draco_include_paths "${tinygltf_path}") +endmacro() diff --git a/contrib/draco/cmake/draco_emscripten.cmake b/contrib/draco/cmake/draco_emscripten.cmake index 10c935043..c9616ae86 100644 --- a/contrib/draco/cmake/draco_emscripten.cmake +++ b/contrib/draco/cmake/draco_emscripten.cmake @@ -1,3 +1,17 @@ +# Copyright 2021 The Draco Authors +# +# 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. + if(DRACO_CMAKE_DRACO_EMSCRIPTEN_CMAKE_) return() endif() # DRACO_CMAKE_DRACO_EMSCRIPTEN_CMAKE_ @@ -18,39 +32,64 @@ endmacro() # Obtains the required Emscripten flags for Draco targets. macro(draco_get_required_emscripten_flags) - set(em_FLAG_LIST_VAR) + set(em_FLAG_LIST_VAR_COMPILER) + set(em_FLAG_LIST_VAR_LINKER) set(em_flags) - set(em_single_arg_opts FLAG_LIST_VAR) + set(em_single_arg_opts FLAG_LIST_VAR_COMPILER FLAG_LIST_VAR_LINKER) set(em_multi_arg_opts) cmake_parse_arguments(em "${em_flags}" "${em_single_arg_opts}" "${em_multi_arg_opts}" ${ARGN}) - if(NOT em_FLAG_LIST_VAR) - message(FATAL "draco_get_required_emscripten_flags: FLAG_LIST_VAR required") + if(NOT em_FLAG_LIST_VAR_COMPILER) + message( + FATAL + "draco_get_required_emscripten_flags: FLAG_LIST_VAR_COMPILER required") + endif() + + if(NOT em_FLAG_LIST_VAR_LINKER) + message( + FATAL + "draco_get_required_emscripten_flags: FLAG_LIST_VAR_LINKER required") endif() if(DRACO_JS_GLUE) unset(required_flags) - list(APPEND ${em_FLAG_LIST_VAR} "-sALLOW_MEMORY_GROWTH=1") - list(APPEND ${em_FLAG_LIST_VAR} "-Wno-almost-asm") - list(APPEND ${em_FLAG_LIST_VAR} "--memory-init-file" "0") - list(APPEND ${em_FLAG_LIST_VAR} "-fno-omit-frame-pointer") - list(APPEND ${em_FLAG_LIST_VAR} "-sMODULARIZE=1") - list(APPEND ${em_FLAG_LIST_VAR} "-sNO_FILESYSTEM=1") - list(APPEND ${em_FLAG_LIST_VAR} "-sEXPORTED_RUNTIME_METHODS=[]") - list(APPEND ${em_FLAG_LIST_VAR} "-sPRECISE_F32=1") - list(APPEND ${em_FLAG_LIST_VAR} "-sNODEJS_CATCH_EXIT=0") - list(APPEND ${em_FLAG_LIST_VAR} "-sNODEJS_CATCH_REJECTION=0") + # TODO(tomfinegan): Revisit splitting of compile/link flags for Emscripten, + # and drop -Wno-unused-command-line-argument. Emscripten complains about + # what are supposedly link-only flags sent with compile commands, but then + # proceeds to produce broken code if the warnings are heeded. + list(APPEND ${em_FLAG_LIST_VAR_COMPILER} + "-Wno-unused-command-line-argument") + + list(APPEND ${em_FLAG_LIST_VAR_COMPILER} "-Wno-almost-asm") + list(APPEND ${em_FLAG_LIST_VAR_COMPILER} "--memory-init-file" "0") + list(APPEND ${em_FLAG_LIST_VAR_COMPILER} "-fno-omit-frame-pointer") + + # According to Emscripten the following flags are linker only, but sending + # these flags (en masse) to only the linker results in a broken Emscripten + # build with an empty DracoDecoderModule. + list(APPEND ${em_FLAG_LIST_VAR_COMPILER} "-sALLOW_MEMORY_GROWTH=1") + list(APPEND ${em_FLAG_LIST_VAR_COMPILER} "-sMODULARIZE=1") + list(APPEND ${em_FLAG_LIST_VAR_COMPILER} "-sFILESYSTEM=0") + list(APPEND ${em_FLAG_LIST_VAR_COMPILER} + "-sEXPORTED_FUNCTIONS=[\"_free\",\"_malloc\"]") + list(APPEND ${em_FLAG_LIST_VAR_COMPILER} "-sPRECISE_F32=1") + list(APPEND ${em_FLAG_LIST_VAR_COMPILER} "-sNODEJS_CATCH_EXIT=0") + list(APPEND ${em_FLAG_LIST_VAR_COMPILER} "-sNODEJS_CATCH_REJECTION=0") if(DRACO_FAST) - list(APPEND ${em_FLAG_LIST_VAR} "--llvm-lto" "1") + list(APPEND ${em_FLAG_LIST_VAR_COMPILER} "--llvm-lto" "1") endif() + + # The WASM flag is reported as linker only. if(DRACO_WASM) - list(APPEND ${em_FLAG_LIST_VAR} "-sWASM=1") + list(APPEND ${em_FLAG_LIST_VAR_COMPILER} "-sWASM=1") else() - list(APPEND ${em_FLAG_LIST_VAR} "-sWASM=0") + list(APPEND ${em_FLAG_LIST_VAR_COMPILER} "-sWASM=0") endif() + + # The LEGACY_VM_SUPPORT flag is reported as linker only. if(DRACO_IE_COMPATIBLE) - list(APPEND ${em_FLAG_LIST_VAR} "-sLEGACY_VM_SUPPORT=1") + list(APPEND ${em_FLAG_LIST_VAR_COMPILER} "-sLEGACY_VM_SUPPORT=1") endif() endif() endmacro() @@ -66,10 +105,11 @@ macro(draco_generate_emscripten_glue) "${glue_multi_arg_opts}" ${ARGN}) if(DRACO_VERBOSE GREATER 1) - message("--------- draco_generate_emscripten_glue -----------\n" - "glue_INPUT_IDL=${glue_INPUT_IDL}\n" - "glue_OUTPUT_PATH=${glue_OUTPUT_PATH}\n" ] - "----------------------------------------------------\n") + message( + "--------- draco_generate_emscripten_glue -----------\n" + "glue_INPUT_IDL=${glue_INPUT_IDL}\n" + "glue_OUTPUT_PATH=${glue_OUTPUT_PATH}\n" + "----------------------------------------------------\n") endif() if(NOT glue_INPUT_IDL OR NOT glue_OUTPUT_PATH) @@ -79,22 +119,22 @@ macro(draco_generate_emscripten_glue) endif() # Generate the glue source. - execute_process(COMMAND ${PYTHON_EXECUTABLE} - $ENV{EMSCRIPTEN}/tools/webidl_binder.py - ${glue_INPUT_IDL} ${glue_OUTPUT_PATH}) + execute_process( + COMMAND ${PYTHON_EXECUTABLE} $ENV{EMSCRIPTEN}/tools/webidl_binder.py + ${glue_INPUT_IDL} ${glue_OUTPUT_PATH}) if(NOT EXISTS "${glue_OUTPUT_PATH}.cpp") message(FATAL_ERROR "JS glue generation failed for ${glue_INPUT_IDL}.") endif() # Create a dependency so that it regenerated on edits. - add_custom_command(OUTPUT "${glue_OUTPUT_PATH}.cpp" - COMMAND ${PYTHON_EXECUTABLE} - $ENV{EMSCRIPTEN}/tools/webidl_binder.py - ${glue_INPUT_IDL} ${glue_OUTPUT_PATH} - DEPENDS ${draco_js_dec_idl} - COMMENT "Generating ${glue_OUTPUT_PATH}.cpp." - WORKING_DIRECTORY ${draco_build} - VERBATIM) + add_custom_command( + OUTPUT "${glue_OUTPUT_PATH}.cpp" + COMMAND ${PYTHON_EXECUTABLE} $ENV{EMSCRIPTEN}/tools/webidl_binder.py + ${glue_INPUT_IDL} ${glue_OUTPUT_PATH} + DEPENDS ${draco_js_dec_idl} + COMMENT "Generating ${glue_OUTPUT_PATH}.cpp." + WORKING_DIRECTORY ${draco_build} + VERBATIM) endmacro() # Wrapper for draco_add_executable() that handles the extra work necessary for @@ -120,8 +160,14 @@ macro(draco_add_emscripten_executable) unset(emexe_LINK_FLAGS) set(optional_args) set(single_value_args NAME GLUE_PATH) - set(multi_value_args SOURCES DEFINES FEATURES INCLUDES LINK_FLAGS - PRE_LINK_JS_SOURCES POST_LINK_JS_SOURCES) + set(multi_value_args + SOURCES + DEFINES + FEATURES + INCLUDES + LINK_FLAGS + PRE_LINK_JS_SOURCES + POST_LINK_JS_SOURCES) cmake_parse_arguments(emexe "${optional_args}" "${single_value_args}" "${multi_value_args}" ${ARGN}) @@ -136,49 +182,50 @@ macro(draco_add_emscripten_executable) endif() if(DRACO_VERBOSE GREATER 1) - message("--------- draco_add_emscripten_executable ---------\n" - "emexe_NAME=${emexe_NAME}\n" - "emexe_SOURCES=${emexe_SOURCES}\n" - "emexe_DEFINES=${emexe_DEFINES}\n" - "emexe_INCLUDES=${emexe_INCLUDES}\n" - "emexe_LINK_FLAGS=${emexe_LINK_FLAGS}\n" - "emexe_GLUE_PATH=${emexe_GLUE_PATH}\n" - "emexe_FEATURES=${emexe_FEATURES}\n" - "emexe_PRE_LINK_JS_SOURCES=${emexe_PRE_LINK_JS_SOURCES}\n" - "emexe_POST_LINK_JS_SOURCES=${emexe_POST_LINK_JS_SOURCES}\n" - "----------------------------------------------------\n") + message( + "--------- draco_add_emscripten_executable ---------\n" + "emexe_NAME=${emexe_NAME}\n" + "emexe_SOURCES=${emexe_SOURCES}\n" + "emexe_DEFINES=${emexe_DEFINES}\n" + "emexe_INCLUDES=${emexe_INCLUDES}\n" + "emexe_LINK_FLAGS=${emexe_LINK_FLAGS}\n" + "emexe_GLUE_PATH=${emexe_GLUE_PATH}\n" + "emexe_FEATURES=${emexe_FEATURES}\n" + "emexe_PRE_LINK_JS_SOURCES=${emexe_PRE_LINK_JS_SOURCES}\n" + "emexe_POST_LINK_JS_SOURCES=${emexe_POST_LINK_JS_SOURCES}\n" + "----------------------------------------------------\n") endif() # The Emscripten linker needs the C++ flags in addition to whatever has been # passed in with the target. list(APPEND emexe_LINK_FLAGS ${DRACO_CXX_FLAGS}) - if(DRACO_GLTF) - draco_add_executable(NAME - ${emexe_NAME} - OUTPUT_NAME - ${emexe_NAME}_gltf - SOURCES - ${emexe_SOURCES} - DEFINES - ${emexe_DEFINES} - INCLUDES - ${emexe_INCLUDES} - LINK_FLAGS - ${emexe_LINK_FLAGS}) + if(DRACO_GLTF_BITSTREAM) + # Add "_gltf" suffix to target output name. + draco_add_executable( + NAME ${emexe_NAME} + OUTPUT_NAME ${emexe_NAME}_gltf + SOURCES ${emexe_SOURCES} + DEFINES ${emexe_DEFINES} + INCLUDES ${emexe_INCLUDES} + LINK_FLAGS ${emexe_LINK_FLAGS}) else() - draco_add_executable(NAME ${emexe_NAME} SOURCES ${emexe_SOURCES} DEFINES - ${emexe_DEFINES} INCLUDES ${emexe_INCLUDES} LINK_FLAGS - ${emexe_LINK_FLAGS}) + draco_add_executable( + NAME ${emexe_NAME} + SOURCES ${emexe_SOURCES} + DEFINES ${emexe_DEFINES} + INCLUDES ${emexe_INCLUDES} + LINK_FLAGS ${emexe_LINK_FLAGS}) endif() foreach(feature ${emexe_FEATURES}) draco_enable_feature(FEATURE ${feature} TARGETS ${emexe_NAME}) endforeach() - set_property(SOURCE ${emexe_SOURCES} - APPEND - PROPERTY OBJECT_DEPENDS "${emexe_GLUE_PATH}.cpp") + set_property( + SOURCE ${emexe_SOURCES} + APPEND + PROPERTY OBJECT_DEPENDS "${emexe_GLUE_PATH}.cpp") em_link_pre_js(${emexe_NAME} ${emexe_PRE_LINK_JS_SOURCES}) em_link_post_js(${emexe_NAME} "${emexe_GLUE_PATH}.js" ${emexe_POST_LINK_JS_SOURCES}) diff --git a/contrib/draco/cmake/draco_flags.cmake b/contrib/draco/cmake/draco_flags.cmake index 0397859a4..f3b24c6e1 100644 --- a/contrib/draco/cmake/draco_flags.cmake +++ b/contrib/draco/cmake/draco_flags.cmake @@ -1,3 +1,17 @@ +# Copyright 2021 The Draco Authors +# +# 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. + if(DRACO_CMAKE_DRACO_FLAGS_CMAKE_) return() endif() # DRACO_CMAKE_DRACO_FLAGS_CMAKE_ @@ -24,7 +38,7 @@ macro(draco_set_compiler_flags_for_sources) endif() set_source_files_properties(${compiler_SOURCES} PROPERTIES COMPILE_FLAGS - ${compiler_FLAGS}) + ${compiler_FLAGS}) if(DRACO_VERBOSE GREATER 1) foreach(source ${compiler_SOURCES}) @@ -85,8 +99,8 @@ macro(draco_test_cxx_flag) # are passed as a list it will remove the list separators, and attempt to run # a compile command using list entries concatenated together as a single # argument. Avoid the problem by forcing the argument to be a string. - draco_set_and_stringify(SOURCE_VARS all_cxx_flags DEST all_cxx_flags) - check_cxx_compiler_flag("${all_cxx_flags}" draco_all_cxx_flags_pass) + draco_set_and_stringify(SOURCE_VARS all_cxx_flags DEST all_cxx_flags_string) + check_cxx_compiler_flag("${all_cxx_flags_string}" draco_all_cxx_flags_pass) if(cxx_test_FLAG_REQUIRED AND NOT draco_all_cxx_flags_pass) draco_die("Flag test failed for required flag(s): " @@ -245,3 +259,34 @@ macro(draco_set_cxx_flags) draco_test_cxx_flag(FLAG_LIST_VAR_NAMES ${cxx_flag_lists}) endif() endmacro() + +# Collects Draco built-in and user-specified linker flags and tests them. Halts +# configuration and reports the error when any flags cause the build to fail. +# +# Note: draco_test_exe_linker_flag() does the real work of setting the flags and +# running the test compile commands. +macro(draco_set_exe_linker_flags) + unset(linker_flag_lists) + + if(DRACO_VERBOSE) + message("draco_set_exe_linker_flags: " + "draco_base_exe_linker_flags=${draco_base_exe_linker_flags}") + endif() + + if(draco_base_exe_linker_flags) + list(APPEND linker_flag_lists draco_base_exe_linker_flags) + endif() + + if(linker_flag_lists) + unset(test_linker_flags) + + if(DRACO_VERBOSE) + message("draco_set_exe_linker_flags: " + "linker_flag_lists=${linker_flag_lists}") + endif() + + draco_set_and_stringify(DEST test_linker_flags SOURCE_VARS + ${linker_flag_lists}) + draco_test_exe_linker_flag(FLAG_LIST_VAR_NAME test_linker_flags) + endif() +endmacro() diff --git a/contrib/draco/cmake/draco_helpers.cmake b/contrib/draco/cmake/draco_helpers.cmake index 0b3b804cf..69e24c5be 100644 --- a/contrib/draco/cmake/draco_helpers.cmake +++ b/contrib/draco/cmake/draco_helpers.cmake @@ -1,3 +1,17 @@ +# Copyright 2021 The Draco Authors +# +# 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. + if(DRACO_CMAKE_DRACO_HELPERS_CMAKE_) return() endif() # DRACO_CMAKE_DRACO_HELPERS_CMAKE_ diff --git a/contrib/draco/cmake/draco_install.cmake b/contrib/draco/cmake/draco_install.cmake index 09bfb591d..3be1ba163 100644 --- a/contrib/draco/cmake/draco_install.cmake +++ b/contrib/draco/cmake/draco_install.cmake @@ -1,32 +1,32 @@ +# Copyright 2021 The Draco Authors +# +# 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. + if(DRACO_CMAKE_DRACO_INSTALL_CMAKE_) return() endif() # DRACO_CMAKE_DRACO_INSTALL_CMAKE_ set(DRACO_CMAKE_DRACO_INSTALL_CMAKE_ 1) +include(CMakePackageConfigHelpers) +include(GNUInstallDirs) + # Sets up the draco install targets. Must be called after the static library # target is created. macro(draco_setup_install_target) - include(GNUInstallDirs) - - # pkg-config: draco.pc - set(prefix "${CMAKE_INSTALL_PREFIX}") - set(exec_prefix "\${prefix}") - set(libdir "\${prefix}/${CMAKE_INSTALL_LIBDIR}") - set(includedir "\${prefix}/${CMAKE_INSTALL_INCLUDEDIR}") - set(draco_lib_name "draco") - - configure_file("${draco_root}/cmake/draco.pc.template" - "${draco_build}/draco.pc" @ONLY NEWLINE_STYLE UNIX) - install(FILES "${draco_build}/draco.pc" - DESTINATION "${prefix}/${CMAKE_INSTALL_LIBDIR}/pkgconfig") - - # CMake config: draco-config.cmake - set(DRACO_INCLUDE_DIRS "${prefix}/${CMAKE_INSTALL_INCLUDEDIR}") - configure_file("${draco_root}/cmake/draco-config.cmake.template" - "${draco_build}/draco-config.cmake" @ONLY NEWLINE_STYLE UNIX) - install( - FILES "${draco_build}/draco-config.cmake" - DESTINATION "${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_DATAROOTDIR}/cmake") + set(bin_path "${CMAKE_INSTALL_BINDIR}") + set(data_path "${CMAKE_INSTALL_DATAROOTDIR}") + set(includes_path "${CMAKE_INSTALL_INCLUDEDIR}") + set(libs_path "${CMAKE_INSTALL_LIBDIR}") foreach(file ${draco_sources}) if(file MATCHES "h$") @@ -34,46 +34,88 @@ macro(draco_setup_install_target) endif() endforeach() + list(REMOVE_DUPLICATES draco_api_includes) + # Strip $draco_src_root from the file paths: we need to install relative to # $include_directory. list(TRANSFORM draco_api_includes REPLACE "${draco_src_root}/" "") - set(include_directory "${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_INCLUDEDIR}") foreach(draco_api_include ${draco_api_includes}) get_filename_component(file_directory ${draco_api_include} DIRECTORY) - set(target_directory "${include_directory}/draco/${file_directory}") + set(target_directory "${includes_path}/draco/${file_directory}") install(FILES ${draco_src_root}/${draco_api_include} DESTINATION "${target_directory}") endforeach() - install( - FILES "${draco_build}/draco/draco_features.h" - DESTINATION "${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_INCLUDEDIR}/draco/") + install(FILES "${draco_build}/draco/draco_features.h" + DESTINATION "${includes_path}/draco/") - install(TARGETS draco_decoder DESTINATION - "${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_BINDIR}") - install(TARGETS draco_encoder DESTINATION - "${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_BINDIR}") + install(TARGETS draco_decoder DESTINATION "${bin_path}") + install(TARGETS draco_encoder DESTINATION "${bin_path}") + + if(DRACO_TRANSCODER_SUPPORTED) + install(TARGETS draco_transcoder DESTINATION "${bin_path}") + endif() if(MSVC) - install(TARGETS draco DESTINATION - "${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_LIBDIR}") + install( + TARGETS draco + EXPORT dracoExport + RUNTIME DESTINATION "${bin_path}" + ARCHIVE DESTINATION "${libs_path}" + LIBRARY DESTINATION "${libs_path}") else() - install(TARGETS draco_static DESTINATION - "${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_LIBDIR}") + install( + TARGETS draco_static + EXPORT dracoExport + DESTINATION "${libs_path}") + if(BUILD_SHARED_LIBS) - install(TARGETS draco_shared DESTINATION - "${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_LIBDIR}") + install( + TARGETS draco_shared + EXPORT dracoExport + RUNTIME DESTINATION "${bin_path}" + ARCHIVE DESTINATION "${libs_path}" + LIBRARY DESTINATION "${libs_path}") endif() endif() if(DRACO_UNITY_PLUGIN) - install(TARGETS dracodec_unity DESTINATION - "${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_LIBDIR}") - endif() - if(DRACO_MAYA_PLUGIN) - install(TARGETS draco_maya_wrapper DESTINATION - "${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_LIBDIR}") + install(TARGETS dracodec_unity DESTINATION "${libs_path}") endif() + if(DRACO_MAYA_PLUGIN) + install(TARGETS draco_maya_wrapper DESTINATION "${libs_path}") + endif() + + # pkg-config: draco.pc + configure_file("${draco_root}/cmake/draco.pc.template" + "${draco_build}/draco.pc" @ONLY NEWLINE_STYLE UNIX) + install(FILES "${draco_build}/draco.pc" DESTINATION "${libs_path}/pkgconfig") + + # CMake config: draco-config.cmake + configure_package_config_file( + "${draco_root}/cmake/draco-config.cmake.template" + "${draco_build}/draco-config.cmake" + INSTALL_DESTINATION "${data_path}/cmake/draco") + + write_basic_package_version_file( + "${draco_build}/draco-config-version.cmake" + VERSION ${DRACO_VERSION} + COMPATIBILITY AnyNewerVersion) + + export( + EXPORT dracoExport + NAMESPACE draco:: + FILE "${draco_build}/draco-targets.cmake") + + install( + EXPORT dracoExport + NAMESPACE draco:: + FILE draco-targets.cmake + DESTINATION "${data_path}/cmake/draco") + + install(FILES "${draco_build}/draco-config.cmake" + "${draco_build}/draco-config-version.cmake" + DESTINATION "${data_path}/cmake/draco") endmacro() diff --git a/contrib/draco/cmake/draco_intrinsics.cmake b/contrib/draco/cmake/draco_intrinsics.cmake index 9011c0de5..178df97a6 100644 --- a/contrib/draco/cmake/draco_intrinsics.cmake +++ b/contrib/draco/cmake/draco_intrinsics.cmake @@ -1,3 +1,17 @@ +# Copyright 2021 The Draco Authors +# +# 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. + if(DRACO_CMAKE_DRACO_INTRINSICS_CMAKE_) return() endif() # DRACO_CMAKE_DRACO_INTRINSICS_CMAKE_ @@ -61,17 +75,15 @@ macro(draco_process_intrinsics_sources) unset(sse4_sources) list(APPEND sse4_sources ${arg_SOURCES}) - list(FILTER sse4_sources INCLUDE REGEX - "${draco_sse4_source_file_suffix}$") + list(FILTER sse4_sources INCLUDE REGEX "${draco_sse4_source_file_suffix}$") if(sse4_sources) unset(sse4_flags) - draco_get_intrinsics_flag_for_suffix(SUFFIX - ${draco_sse4_source_file_suffix} - VARIABLE sse4_flags) + draco_get_intrinsics_flag_for_suffix( + SUFFIX ${draco_sse4_source_file_suffix} VARIABLE sse4_flags) if(sse4_flags) draco_set_compiler_flags_for_sources(SOURCES ${sse4_sources} FLAGS - ${sse4_flags}) + ${sse4_flags}) endif() endif() endif() @@ -79,17 +91,15 @@ macro(draco_process_intrinsics_sources) if(DRACO_ENABLE_NEON AND draco_have_neon) unset(neon_sources) list(APPEND neon_sources ${arg_SOURCES}) - list(FILTER neon_sources INCLUDE REGEX - "${draco_neon_source_file_suffix}$") + list(FILTER neon_sources INCLUDE REGEX "${draco_neon_source_file_suffix}$") if(neon_sources AND DRACO_NEON_INTRINSICS_FLAG) unset(neon_flags) - draco_get_intrinsics_flag_for_suffix(SUFFIX - ${draco_neon_source_file_suffix} - VARIABLE neon_flags) + draco_get_intrinsics_flag_for_suffix( + SUFFIX ${draco_neon_source_file_suffix} VARIABLE neon_flags) if(neon_flags) draco_set_compiler_flags_for_sources(SOURCES ${neon_sources} FLAGS - ${neon_flags}) + ${neon_flags}) endif() endif() endif() diff --git a/contrib/draco/cmake/draco_options.cmake b/contrib/draco/cmake/draco_options.cmake index 832bfb69f..085149774 100644 --- a/contrib/draco/cmake/draco_options.cmake +++ b/contrib/draco/cmake/draco_options.cmake @@ -1,3 +1,17 @@ +# Copyright 2021 The Draco Authors +# +# 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. + if(DRACO_CMAKE_DRACO_OPTIONS_CMAKE_) return() endif() # DRACO_CMAKE_DRACO_OPTIONS_CMAKE_ @@ -18,17 +32,22 @@ macro(draco_option) cmake_parse_arguments(option "${optional_args}" "${single_value_args}" "${multi_value_args}" ${ARGN}) - if(NOT (option_NAME AND option_HELPSTRING AND DEFINED option_VALUE)) + if(NOT + (option_NAME + AND option_HELPSTRING + AND DEFINED option_VALUE)) message(FATAL_ERROR "draco_option: NAME HELPSTRING and VALUE required.") endif() option(${option_NAME} ${option_HELPSTRING} ${option_VALUE}) if(DRACO_VERBOSE GREATER 2) - message("--------- draco_option ---------\n" "option_NAME=${option_NAME}\n" - "option_HELPSTRING=${option_HELPSTRING}\n" - "option_VALUE=${option_VALUE}\n" - "------------------------------------------\n") + message( + "--------- draco_option ---------\n" + "option_NAME=${option_NAME}\n" + "option_HELPSTRING=${option_HELPSTRING}\n" + "option_VALUE=${option_VALUE}\n" + "------------------------------------------\n") endif() list(APPEND draco_options ${option_NAME}) @@ -44,33 +63,74 @@ endmacro() # Set default options. macro(draco_set_default_options) - draco_option(NAME DRACO_FAST HELPSTRING "Try to build faster libs." VALUE OFF) - draco_option(NAME DRACO_JS_GLUE HELPSTRING - "Enable JS Glue and JS targets when using Emscripten." VALUE ON) - draco_option(NAME DRACO_IE_COMPATIBLE HELPSTRING - "Enable support for older IE builds when using Emscripten." VALUE - OFF) - draco_option(NAME DRACO_MESH_COMPRESSION HELPSTRING "Enable mesh compression." - VALUE ON) - draco_option(NAME DRACO_POINT_CLOUD_COMPRESSION HELPSTRING - "Enable point cloud compression." VALUE ON) - draco_option(NAME DRACO_PREDICTIVE_EDGEBREAKER HELPSTRING - "Enable predictive edgebreaker." VALUE ON) - draco_option(NAME DRACO_STANDARD_EDGEBREAKER HELPSTRING - "Enable stand edgebreaker." VALUE ON) - draco_option(NAME DRACO_BACKWARDS_COMPATIBILITY HELPSTRING - "Enable backwards compatibility." VALUE ON) - draco_option(NAME DRACO_DECODER_ATTRIBUTE_DEDUPLICATION HELPSTRING - "Enable attribute deduping." VALUE OFF) - draco_option(NAME DRACO_TESTS HELPSTRING "Enables tests." VALUE OFF) - draco_option(NAME DRACO_WASM HELPSTRING "Enables WASM support." VALUE OFF) - draco_option(NAME DRACO_UNITY_PLUGIN HELPSTRING - "Build plugin library for Unity." VALUE OFF) - draco_option(NAME DRACO_ANIMATION_ENCODING HELPSTRING "Enable animation." - VALUE OFF) - draco_option(NAME DRACO_GLTF HELPSTRING "Support GLTF." VALUE OFF) - draco_option(NAME DRACO_MAYA_PLUGIN HELPSTRING - "Build plugin library for Maya." VALUE OFF) + draco_option( + NAME DRACO_FAST + HELPSTRING "Try to build faster libs." + VALUE OFF) + draco_option( + NAME DRACO_JS_GLUE + HELPSTRING "Enable JS Glue and JS targets when using Emscripten." + VALUE ON) + draco_option( + NAME DRACO_IE_COMPATIBLE + HELPSTRING "Enable support for older IE builds when using Emscripten." + VALUE OFF) + draco_option( + NAME DRACO_MESH_COMPRESSION + HELPSTRING "Enable mesh compression." + VALUE ON) + draco_option( + NAME DRACO_POINT_CLOUD_COMPRESSION + HELPSTRING "Enable point cloud compression." + VALUE ON) + draco_option( + NAME DRACO_PREDICTIVE_EDGEBREAKER + HELPSTRING "Enable predictive edgebreaker." + VALUE ON) + draco_option( + NAME DRACO_STANDARD_EDGEBREAKER + HELPSTRING "Enable stand edgebreaker." + VALUE ON) + draco_option( + NAME DRACO_BACKWARDS_COMPATIBILITY + HELPSTRING "Enable backwards compatibility." + VALUE ON) + draco_option( + NAME DRACO_DECODER_ATTRIBUTE_DEDUPLICATION + HELPSTRING "Enable attribute deduping." + VALUE OFF) + draco_option( + NAME DRACO_TESTS + HELPSTRING "Enables tests." + VALUE OFF) + draco_option( + NAME DRACO_WASM + HELPSTRING "Enables WASM support." + VALUE OFF) + draco_option( + NAME DRACO_UNITY_PLUGIN + HELPSTRING "Build plugin library for Unity." + VALUE OFF) + draco_option( + NAME DRACO_ANIMATION_ENCODING + HELPSTRING "Enable animation." + VALUE OFF) + draco_option( + NAME DRACO_GLTF_BITSTREAM + HELPSTRING "Draco GLTF extension bitstream specified features only." + VALUE OFF) + draco_option( + NAME DRACO_MAYA_PLUGIN + HELPSTRING "Build plugin library for Maya." + VALUE OFF) + draco_option( + NAME DRACO_TRANSCODER_SUPPORTED + HELPSTRING "Enable the Draco transcoder." + VALUE OFF) + draco_option( + NAME DRACO_DEBUG_COMPILER_WARNINGS + HELPSTRING "Turn on more warnings." + VALUE OFF) draco_check_deprecated_options() endmacro() @@ -117,14 +177,16 @@ macro(draco_check_deprecated_options) DRACO_MAYA_PLUGIN) draco_handle_deprecated_option(OLDNAME BUILD_USD_PLUGIN NEWNAME BUILD_SHARED_LIBS) + draco_handle_deprecated_option(OLDNAME DRACO_GLTF NEWNAME + DRACO_GLTF_BITSTREAM) endmacro() # Macro for setting Draco features based on user configuration. Features enabled # by this macro are Draco global. macro(draco_set_optional_features) - if(DRACO_GLTF) - # Override settings when building for GLTF. + if(DRACO_GLTF_BITSTREAM) + # Enable only the features included in the Draco GLTF bitstream spec. draco_enable_feature(FEATURE "DRACO_MESH_COMPRESSION_SUPPORTED") draco_enable_feature(FEATURE "DRACO_NORMAL_ENCODING_SUPPORTED") draco_enable_feature(FEATURE "DRACO_STANDARD_EDGEBREAKER_SUPPORTED") @@ -170,6 +232,11 @@ macro(draco_set_optional_features) set(CMAKE_POSITION_INDEPENDENT_CODE ON) endif() + if(DRACO_TRANSCODER_SUPPORTED) + draco_enable_feature(FEATURE "DRACO_TRANSCODER_SUPPORTED") + endif() + + endmacro() # Macro that handles tracking of Draco preprocessor symbols for the purpose of @@ -221,8 +288,56 @@ function(draco_generate_features_h) file(APPEND "${draco_features_file_name}.new" "#define ${feature}\n") endforeach() + if(MSVC) + if(NOT DRACO_DEBUG_COMPILER_WARNINGS) + file(APPEND "${draco_features_file_name}.new" + "// Enable DRACO_DEBUG_COMPILER_WARNINGS at CMake generation \n" + "// time to remove these pragmas.\n") + + # warning C4018: '': signed/unsigned mismatch. + file(APPEND "${draco_features_file_name}.new" + "#pragma warning(disable:4018)\n") + + # warning C4146: unary minus operator applied to unsigned type, result + # still unsigned + file(APPEND "${draco_features_file_name}.new" + "#pragma warning(disable:4146)\n") + + # warning C4244: 'return': conversion from '' to '', possible + # loss of data. + file(APPEND "${draco_features_file_name}.new" + "#pragma warning(disable:4244)\n") + + # warning C4267: 'initializing' conversion from '' to '', + # possible loss of data. + file(APPEND "${draco_features_file_name}.new" + "#pragma warning(disable:4267)\n") + + # warning C4305: 'context' : truncation from 'type1' to 'type2'. + file(APPEND "${draco_features_file_name}.new" + "#pragma warning(disable:4305)\n") + + # warning C4661: 'identifier' : no suitable definition provided for + # explicit template instantiation request. + file(APPEND "${draco_features_file_name}.new" + "#pragma warning(disable:4661)\n") + + # warning C4800: Implicit conversion from 'type' to bool. Possible + # information loss. + # Also, in older MSVC releases: + # warning C4800: 'type' : forcing value to bool 'true' or 'false' + # (performance warning). + file(APPEND "${draco_features_file_name}.new" + "#pragma warning(disable:4800)\n") + + # warning C4804: '': unsafe use of type '' in operation. + file(APPEND "${draco_features_file_name}.new" + "#pragma warning(disable:4804)\n") + endif() + endif() + file(APPEND "${draco_features_file_name}.new" - "\n#endif // DRACO_FEATURES_H_") + "\n#endif // DRACO_FEATURES_H_\n") # Will replace ${draco_features_file_name} only if the file content has # changed. This prevents forced Draco rebuilds after CMake runs. diff --git a/contrib/draco/cmake/draco_sanitizer.cmake b/contrib/draco/cmake/draco_sanitizer.cmake index d2e41a6cb..77d141481 100644 --- a/contrib/draco/cmake/draco_sanitizer.cmake +++ b/contrib/draco/cmake/draco_sanitizer.cmake @@ -1,3 +1,17 @@ +# Copyright 2021 The Draco Authors +# +# 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. + if(DRACO_CMAKE_DRACO_SANITIZER_CMAKE_) return() endif() # DRACO_CMAKE_DRACO_SANITIZER_CMAKE_ @@ -5,7 +19,9 @@ set(DRACO_CMAKE_DRACO_SANITIZER_CMAKE_ 1) # Handles the details of enabling sanitizers. macro(draco_configure_sanitizer) - if(DRACO_SANITIZE AND NOT EMSCRIPTEN AND NOT MSVC) + if(DRACO_SANITIZE + AND NOT EMSCRIPTEN + AND NOT MSVC) if(CMAKE_CXX_COMPILER_ID MATCHES "Clang") if(DRACO_SANITIZE MATCHES "cfi") list(APPEND SAN_CXX_FLAGS "-flto" "-fno-sanitize-trap=cfi") @@ -13,8 +29,8 @@ macro(draco_configure_sanitizer) "-fuse-ld=gold") endif() - if(${CMAKE_SIZEOF_VOID_P} EQUAL 4 - AND DRACO_SANITIZE MATCHES "integer|undefined") + if(${CMAKE_SIZEOF_VOID_P} EQUAL 4 AND DRACO_SANITIZE MATCHES + "integer|undefined") list(APPEND SAN_LINKER_FLAGS "--rtlib=compiler-rt" "-lgcc_s") endif() endif() diff --git a/contrib/draco/cmake/draco_targets.cmake b/contrib/draco/cmake/draco_targets.cmake index 0456c4d7b..c8c79f511 100644 --- a/contrib/draco/cmake/draco_targets.cmake +++ b/contrib/draco/cmake/draco_targets.cmake @@ -1,3 +1,17 @@ +# Copyright 2021 The Draco Authors +# +# 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. + if(DRACO_CMAKE_DRACO_TARGETS_CMAKE_) return() endif() # DRACO_CMAKE_DRACO_TARGETS_CMAKE_ @@ -51,26 +65,33 @@ macro(draco_add_executable) unset(exe_LIB_DEPS) set(optional_args TEST) set(single_value_args NAME OUTPUT_NAME) - set(multi_value_args SOURCES DEFINES INCLUDES COMPILE_FLAGS LINK_FLAGS - OBJLIB_DEPS LIB_DEPS) + set(multi_value_args + SOURCES + DEFINES + INCLUDES + COMPILE_FLAGS + LINK_FLAGS + OBJLIB_DEPS + LIB_DEPS) cmake_parse_arguments(exe "${optional_args}" "${single_value_args}" "${multi_value_args}" ${ARGN}) if(DRACO_VERBOSE GREATER 1) - message("--------- draco_add_executable ---------\n" - "exe_TEST=${exe_TEST}\n" - "exe_TEST_DEFINES_MAIN=${exe_TEST_DEFINES_MAIN}\n" - "exe_NAME=${exe_NAME}\n" - "exe_OUTPUT_NAME=${exe_OUTPUT_NAME}\n" - "exe_SOURCES=${exe_SOURCES}\n" - "exe_DEFINES=${exe_DEFINES}\n" - "exe_INCLUDES=${exe_INCLUDES}\n" - "exe_COMPILE_FLAGS=${exe_COMPILE_FLAGS}\n" - "exe_LINK_FLAGS=${exe_LINK_FLAGS}\n" - "exe_OBJLIB_DEPS=${exe_OBJLIB_DEPS}\n" - "exe_LIB_DEPS=${exe_LIB_DEPS}\n" - "------------------------------------------\n") + message( + "--------- draco_add_executable ---------\n" + "exe_TEST=${exe_TEST}\n" + "exe_TEST_DEFINES_MAIN=${exe_TEST_DEFINES_MAIN}\n" + "exe_NAME=${exe_NAME}\n" + "exe_OUTPUT_NAME=${exe_OUTPUT_NAME}\n" + "exe_SOURCES=${exe_SOURCES}\n" + "exe_DEFINES=${exe_DEFINES}\n" + "exe_INCLUDES=${exe_INCLUDES}\n" + "exe_COMPILE_FLAGS=${exe_COMPILE_FLAGS}\n" + "exe_LINK_FLAGS=${exe_LINK_FLAGS}\n" + "exe_OBJLIB_DEPS=${exe_OBJLIB_DEPS}\n" + "exe_LIB_DEPS=${exe_LIB_DEPS}\n" + "------------------------------------------\n") endif() if(NOT (exe_NAME AND exe_SOURCES)) @@ -87,7 +108,12 @@ macro(draco_add_executable) endif() add_executable(${exe_NAME} ${exe_SOURCES}) - set_target_properties(${exe_NAME} PROPERTIES VERSION ${DRACO_VERSION}) + + target_compile_features(${exe_NAME} PUBLIC cxx_std_11) + + if(NOT EMSCRIPTEN) + set_target_properties(${exe_NAME} PROPERTIES VERSION ${DRACO_VERSION}) + endif() if(exe_OUTPUT_NAME) set_target_properties(${exe_NAME} PROPERTIES OUTPUT_NAME ${exe_OUTPUT_NAME}) @@ -104,8 +130,8 @@ macro(draco_add_executable) endif() if(exe_COMPILE_FLAGS OR DRACO_CXX_FLAGS) - target_compile_options(${exe_NAME} - PRIVATE ${exe_COMPILE_FLAGS} ${DRACO_CXX_FLAGS}) + target_compile_options(${exe_NAME} PRIVATE ${exe_COMPILE_FLAGS} + ${DRACO_CXX_FLAGS}) endif() if(exe_LINK_FLAGS OR DRACO_EXE_LINKER_FLAGS) @@ -113,8 +139,8 @@ macro(draco_add_executable) list(APPEND exe_LINK_FLAGS "${DRACO_EXE_LINKER_FLAGS}") # LINK_FLAGS is managed as a string. draco_set_and_stringify(SOURCE "${exe_LINK_FLAGS}" DEST exe_LINK_FLAGS) - set_target_properties(${exe_NAME} - PROPERTIES LINK_FLAGS "${exe_LINK_FLAGS}") + set_target_properties(${exe_NAME} PROPERTIES LINK_FLAGS + "${exe_LINK_FLAGS}") else() target_link_options(${exe_NAME} PRIVATE ${exe_LINK_FLAGS} ${DRACO_EXE_LINKER_FLAGS}) @@ -136,12 +162,7 @@ macro(draco_add_executable) endif() if(exe_LIB_DEPS) - unset(exe_static) - if("${CMAKE_EXE_LINKER_FLAGS} ${DRACO_EXE_LINKER_FLAGS}" MATCHES "static") - set(exe_static ON) - endif() - - if(exe_static AND CMAKE_CXX_COMPILER_ID MATCHES "Clang|GNU") + if(CMAKE_CXX_COMPILER_ID MATCHES "^Clang|^GNU") # Third party dependencies can introduce dependencies on system and test # libraries. Since the target created here is an executable, and CMake # does not provide a method of controlling order of link dependencies, @@ -149,6 +170,10 @@ macro(draco_add_executable) # ensure that dependencies of third party targets can be resolved when # those dependencies happen to be resolved by dependencies of the current # target. + # TODO(tomfinegan): For portability use LINK_GROUP with RESCAN instead of + # directly (ab)using compiler/linker specific flags once CMake v3.24 is in + # wider use. See: + # https://cmake.org/cmake/help/latest/manual/cmake-generator-expressions.7.html#genex:LINK_GROUP list(INSERT exe_LIB_DEPS 0 -Wl,--start-group) list(APPEND exe_LIB_DEPS -Wl,--end-group) endif() @@ -209,27 +234,36 @@ macro(draco_add_library) unset(lib_TARGET_PROPERTIES) set(optional_args TEST) set(single_value_args NAME OUTPUT_NAME TYPE) - set(multi_value_args SOURCES DEFINES INCLUDES COMPILE_FLAGS LINK_FLAGS - OBJLIB_DEPS LIB_DEPS PUBLIC_INCLUDES TARGET_PROPERTIES) + set(multi_value_args + SOURCES + DEFINES + INCLUDES + COMPILE_FLAGS + LINK_FLAGS + OBJLIB_DEPS + LIB_DEPS + PUBLIC_INCLUDES + TARGET_PROPERTIES) cmake_parse_arguments(lib "${optional_args}" "${single_value_args}" "${multi_value_args}" ${ARGN}) if(DRACO_VERBOSE GREATER 1) - message("--------- draco_add_library ---------\n" - "lib_TEST=${lib_TEST}\n" - "lib_NAME=${lib_NAME}\n" - "lib_OUTPUT_NAME=${lib_OUTPUT_NAME}\n" - "lib_TYPE=${lib_TYPE}\n" - "lib_SOURCES=${lib_SOURCES}\n" - "lib_DEFINES=${lib_DEFINES}\n" - "lib_INCLUDES=${lib_INCLUDES}\n" - "lib_COMPILE_FLAGS=${lib_COMPILE_FLAGS}\n" - "lib_LINK_FLAGS=${lib_LINK_FLAGS}\n" - "lib_OBJLIB_DEPS=${lib_OBJLIB_DEPS}\n" - "lib_LIB_DEPS=${lib_LIB_DEPS}\n" - "lib_PUBLIC_INCLUDES=${lib_PUBLIC_INCLUDES}\n" - "---------------------------------------\n") + message( + "--------- draco_add_library ---------\n" + "lib_TEST=${lib_TEST}\n" + "lib_NAME=${lib_NAME}\n" + "lib_OUTPUT_NAME=${lib_OUTPUT_NAME}\n" + "lib_TYPE=${lib_TYPE}\n" + "lib_SOURCES=${lib_SOURCES}\n" + "lib_DEFINES=${lib_DEFINES}\n" + "lib_INCLUDES=${lib_INCLUDES}\n" + "lib_COMPILE_FLAGS=${lib_COMPILE_FLAGS}\n" + "lib_LINK_FLAGS=${lib_LINK_FLAGS}\n" + "lib_OBJLIB_DEPS=${lib_OBJLIB_DEPS}\n" + "lib_LIB_DEPS=${lib_LIB_DEPS}\n" + "lib_PUBLIC_INCLUDES=${lib_PUBLIC_INCLUDES}\n" + "---------------------------------------\n") endif() if(NOT (lib_NAME AND lib_TYPE)) @@ -256,14 +290,24 @@ macro(draco_add_library) endif() add_library(${lib_NAME} ${lib_TYPE} ${lib_SOURCES}) + + target_compile_features(${lib_NAME} PUBLIC cxx_std_11) + + target_include_directories(${lib_NAME} PUBLIC $) + + if(BUILD_SHARED_LIBS) + # Enable PIC for all targets in shared configurations. + set_target_properties(${lib_NAME} PROPERTIES POSITION_INDEPENDENT_CODE ON) + endif() + if(lib_SOURCES) draco_process_intrinsics_sources(TARGET ${lib_NAME} SOURCES ${lib_SOURCES}) endif() if(lib_OUTPUT_NAME) if(NOT (BUILD_SHARED_LIBS AND MSVC)) - set_target_properties(${lib_NAME} - PROPERTIES OUTPUT_NAME ${lib_OUTPUT_NAME}) + set_target_properties(${lib_NAME} PROPERTIES OUTPUT_NAME + ${lib_OUTPUT_NAME}) endif() endif() @@ -280,8 +324,8 @@ macro(draco_add_library) endif() if(lib_COMPILE_FLAGS OR DRACO_CXX_FLAGS) - target_compile_options(${lib_NAME} - PRIVATE ${lib_COMPILE_FLAGS} ${DRACO_CXX_FLAGS}) + target_compile_options(${lib_NAME} PRIVATE ${lib_COMPILE_FLAGS} + ${DRACO_CXX_FLAGS}) endif() if(lib_LINK_FLAGS) @@ -320,11 +364,12 @@ macro(draco_add_library) set_target_properties(${lib_NAME} PROPERTIES PREFIX "") endif() - # VERSION and SOVERSION as necessary - if(NOT lib_TYPE STREQUAL STATIC AND NOT lib_TYPE STREQUAL MODULE) - set_target_properties(${lib_NAME} PROPERTIES VERSION ${DRACO_VERSION}) - if(NOT MSVC) - set_target_properties(${lib_NAME} PROPERTIES SOVERSION ${DRACO_SOVERSION}) + if(NOT EMSCRIPTEN) + # VERSION and SOVERSION as necessary + if((lib_TYPE STREQUAL BUNDLE OR lib_TYPE STREQUAL SHARED) AND NOT MSVC) + set_target_properties( + ${lib_NAME} PROPERTIES VERSION ${DRACO_SOVERSION} + SOVERSION ${DRACO_SOVERSION_MAJOR}) endif() endif() diff --git a/contrib/draco/cmake/draco_test_config.h.cmake b/contrib/draco/cmake/draco_test_config.h.cmake index 77a574123..9bb174569 100644 --- a/contrib/draco/cmake/draco_test_config.h.cmake +++ b/contrib/draco/cmake/draco_test_config.h.cmake @@ -1,3 +1,17 @@ +// Copyright 2021 The Draco Authors +// +// 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 DRACO_TESTING_DRACO_TEST_CONFIG_H_ #define DRACO_TESTING_DRACO_TEST_CONFIG_H_ @@ -9,5 +23,6 @@ #define DRACO_TEST_DATA_DIR "${DRACO_TEST_DATA_DIR}" #define DRACO_TEST_TEMP_DIR "${DRACO_TEST_TEMP_DIR}" +#define DRACO_TEST_ROOT_DIR "${DRACO_TEST_ROOT_DIR}" #endif // DRACO_TESTING_DRACO_TEST_CONFIG_H_ diff --git a/contrib/draco/cmake/draco_tests.cmake b/contrib/draco/cmake/draco_tests.cmake index a6dfc5b57..1d905a969 100644 --- a/contrib/draco/cmake/draco_tests.cmake +++ b/contrib/draco/cmake/draco_tests.cmake @@ -1,3 +1,17 @@ +# Copyright 2021 The Draco Authors +# +# 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. + if(DRACO_CMAKE_DRACO_TESTS_CMAKE) return() endif() @@ -10,6 +24,13 @@ set(draco_factory_test_sources "${draco_src_root}/io/file_reader_factory_test.cc" "${draco_src_root}/io/file_writer_factory_test.cc") +list( + APPEND draco_test_common_sources + "${draco_src_root}/core/draco_test_base.h" + "${draco_src_root}/core/draco_test_utils.cc" + "${draco_src_root}/core/draco_test_utils.h" + "${draco_src_root}/core/status.cc") + list( APPEND draco_test_sources @@ -30,22 +51,23 @@ list( "${draco_src_root}/compression/point_cloud/point_cloud_kd_tree_encoding_test.cc" "${draco_src_root}/compression/point_cloud/point_cloud_sequential_encoding_test.cc" "${draco_src_root}/core/buffer_bit_coding_test.cc" - "${draco_src_root}/core/draco_test_base.h" - "${draco_src_root}/core/draco_test_utils.cc" - "${draco_src_root}/core/draco_test_utils.h" "${draco_src_root}/core/math_utils_test.cc" "${draco_src_root}/core/quantization_utils_test.cc" "${draco_src_root}/core/status_test.cc" "${draco_src_root}/core/vector_d_test.cc" "${draco_src_root}/io/file_reader_test_common.h" "${draco_src_root}/io/file_utils_test.cc" + "${draco_src_root}/io/file_writer_utils_test.cc" "${draco_src_root}/io/stdio_file_reader_test.cc" "${draco_src_root}/io/stdio_file_writer_test.cc" "${draco_src_root}/io/obj_decoder_test.cc" "${draco_src_root}/io/obj_encoder_test.cc" "${draco_src_root}/io/ply_decoder_test.cc" "${draco_src_root}/io/ply_reader_test.cc" + "${draco_src_root}/io/stl_decoder_test.cc" + "${draco_src_root}/io/stl_encoder_test.cc" "${draco_src_root}/io/point_cloud_io_test.cc" + "${draco_src_root}/mesh/corner_table_test.cc" "${draco_src_root}/mesh/mesh_are_equivalent_test.cc" "${draco_src_root}/mesh/mesh_cleanup_test.cc" "${draco_src_root}/mesh/triangle_soup_mesh_builder_test.cc" @@ -54,47 +76,71 @@ list( "${draco_src_root}/point_cloud/point_cloud_builder_test.cc" "${draco_src_root}/point_cloud/point_cloud_test.cc") -list(APPEND draco_gtest_all - "${draco_root}/../googletest/googletest/src/gtest-all.cc") -list(APPEND draco_gtest_main - "${draco_root}/../googletest/googletest/src/gtest_main.cc") +if(DRACO_TRANSCODER_SUPPORTED) + list( + APPEND draco_test_sources + "${draco_src_root}/animation/animation_test.cc" + "${draco_src_root}/io/gltf_decoder_test.cc" + "${draco_src_root}/io/gltf_encoder_test.cc" + "${draco_src_root}/io/gltf_utils_test.cc" + "${draco_src_root}/io/gltf_test_helper.cc" + "${draco_src_root}/io/gltf_test_helper.h" + "${draco_src_root}/io/scene_io_test.cc" + "${draco_src_root}/io/texture_io_test.cc" + "${draco_src_root}/material/material_library_test.cc" + "${draco_src_root}/material/material_test.cc" + "${draco_src_root}/metadata/property_table_test.cc" + "${draco_src_root}/metadata/structural_metadata_test.cc" + "${draco_src_root}/scene/instance_array_test.cc" + "${draco_src_root}/scene/light_test.cc" + "${draco_src_root}/scene/mesh_group_test.cc" + "${draco_src_root}/scene/scene_test.cc" + "${draco_src_root}/scene/scene_are_equivalent_test.cc" + "${draco_src_root}/scene/scene_utils_test.cc" + "${draco_src_root}/scene/trs_matrix_test.cc" + "${draco_src_root}/texture/texture_library_test.cc" + "${draco_src_root}/texture/texture_map_test.cc" + "${draco_src_root}/texture/texture_transform_test.cc") + +endif() macro(draco_setup_test_targets) if(DRACO_TESTS) + draco_setup_googletest() + if(NOT (EXISTS ${draco_gtest_all} AND EXISTS ${draco_gtest_main})) - message(FATAL "googletest must be a sibling directory of ${draco_root}.") + message(FATAL_ERROR "googletest missing, run git submodule update --init") endif() list(APPEND draco_test_defines GTEST_HAS_PTHREAD=0) - draco_add_library(TEST - NAME - draco_gtest - TYPE - STATIC - SOURCES - ${draco_gtest_all} - DEFINES - ${draco_defines} - ${draco_test_defines} - INCLUDES - ${draco_test_include_paths}) + draco_add_library( + TEST + NAME draco_test_common + TYPE STATIC + SOURCES ${draco_test_common_sources} + DEFINES ${draco_defines} ${draco_test_defines} + INCLUDES ${draco_test_include_paths}) - draco_add_library(TEST - NAME - draco_gtest_main - TYPE - STATIC - SOURCES - ${draco_gtest_main} - DEFINES - ${draco_defines} - ${draco_test_defines} - INCLUDES - ${draco_test_include_paths}) + draco_add_library( + TEST + NAME draco_gtest + TYPE STATIC + SOURCES ${draco_gtest_all} + DEFINES ${draco_defines} ${draco_test_defines} + INCLUDES ${draco_test_include_paths}) + + draco_add_library( + TEST + NAME draco_gtest_main + TYPE STATIC + SOURCES ${draco_gtest_main} + DEFINES ${draco_defines} ${draco_test_defines} + INCLUDES ${draco_test_include_paths}) set(DRACO_TEST_DATA_DIR "${draco_root}/testdata") set(DRACO_TEST_TEMP_DIR "${draco_build}/draco_test_temp") + set(DRACO_TEST_ROOT_DIR "${draco_root}") file(MAKE_DIRECTORY "${DRACO_TEST_TEMP_DIR}") # Sets DRACO_TEST_DATA_DIR and DRACO_TEST_TEMP_DIR. @@ -102,32 +148,24 @@ macro(draco_setup_test_targets) "${draco_build}/testing/draco_test_config.h") # Create the test targets. - draco_add_executable(NAME - draco_tests - SOURCES - ${draco_test_sources} - DEFINES - ${draco_defines} - ${draco_test_defines} - INCLUDES - ${draco_test_include_paths} - LIB_DEPS - draco_static - draco_gtest - draco_gtest_main) + draco_add_executable( + TEST + NAME draco_tests + SOURCES ${draco_test_sources} + DEFINES ${draco_defines} ${draco_test_defines} + INCLUDES ${draco_test_include_paths} + LIB_DEPS ${draco_dependency} draco_gtest draco_gtest_main + draco_test_common) + + draco_add_executable( + TEST + NAME draco_factory_tests + SOURCES ${draco_factory_test_sources} + DEFINES ${draco_defines} ${draco_test_defines} + INCLUDES ${draco_test_include_paths} + LIB_DEPS ${draco_dependency} draco_gtest draco_gtest_main + draco_test_common) + - draco_add_executable(NAME - draco_factory_tests - SOURCES - ${draco_factory_test_sources} - DEFINES - ${draco_defines} - ${draco_test_defines} - INCLUDES - ${draco_test_include_paths} - LIB_DEPS - draco_static - draco_gtest - draco_gtest_main) endif() endmacro() diff --git a/contrib/draco/cmake/draco_variables.cmake b/contrib/draco/cmake/draco_variables.cmake index 8dbc77a53..6d1b6a99d 100644 --- a/contrib/draco/cmake/draco_variables.cmake +++ b/contrib/draco/cmake/draco_variables.cmake @@ -1,3 +1,17 @@ +# Copyright 2021 The Draco Authors +# +# 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. + if(DRACO_CMAKE_DRACO_VARIABLES_CMAKE_) return() endif() # DRACO_CMAKE_DRACO_VARIABLES_CMAKE_ @@ -14,8 +28,7 @@ macro(draco_variable_must_be_directory variable_name) if("${${variable_name}}" STREQUAL "") message( - FATAL_ERROR - "Empty variable ${variable_name} is required to build draco.") + FATAL_ERROR "Empty variable ${variable_name} is required to build draco.") endif() if(NOT IS_DIRECTORY "${${variable_name}}") @@ -44,11 +57,13 @@ macro(draco_dump_cmake_flag_variables) list(APPEND flag_variables "CMAKE_CXX_FLAGS_INIT" "CMAKE_CXX_FLAGS" "CMAKE_EXE_LINKER_FLAGS_INIT" "CMAKE_EXE_LINKER_FLAGS") if(CMAKE_BUILD_TYPE) - list(APPEND flag_variables "CMAKE_BUILD_TYPE" - "CMAKE_CXX_FLAGS_${CMAKE_BUILD_TYPE}_INIT" - "CMAKE_CXX_FLAGS_${CMAKE_BUILD_TYPE}" - "CMAKE_EXE_LINKER_FLAGS_${CMAKE_BUILD_TYPE}_INIT" - "CMAKE_EXE_LINKER_FLAGS_${CMAKE_BUILD_TYPE}") + list( + APPEND flag_variables + "CMAKE_BUILD_TYPE" + "CMAKE_CXX_FLAGS_${CMAKE_BUILD_TYPE}_INIT" + "CMAKE_CXX_FLAGS_${CMAKE_BUILD_TYPE}" + "CMAKE_EXE_LINKER_FLAGS_${CMAKE_BUILD_TYPE}_INIT" + "CMAKE_EXE_LINKER_FLAGS_${CMAKE_BUILD_TYPE}") endif() foreach(flag_variable ${flag_variables}) message("${flag_variable}:${${flag_variable}}") diff --git a/contrib/draco/cmake/sanitizers.cmake b/contrib/draco/cmake/sanitizers.cmake deleted file mode 100644 index e720bc045..000000000 --- a/contrib/draco/cmake/sanitizers.cmake +++ /dev/null @@ -1,19 +0,0 @@ -if(DRACO_CMAKE_SANITIZERS_CMAKE_) - return() -endif() -set(DRACO_CMAKE_SANITIZERS_CMAKE_ 1) - -if(MSVC OR NOT SANITIZE) - return() -endif() - -include("${draco_root}/cmake/compiler_flags.cmake") - -string(TOLOWER ${SANITIZE} SANITIZE) - -# Require the sanitizer requested. -require_linker_flag("-fsanitize=${SANITIZE}") -require_compiler_flag("-fsanitize=${SANITIZE}" YES) - -# Make callstacks accurate. -require_compiler_flag("-fno-omit-frame-pointer -fno-optimize-sibling-calls" YES) diff --git a/contrib/draco/cmake/toolchains/aarch64-linux-gnu.cmake b/contrib/draco/cmake/toolchains/aarch64-linux-gnu.cmake index 87e0b4a45..a55da20fa 100644 --- a/contrib/draco/cmake/toolchains/aarch64-linux-gnu.cmake +++ b/contrib/draco/cmake/toolchains/aarch64-linux-gnu.cmake @@ -1,3 +1,17 @@ +# Copyright 2021 The Draco Authors +# +# 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. + if(DRACO_CMAKE_TOOLCHAINS_AARCH64_LINUX_GNU_CMAKE_) return() endif() # DRACO_CMAKE_TOOLCHAINS_AARCH64_LINUX_GNU_CMAKE_ diff --git a/contrib/draco/cmake/toolchains/android-ndk-common.cmake b/contrib/draco/cmake/toolchains/android-ndk-common.cmake index 5126d6e29..80396af48 100644 --- a/contrib/draco/cmake/toolchains/android-ndk-common.cmake +++ b/contrib/draco/cmake/toolchains/android-ndk-common.cmake @@ -1,3 +1,17 @@ +# Copyright 2021 The Draco Authors +# +# 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. + if(DRACO_CMAKE_TOOLCHAINS_ANDROID_NDK_COMMON_CMAKE_) return() endif() diff --git a/contrib/draco/cmake/toolchains/android.cmake b/contrib/draco/cmake/toolchains/android.cmake index b8f576d5e..ba50576b7 100644 --- a/contrib/draco/cmake/toolchains/android.cmake +++ b/contrib/draco/cmake/toolchains/android.cmake @@ -1,3 +1,17 @@ +# Copyright 2021 The Draco Authors +# +# 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. + if(DRACO_CMAKE_TOOLCHAINS_ANDROID_CMAKE_) return() endif() # DRACO_CMAKE_TOOLCHAINS_ANDROID_CMAKE_ @@ -16,9 +30,9 @@ if(NOT ANDROID_ABI) set(ANDROID_ABI arm64-v8a) endif() -# Force arm mode for 32-bit targets (instead of the default thumb) to improve -# performance. -if(NOT ANDROID_ARM_MODE) +# Force arm mode for 32-bit arm targets (instead of the default thumb) to +# improve performance. +if(ANDROID_ABI MATCHES "^armeabi" AND NOT ANDROID_ARM_MODE) set(ANDROID_ARM_MODE arm) endif() diff --git a/contrib/draco/cmake/toolchains/arm-ios-common.cmake b/contrib/draco/cmake/toolchains/arm-ios-common.cmake index 65326d1c2..fab54bb39 100644 --- a/contrib/draco/cmake/toolchains/arm-ios-common.cmake +++ b/contrib/draco/cmake/toolchains/arm-ios-common.cmake @@ -1,3 +1,17 @@ +# Copyright 2021 The Draco Authors +# +# 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. + if(DRACO_CMAKE_TOOLCHAINS_ARM_IOS_COMMON_CMAKE_) return() endif() @@ -13,5 +27,3 @@ set(CMAKE_C_COMPILER clang) set(CMAKE_C_COMPILER_ARG1 "-arch ${CMAKE_SYSTEM_PROCESSOR}") set(CMAKE_CXX_COMPILER clang++) set(CMAKE_CXX_COMPILER_ARG1 "-arch ${CMAKE_SYSTEM_PROCESSOR}") - -# TODO(tomfinegan): Handle bit code embedding. diff --git a/contrib/draco/cmake/toolchains/arm-linux-gnueabihf.cmake b/contrib/draco/cmake/toolchains/arm-linux-gnueabihf.cmake index 6e45969e9..f1f83d67c 100644 --- a/contrib/draco/cmake/toolchains/arm-linux-gnueabihf.cmake +++ b/contrib/draco/cmake/toolchains/arm-linux-gnueabihf.cmake @@ -1,3 +1,17 @@ +# Copyright 2021 The Draco Authors +# +# 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. + if(DRACO_CMAKE_TOOLCHAINS_ARM_LINUX_GNUEABIHF_CMAKE_) return() endif() # DRACO_CMAKE_TOOLCHAINS_ARM_LINUX_GNUEABIHF_CMAKE_ diff --git a/contrib/draco/cmake/toolchains/arm64-android-ndk-libcpp.cmake b/contrib/draco/cmake/toolchains/arm64-android-ndk-libcpp.cmake index 4b6d366f0..80d452f97 100644 --- a/contrib/draco/cmake/toolchains/arm64-android-ndk-libcpp.cmake +++ b/contrib/draco/cmake/toolchains/arm64-android-ndk-libcpp.cmake @@ -1,3 +1,17 @@ +# Copyright 2021 The Draco Authors +# +# 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. + if(DRACO_CMAKE_TOOLCHAINS_ARM64_ANDROID_NDK_LIBCPP_CMAKE_) return() endif() diff --git a/contrib/draco/cmake/toolchains/arm64-ios.cmake b/contrib/draco/cmake/toolchains/arm64-ios.cmake index c4ec7e3fa..5365d70f1 100644 --- a/contrib/draco/cmake/toolchains/arm64-ios.cmake +++ b/contrib/draco/cmake/toolchains/arm64-ios.cmake @@ -1,10 +1,23 @@ +# Copyright 2021 The Draco Authors +# +# 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. + if(DRACO_CMAKE_TOOLCHAINS_ARM64_IOS_CMAKE_) return() endif() set(DRACO_CMAKE_TOOLCHAINS_ARM64_IOS_CMAKE_ 1) if(XCODE) - # TODO(tomfinegan): Handle arm builds in Xcode. message(FATAL_ERROR "This toolchain does not support Xcode.") endif() diff --git a/contrib/draco/cmake/toolchains/arm64-linux-gcc.cmake b/contrib/draco/cmake/toolchains/arm64-linux-gcc.cmake index 046ff0139..a332760b2 100644 --- a/contrib/draco/cmake/toolchains/arm64-linux-gcc.cmake +++ b/contrib/draco/cmake/toolchains/arm64-linux-gcc.cmake @@ -1,3 +1,17 @@ +# Copyright 2021 The Draco Authors +# +# 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. + if(DRACO_CMAKE_TOOLCHAINS_ARM64_LINUX_GCC_CMAKE_) return() endif() diff --git a/contrib/draco/cmake/toolchains/armv7-android-ndk-libcpp.cmake b/contrib/draco/cmake/toolchains/armv7-android-ndk-libcpp.cmake index 80ee98b18..bedcc0cad 100644 --- a/contrib/draco/cmake/toolchains/armv7-android-ndk-libcpp.cmake +++ b/contrib/draco/cmake/toolchains/armv7-android-ndk-libcpp.cmake @@ -1,3 +1,17 @@ +# Copyright 2021 The Draco Authors +# +# 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. + if(DRACO_CMAKE_TOOLCHAINS_ARMV7_ANDROID_NDK_LIBCPP_CMAKE_) return() endif() diff --git a/contrib/draco/cmake/toolchains/armv7-ios.cmake b/contrib/draco/cmake/toolchains/armv7-ios.cmake index 8ddd6997b..43e208b1f 100644 --- a/contrib/draco/cmake/toolchains/armv7-ios.cmake +++ b/contrib/draco/cmake/toolchains/armv7-ios.cmake @@ -1,10 +1,23 @@ +# Copyright 2021 The Draco Authors +# +# 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. + if(DRACO_CMAKE_TOOLCHAINS_ARMV7_IOS_CMAKE_) return() endif() set(DRACO_CMAKE_TOOLCHAINS_ARMV7_IOS_CMAKE_ 1) if(XCODE) - # TODO(tomfinegan): Handle arm builds in Xcode. message(FATAL_ERROR "This toolchain does not support Xcode.") endif() diff --git a/contrib/draco/cmake/toolchains/armv7-linux-gcc.cmake b/contrib/draco/cmake/toolchains/armv7-linux-gcc.cmake index 9c9472319..730a87f4b 100644 --- a/contrib/draco/cmake/toolchains/armv7-linux-gcc.cmake +++ b/contrib/draco/cmake/toolchains/armv7-linux-gcc.cmake @@ -1,3 +1,17 @@ +# Copyright 2021 The Draco Authors +# +# 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. + if(DRACO_CMAKE_TOOLCHAINS_ARMV7_LINUX_GCC_CMAKE_) return() endif() diff --git a/contrib/draco/cmake/toolchains/armv7s-ios.cmake b/contrib/draco/cmake/toolchains/armv7s-ios.cmake index b433025ba..472756117 100644 --- a/contrib/draco/cmake/toolchains/armv7s-ios.cmake +++ b/contrib/draco/cmake/toolchains/armv7s-ios.cmake @@ -1,10 +1,23 @@ +# Copyright 2021 The Draco Authors +# +# 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. + if(DRACO_CMAKE_TOOLCHAINS_ARMV7S_IOS_CMAKE_) return() endif() set(DRACO_CMAKE_TOOLCHAINS_ARMV7S_IOS_CMAKE_ 1) if(XCODE) - # TODO(tomfinegan): Handle arm builds in Xcode. message(FATAL_ERROR "This toolchain does not support Xcode.") endif() diff --git a/contrib/draco/cmake/toolchains/i386-ios.cmake b/contrib/draco/cmake/toolchains/i386-ios.cmake index e9a105591..38989d225 100644 --- a/contrib/draco/cmake/toolchains/i386-ios.cmake +++ b/contrib/draco/cmake/toolchains/i386-ios.cmake @@ -1,10 +1,23 @@ +# Copyright 2021 The Draco Authors +# +# 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. + if(DRACO_CMAKE_TOOLCHAINS_i386_IOS_CMAKE_) return() endif() set(DRACO_CMAKE_TOOLCHAINS_i386_IOS_CMAKE_ 1) if(XCODE) - # TODO(tomfinegan): Handle arm builds in Xcode. message(FATAL_ERROR "This toolchain does not support Xcode.") endif() diff --git a/contrib/draco/cmake/toolchains/x86-android-ndk-libcpp.cmake b/contrib/draco/cmake/toolchains/x86-android-ndk-libcpp.cmake index d43383640..6f63f2c31 100644 --- a/contrib/draco/cmake/toolchains/x86-android-ndk-libcpp.cmake +++ b/contrib/draco/cmake/toolchains/x86-android-ndk-libcpp.cmake @@ -1,3 +1,17 @@ +# Copyright 2021 The Draco Authors +# +# 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. + if(DRACO_CMAKE_TOOLCHAINS_X86_ANDROID_NDK_LIBCPP_CMAKE_) return() endif() diff --git a/contrib/draco/cmake/toolchains/x86_64-android-ndk-libcpp.cmake b/contrib/draco/cmake/toolchains/x86_64-android-ndk-libcpp.cmake index d6fabeacc..7a630f4d4 100644 --- a/contrib/draco/cmake/toolchains/x86_64-android-ndk-libcpp.cmake +++ b/contrib/draco/cmake/toolchains/x86_64-android-ndk-libcpp.cmake @@ -1,3 +1,17 @@ +# Copyright 2021 The Draco Authors +# +# 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. + if(DRACO_CMAKE_TOOLCHAINS_X86_64_ANDROID_NDK_LIBCPP_CMAKE_) return() endif() diff --git a/contrib/draco/cmake/toolchains/x86_64-ios.cmake b/contrib/draco/cmake/toolchains/x86_64-ios.cmake index 4c50a72a2..6946ce410 100644 --- a/contrib/draco/cmake/toolchains/x86_64-ios.cmake +++ b/contrib/draco/cmake/toolchains/x86_64-ios.cmake @@ -1,10 +1,23 @@ +# Copyright 2021 The Draco Authors +# +# 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. + if(DRACO_CMAKE_TOOLCHAINS_X86_64_IOS_CMAKE_) return() endif() set(DRACO_CMAKE_TOOLCHAINS_X86_64_IOS_CMAKE_ 1) if(XCODE) - # TODO(tomfinegan): Handle arm builds in Xcode. message(FATAL_ERROR "This toolchain does not support Xcode.") endif() diff --git a/contrib/draco/cmake/util.cmake b/contrib/draco/cmake/util.cmake deleted file mode 100644 index 813146a62..000000000 --- a/contrib/draco/cmake/util.cmake +++ /dev/null @@ -1,79 +0,0 @@ -if(DRACO_CMAKE_UTIL_CMAKE_) - return() -endif() -set(DRACO_CMAKE_UTIL_CMAKE_ 1) - -# Creates dummy source file in $draco_build_dir named $basename.$extension and -# returns the full path to the dummy source file via the $out_file_path -# parameter. -function(create_dummy_source_file basename extension out_file_path) - set(dummy_source_file "${draco_build_dir}/${basename}.${extension}") - file(WRITE "${dummy_source_file}.new" - "// Generated file. DO NOT EDIT!\n" - "// ${target_name} needs a ${extension} file to force link language, \n" - "// or to silence a harmless CMake warning: Ignore me.\n" - "void ${target_name}_dummy_function(void) {}\n") - - # Will replace ${dummy_source_file} only if the file content has changed. - # This prevents forced Draco rebuilds after CMake runs. - configure_file("${dummy_source_file}.new" "${dummy_source_file}") - file(REMOVE "${dummy_source_file}.new") - - set(${out_file_path} ${dummy_source_file} PARENT_SCOPE) -endfunction() - -# Convenience function for adding a dummy source file to $target_name using -# $extension as the file extension. Wraps create_dummy_source_file(). -function(add_dummy_source_file_to_target target_name extension) - create_dummy_source_file("${target_name}" "${extension}" "dummy_source_file") - target_sources(${target_name} PRIVATE ${dummy_source_file}) -endfunction() - -# Extracts the version number from $version_file and returns it to the user via -# $version_string_out_var. This is achieved by finding the first instance of the -# kDracoVersion variable and then removing everything but the string literal -# assigned to the variable. Quotes and semicolon are stripped from the returned -# string. -function(extract_version_string version_file version_string_out_var) - file(STRINGS "${version_file}" draco_version REGEX "kDracoVersion") - list(GET draco_version 0 draco_version) - string(REPLACE "static const char kDracoVersion[] = " "" draco_version - "${draco_version}") - string(REPLACE ";" "" draco_version "${draco_version}") - string(REPLACE "\"" "" draco_version "${draco_version}") - set("${version_string_out_var}" "${draco_version}" PARENT_SCOPE) -endfunction() - -# Sets CMake compiler launcher to $launcher_name when $launcher_name is found in -# $PATH. Warns user about ignoring build flag $launcher_flag when $launcher_name -# is not found in $PATH. -function(set_compiler_launcher launcher_flag launcher_name) - find_program(launcher_path "${launcher_name}") - if(launcher_path) - set(CMAKE_C_COMPILER_LAUNCHER "${launcher_path}" PARENT_SCOPE) - set(CMAKE_CXX_COMPILER_LAUNCHER "${launcher_path}" PARENT_SCOPE) - message("--- Using ${launcher_name} as compiler launcher.") - else() - message( - WARNING "--- Cannot find ${launcher_name}, ${launcher_flag} ignored.") - endif() -endfunction() - -# Terminates CMake execution when $var_name is unset in the environment. Sets -# CMake variable to the value of the environment variable when the variable is -# present in the environment. -macro(require_variable var_name) - if("$ENV{${var_name}}" STREQUAL "") - message(FATAL_ERROR "${var_name} must be set in environment.") - endif() - set_variable_if_unset(${var_name} "") -endmacro() - -# Sets $var_name to $default_value if not already set. -macro(set_variable_if_unset var_name default_value) - if(NOT "$ENV{${var_name}}" STREQUAL "") - set(${var_name} $ENV{${var_name}}) - elseif(NOT ${var_name}) - set(${var_name} ${default_value}) - endif() -endmacro() diff --git a/contrib/draco/src/draco/animation/animation.cc b/contrib/draco/src/draco/animation/animation.cc new file mode 100644 index 000000000..471cf2942 --- /dev/null +++ b/contrib/draco/src/draco/animation/animation.cc @@ -0,0 +1,47 @@ +// Copyright 2019 The Draco Authors. +// +// 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 "draco/animation/animation.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED + +namespace draco { + +void Animation::Copy(const Animation &src) { + name_ = src.name_; + channels_.clear(); + for (int i = 0; i < src.NumChannels(); ++i) { + std::unique_ptr new_channel(new AnimationChannel()); + new_channel->Copy(*src.GetChannel(i)); + channels_.push_back(std::move(new_channel)); + } + + samplers_.clear(); + for (int i = 0; i < src.NumSamplers(); ++i) { + std::unique_ptr new_sampler(new AnimationSampler()); + new_sampler->Copy(*src.GetSampler(i)); + samplers_.push_back(std::move(new_sampler)); + } + + node_animation_data_.clear(); + for (int i = 0; i < src.NumNodeAnimationData(); ++i) { + std::unique_ptr new_data(new NodeAnimationData()); + new_data->Copy(*src.GetNodeAnimationData(i)); + node_animation_data_.push_back(std::move(new_data)); + } +} + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED diff --git a/contrib/draco/src/draco/animation/animation.h b/contrib/draco/src/draco/animation/animation.h new file mode 100644 index 000000000..3713f9886 --- /dev/null +++ b/contrib/draco/src/draco/animation/animation.h @@ -0,0 +1,149 @@ +// Copyright 2019 The Draco Authors. +// +// 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 DRACO_ANIMATION_ANIMATION_H_ +#define DRACO_ANIMATION_ANIMATION_H_ + +#include "draco/draco_features.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED +#include +#include + +#include "draco/animation/node_animation_data.h" +#include "draco/core/status.h" + +namespace draco { + +// Struct to hold information about an animation's sampler. +struct AnimationSampler { + enum class SamplerInterpolation { LINEAR, STEP, CUBICSPLINE }; + + static std::string InterpolationToString(SamplerInterpolation value) { + switch (value) { + case SamplerInterpolation::STEP: + return "STEP"; + case SamplerInterpolation::CUBICSPLINE: + return "CUBICSPLINE"; + default: + return "LINEAR"; + } + } + + AnimationSampler() + : input_index(-1), + interpolation_type(SamplerInterpolation::LINEAR), + output_index(-1) {} + + void Copy(const AnimationSampler &src) { + input_index = src.input_index; + interpolation_type = src.interpolation_type; + output_index = src.output_index; + } + + int input_index; + SamplerInterpolation interpolation_type; + int output_index; +}; + +// Struct to hold information about an animation's channel. +struct AnimationChannel { + enum class ChannelTransformation { TRANSLATION, ROTATION, SCALE, WEIGHTS }; + + static std::string TransformationToString(ChannelTransformation value) { + switch (value) { + case ChannelTransformation::ROTATION: + return "rotation"; + case ChannelTransformation::SCALE: + return "scale"; + case ChannelTransformation::WEIGHTS: + return "weights"; + default: + return "translation"; + } + } + + AnimationChannel() + : target_index(-1), + transformation_type(ChannelTransformation::TRANSLATION), + sampler_index(-1) {} + + void Copy(const AnimationChannel &src) { + target_index = src.target_index; + transformation_type = src.transformation_type; + sampler_index = src.sampler_index; + } + + int target_index; + ChannelTransformation transformation_type; + int sampler_index; +}; + +// This class is used to hold data and information of glTF animations. +class Animation { + public: + Animation() {} + + void Copy(const Animation &src); + + const std::string &GetName() const { return name_; } + void SetName(const std::string &name) { name_ = name; } + + // Returns the number of channels in an animation. + int NumChannels() const { return channels_.size(); } + // Returns the number of samplers in an animation. + int NumSamplers() const { return samplers_.size(); } + // Returns the number of accessors in an animation. + int NumNodeAnimationData() const { return node_animation_data_.size(); } + + // Returns a channel in the animation. + AnimationChannel *GetChannel(int index) { return channels_[index].get(); } + const AnimationChannel *GetChannel(int index) const { + return channels_[index].get(); + } + // Returns a sampler in the animation. + AnimationSampler *GetSampler(int index) { return samplers_[index].get(); } + const AnimationSampler *GetSampler(int index) const { + return samplers_[index].get(); + } + // Returns an accessor in the animation. + NodeAnimationData *GetNodeAnimationData(int index) { + return node_animation_data_[index].get(); + } + const NodeAnimationData *GetNodeAnimationData(int index) const { + return node_animation_data_[index].get(); + } + + void AddNodeAnimationData( + std::unique_ptr node_animation_data) { + node_animation_data_.push_back(std::move(node_animation_data)); + } + void AddSampler(std::unique_ptr sampler) { + samplers_.push_back(std::move(sampler)); + } + void AddChannel(std::unique_ptr channel) { + channels_.push_back(std::move(channel)); + } + + private: + std::string name_; + std::vector> samplers_; + std::vector> channels_; + std::vector> node_animation_data_; +}; + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED +#endif // DRACO_ANIMATION_ANIMATION_H_ diff --git a/contrib/draco/src/draco/animation/animation_test.cc b/contrib/draco/src/draco/animation/animation_test.cc new file mode 100644 index 000000000..473938bca --- /dev/null +++ b/contrib/draco/src/draco/animation/animation_test.cc @@ -0,0 +1,71 @@ +// Copyright 2021 The Draco Authors. +// +// 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 "draco/animation/animation.h" + +#include "draco/core/draco_test_base.h" +#include "draco/draco_features.h" + +namespace { + +#ifdef DRACO_TRANSCODER_SUPPORTED +TEST(AnimationTest, TestCopy) { + // Test copying of animation data. + draco::Animation src_anim; + ASSERT_TRUE(src_anim.GetName().empty()); + src_anim.SetName("Walking"); + ASSERT_EQ(src_anim.GetName(), "Walking"); + + std::unique_ptr src_sampler_0( + new draco::AnimationSampler()); + src_sampler_0->interpolation_type = + draco::AnimationSampler::SamplerInterpolation::CUBICSPLINE; + std::unique_ptr src_sampler_1( + new draco::AnimationSampler()); + src_sampler_1->Copy(*src_sampler_0); + + ASSERT_EQ(src_sampler_0->interpolation_type, + src_sampler_1->interpolation_type); + + src_sampler_1->interpolation_type = + draco::AnimationSampler::SamplerInterpolation::STEP; + + src_anim.AddSampler(std::move(src_sampler_0)); + src_anim.AddSampler(std::move(src_sampler_1)); + ASSERT_EQ(src_anim.NumSamplers(), 2); + + std::unique_ptr src_channel( + new draco::AnimationChannel()); + src_channel->transformation_type = + draco::AnimationChannel::ChannelTransformation::WEIGHTS; + src_anim.AddChannel(std::move(src_channel)); + ASSERT_EQ(src_anim.NumChannels(), 1); + + draco::Animation dst_anim; + dst_anim.Copy(src_anim); + + ASSERT_EQ(dst_anim.GetName(), src_anim.GetName()); + ASSERT_EQ(dst_anim.NumSamplers(), 2); + ASSERT_EQ(dst_anim.NumChannels(), 1); + + ASSERT_EQ(dst_anim.GetSampler(0)->interpolation_type, + src_anim.GetSampler(0)->interpolation_type); + ASSERT_EQ(dst_anim.GetSampler(1)->interpolation_type, + src_anim.GetSampler(1)->interpolation_type); + ASSERT_EQ(dst_anim.GetChannel(0)->transformation_type, + src_anim.GetChannel(0)->transformation_type); +} +#endif // DRACO_TRANSCODER_SUPPORTED + +} // namespace diff --git a/contrib/draco/src/draco/animation/keyframe_animation_encoding_test.cc b/contrib/draco/src/draco/animation/keyframe_animation_encoding_test.cc index 4a6491f9d..fcd0eaa6f 100644 --- a/contrib/draco/src/draco/animation/keyframe_animation_encoding_test.cc +++ b/contrib/draco/src/draco/animation/keyframe_animation_encoding_test.cc @@ -26,8 +26,9 @@ class KeyframeAnimationEncodingTest : public ::testing::Test { bool CreateAndAddTimestamps(int32_t num_frames) { timestamps_.resize(num_frames); - for (int i = 0; i < timestamps_.size(); ++i) + for (int i = 0; i < timestamps_.size(); ++i) { timestamps_[i] = static_cast(i); + } return keyframe_animation_.SetTimestamps(timestamps_); } @@ -35,8 +36,9 @@ class KeyframeAnimationEncodingTest : public ::testing::Test { uint32_t num_components) { // Create and add animation data with. animation_data_.resize(num_frames * num_components); - for (int i = 0; i < animation_data_.size(); ++i) + for (int i = 0; i < animation_data_.size(); ++i) { animation_data_[i] = static_cast(i); + } return keyframe_animation_.AddKeyframes(draco::DT_FLOAT32, num_components, animation_data_); } @@ -49,7 +51,7 @@ class KeyframeAnimationEncodingTest : public ::testing::Test { ASSERT_EQ(animation0.num_animations(), animation1.num_animations()); if (quantized) { - // TODO(hemmer) : Add test for stable quantization. + // TODO(b/199760123) : Add test for stable quantization. // Quantization will result in slightly different values. // Skip comparing values. return; @@ -109,9 +111,8 @@ class KeyframeAnimationEncodingTest : public ::testing::Test { } } - ASSERT_TRUE( - encoder.EncodeKeyframeAnimation(keyframe_animation_, options, &buffer) - .ok()); + DRACO_ASSERT_OK( + encoder.EncodeKeyframeAnimation(keyframe_animation_, options, &buffer)); draco::DecoderBuffer dec_decoder; draco::KeyframeAnimationDecoder decoder; @@ -122,8 +123,8 @@ class KeyframeAnimationEncodingTest : public ::testing::Test { std::unique_ptr decoded_animation( new KeyframeAnimation()); DecoderOptions dec_options; - ASSERT_TRUE( - decoder.Decode(dec_options, &dec_buffer, decoded_animation.get()).ok()); + DRACO_ASSERT_OK( + decoder.Decode(dec_options, &dec_buffer, decoded_animation.get())); // Verify if animation before and after compression is identical. CompareAnimationData(keyframe_animation_, diff --git a/contrib/draco/src/draco/animation/keyframe_animation_test.cc b/contrib/draco/src/draco/animation/keyframe_animation_test.cc index bc92b25ff..94566972b 100644 --- a/contrib/draco/src/draco/animation/keyframe_animation_test.cc +++ b/contrib/draco/src/draco/animation/keyframe_animation_test.cc @@ -24,8 +24,9 @@ class KeyframeAnimationTest : public ::testing::Test { bool CreateAndAddTimestamps(int32_t num_frames) { timestamps_.resize(num_frames); - for (int i = 0; i < timestamps_.size(); ++i) + for (int i = 0; i < timestamps_.size(); ++i) { timestamps_[i] = static_cast(i); + } return keyframe_animation_.SetTimestamps(timestamps_); } @@ -33,8 +34,9 @@ class KeyframeAnimationTest : public ::testing::Test { uint32_t num_components) { // Create and add animation data with. animation_data_.resize(num_frames * num_components); - for (int i = 0; i < animation_data_.size(); ++i) + for (int i = 0; i < animation_data_.size(); ++i) { animation_data_[i] = static_cast(i); + } return keyframe_animation_.AddKeyframes(draco::DT_FLOAT32, num_components, animation_data_); } diff --git a/contrib/draco/src/draco/animation/node_animation_data.h b/contrib/draco/src/draco/animation/node_animation_data.h new file mode 100644 index 000000000..7799e3376 --- /dev/null +++ b/contrib/draco/src/draco/animation/node_animation_data.h @@ -0,0 +1,150 @@ +// Copyright 2019 The Draco Authors. +// +// 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 DRACO_ANIMATION_NODE_ANIMATION_DATA_H_ +#define DRACO_ANIMATION_NODE_ANIMATION_DATA_H_ + +#include "draco/draco_features.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED + +#include "draco/core/hash_utils.h" +#include "draco/core/status.h" +#include "draco/core/status_or.h" + +namespace draco { + +// This class is used to store information and data for animations that only +// affect the nodes. +// TODO(fgalligan): Think about changing the name of this class now that Skin +// is using it. +class NodeAnimationData { + public: + enum class Type { SCALAR, VEC3, VEC4, MAT4 }; + + NodeAnimationData() : type_(Type::SCALAR), count_(0), normalized_(false) {} + + void Copy(const NodeAnimationData &src) { + type_ = src.type_; + count_ = src.count_; + normalized_ = src.normalized_; + data_ = src.data_; + } + + Type type() const { return type_; } + int count() const { return count_; } + bool normalized() const { return normalized_; } + + std::vector *GetMutableData() { return &data_; } + const std::vector *GetData() const { return &data_; } + + void SetType(Type type) { type_ = type; } + void SetCount(int count) { count_ = count; } + void SetNormalized(bool normalized) { normalized_ = normalized; } + + int ComponentSize() const { return sizeof(float); } + int NumComponents() const { + switch (type_) { + case Type::SCALAR: + return 1; + case Type::VEC3: + return 3; + case Type::MAT4: + return 16; + default: + return 4; + } + } + + std::string TypeAsString() const { + switch (type_) { + case Type::SCALAR: + return "SCALAR"; + case Type::VEC3: + return "VEC3"; + case Type::MAT4: + return "MAT4"; + default: + return "VEC4"; + } + } + + bool operator==(const NodeAnimationData &nad) const { + return type_ == nad.type_ && count_ == nad.count_ && + normalized_ == nad.normalized_ && data_ == nad.data_; + } + + private: + Type type_; + int count_; + bool normalized_; + std::vector data_; +}; + +// Wrapper class for hashing NodeAnimationData. When using different containers, +// this class is preferable instead of copying the data in NodeAnimationData +// every time. +class NodeAnimationDataHash { + public: + NodeAnimationDataHash() = delete; + NodeAnimationDataHash &operator=(const NodeAnimationDataHash &) = delete; + NodeAnimationDataHash(NodeAnimationDataHash &&) = delete; + NodeAnimationDataHash &operator=(NodeAnimationDataHash &&) = delete; + + explicit NodeAnimationDataHash(const NodeAnimationData *nad) + : node_animation_data_(nad) { + hash_ = NodeAnimationDataHash::HashNodeAnimationData(*node_animation_data_); + } + + NodeAnimationDataHash(const NodeAnimationDataHash &nadh) { + node_animation_data_ = nadh.node_animation_data_; + hash_ = nadh.hash_; + } + + bool operator==(const NodeAnimationDataHash &nadh) const { + return *node_animation_data_ == *nadh.node_animation_data_; + } + + struct Hash { + size_t operator()(const NodeAnimationDataHash &nadh) const { + return nadh.hash_; + } + }; + + const NodeAnimationData *GetNodeAnimationData() { + return node_animation_data_; + } + + private: + // Returns a hash of |nad|. + static size_t HashNodeAnimationData(const NodeAnimationData &nad) { + size_t hash = 79; // Magic number. + hash = HashCombine(static_cast(nad.type()), hash); + hash = HashCombine(nad.count(), hash); + hash = HashCombine(nad.normalized(), hash); + const uint64_t data_hash = + FingerprintString(reinterpret_cast(nad.GetData()->data()), + nad.GetData()->size() * sizeof(float)); + hash = HashCombine(data_hash, hash); + return hash; + } + + const NodeAnimationData *node_animation_data_; + size_t hash_; +}; + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED +#endif // DRACO_ANIMATION_NODE_ANIMATION_DATA_H_ diff --git a/contrib/draco/src/draco/animation/skin.cc b/contrib/draco/src/draco/animation/skin.cc new file mode 100644 index 000000000..f232978c2 --- /dev/null +++ b/contrib/draco/src/draco/animation/skin.cc @@ -0,0 +1,29 @@ +// Copyright 2019 The Draco Authors. +// +// 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 "draco/animation/skin.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED + +namespace draco { + +void Skin::Copy(const Skin &s) { + inverse_bind_matrices_.Copy(s.GetInverseBindMatrices()); + joints_ = s.GetJoints(); + joint_root_index_ = s.GetJointRoot(); +} + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED diff --git a/contrib/draco/src/draco/animation/skin.h b/contrib/draco/src/draco/animation/skin.h new file mode 100644 index 000000000..81ca997eb --- /dev/null +++ b/contrib/draco/src/draco/animation/skin.h @@ -0,0 +1,64 @@ +// Copyright 2019 The Draco Authors. +// +// 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 DRACO_ANIMATION_SKIN_H_ +#define DRACO_ANIMATION_SKIN_H_ + +#include "draco/draco_features.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED + +#include + +#include "draco/animation/node_animation_data.h" +#include "draco/scene/scene_indices.h" + +namespace draco { + +// This class is used to store information on animation skins. +class Skin { + public: + Skin() : joint_root_index_(-1) {} + + void Copy(const Skin &s); + + NodeAnimationData &GetInverseBindMatrices() { return inverse_bind_matrices_; } + const NodeAnimationData &GetInverseBindMatrices() const { + return inverse_bind_matrices_; + } + + int AddJoint(SceneNodeIndex index) { + joints_.push_back(index); + return joints_.size() - 1; + } + int NumJoints() const { return joints_.size(); } + SceneNodeIndex GetJoint(int index) const { return joints_[index]; } + SceneNodeIndex &GetJoint(int index) { return joints_[index]; } + const std::vector &GetJoints() const { return joints_; } + + void SetJointRoot(SceneNodeIndex index) { joint_root_index_ = index; } + SceneNodeIndex GetJointRoot() const { return joint_root_index_; } + + private: + NodeAnimationData inverse_bind_matrices_; + + // List of node indices that make up the joint hierarchy. + std::vector joints_; + SceneNodeIndex joint_root_index_; +}; + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED +#endif // DRACO_ANIMATION_SKIN_H_ diff --git a/contrib/draco/src/draco/attributes/attribute_transform.cc b/contrib/draco/src/draco/attributes/attribute_transform.cc index 174e6b822..fb2ed1829 100644 --- a/contrib/draco/src/draco/attributes/attribute_transform.cc +++ b/contrib/draco/src/draco/attributes/attribute_transform.cc @@ -28,12 +28,13 @@ std::unique_ptr AttributeTransform::InitTransformedAttribute( const PointAttribute &src_attribute, int num_entries) { const int num_components = GetTransformedNumComponents(src_attribute); const DataType dt = GetTransformedDataType(src_attribute); - GeometryAttribute va; - va.Init(src_attribute.attribute_type(), nullptr, num_components, dt, false, + GeometryAttribute ga; + ga.Init(src_attribute.attribute_type(), nullptr, num_components, dt, false, num_components * DataTypeLength(dt), 0); - std::unique_ptr transformed_attribute(new PointAttribute(va)); + std::unique_ptr transformed_attribute(new PointAttribute(ga)); transformed_attribute->Reset(num_entries); transformed_attribute->SetIdentityMapping(); + transformed_attribute->set_unique_id(src_attribute.unique_id()); return transformed_attribute; } diff --git a/contrib/draco/src/draco/attributes/geometry_attribute.cc b/contrib/draco/src/draco/attributes/geometry_attribute.cc index b62478426..141130f43 100644 --- a/contrib/draco/src/draco/attributes/geometry_attribute.cc +++ b/contrib/draco/src/draco/attributes/geometry_attribute.cc @@ -26,7 +26,7 @@ GeometryAttribute::GeometryAttribute() unique_id_(0) {} void GeometryAttribute::Init(GeometryAttribute::Type attribute_type, - DataBuffer *buffer, int8_t num_components, + DataBuffer *buffer, uint8_t num_components, DataType data_type, bool normalized, int64_t byte_stride, int64_t byte_offset) { buffer_ = buffer; diff --git a/contrib/draco/src/draco/attributes/geometry_attribute.h b/contrib/draco/src/draco/attributes/geometry_attribute.h index f4d099b1b..28f743fa0 100644 --- a/contrib/draco/src/draco/attributes/geometry_attribute.h +++ b/contrib/draco/src/draco/attributes/geometry_attribute.h @@ -15,12 +15,18 @@ #ifndef DRACO_ATTRIBUTES_GEOMETRY_ATTRIBUTE_H_ #define DRACO_ATTRIBUTES_GEOMETRY_ATTRIBUTE_H_ +#include #include +#include #include #include "draco/attributes/geometry_indices.h" #include "draco/core/data_buffer.h" #include "draco/core/hash_utils.h" +#include "draco/draco_features.h" +#ifdef DRACO_TRANSCODER_SUPPORTED +#include "draco/core/status.h" +#endif namespace draco { @@ -51,6 +57,16 @@ class GeometryAttribute { // predefined use case. Such attributes are often used for a shader specific // data. GENERIC, +#ifdef DRACO_TRANSCODER_SUPPORTED + // TODO(ostava): Adding a new attribute would be bit-stream change for GLTF. + // Older decoders wouldn't know what to do with this attribute type. This + // should be open-sourced only when we are ready to increase our bit-stream + // version. + TANGENT, + MATERIAL, + JOINTS, + WEIGHTS, +#endif // Total number of different attribute types. // Always keep behind all named attributes. NAMED_ATTRIBUTES_COUNT, @@ -58,7 +74,7 @@ class GeometryAttribute { GeometryAttribute(); // Initializes and enables the attribute. - void Init(Type attribute_type, DataBuffer *buffer, int8_t num_components, + void Init(Type attribute_type, DataBuffer *buffer, uint8_t num_components, DataType data_type, bool normalized, int64_t byte_stride, int64_t byte_offset); bool IsValid() const { return buffer_ != nullptr; } @@ -129,6 +145,17 @@ class GeometryAttribute { buffer_->Write(byte_pos, value, byte_stride()); } +#ifdef DRACO_TRANSCODER_SUPPORTED + // Sets a value of an attribute entry. The input |value| must have + // |input_num_components| entries and it will be automatically converted to + // the internal format used by the geometry attribute. If the conversion is + // not possible, an error status will be returned. + template + Status ConvertAndSetAttributeValue(AttributeValueIndex avi, + int input_num_components, + const InputT *value); +#endif + // DEPRECATED: Use // ConvertValue(AttributeValueIndex att_id, // int out_num_components, @@ -233,10 +260,11 @@ class GeometryAttribute { // Returns the number of components that are stored for each entry. // For position attribute this is usually three (x,y,z), // while texture coordinates have two components (u,v). - int8_t num_components() const { return num_components_; } + uint8_t num_components() const { return num_components_; } // Indicates whether the data type should be normalized before interpretation, // that is, it should be divided by the max value of the data type. bool normalized() const { return normalized_; } + void set_normalized(bool normalized) { normalized_ = normalized; } // The buffer storing the entire data of the attribute. const DataBuffer *buffer() const { return buffer_; } // Returns the number of bytes between two attribute entries, this is, at @@ -260,7 +288,7 @@ class GeometryAttribute { // T is the stored attribute data type. // OutT is the desired data type of the attribute. template - bool ConvertTypedValue(AttributeValueIndex att_id, int8_t out_num_components, + bool ConvertTypedValue(AttributeValueIndex att_id, uint8_t out_num_components, OutT *out_value) const { const uint8_t *src_address = GetAddress(att_id); @@ -270,29 +298,10 @@ class GeometryAttribute { return false; } const T in_value = *reinterpret_cast(src_address); - - // Make sure the in_value fits within the range of values that OutT - // is able to represent. Perform the check only for integral types. - if (std::is_integral::value && std::is_integral::value) { - static constexpr OutT kOutMin = - std::is_signed::value ? std::numeric_limits::lowest() : 0; - if (in_value < kOutMin || in_value > std::numeric_limits::max()) { - return false; - } + if (!ConvertComponentValue(in_value, normalized_, + out_value + i)) { + return false; } - - out_value[i] = static_cast(in_value); - // When converting integer to floating point, normalize the value if - // necessary. - if (std::is_integral::value && std::is_floating_point::value && - normalized_) { - out_value[i] /= static_cast(std::numeric_limits::max()); - } - // TODO(ostava): Add handling of normalized attributes when converting - // between different integer representations. If the attribute is - // normalized, integer values should be converted as if they represent 0-1 - // range. E.g. when we convert uint16 to uint8, the range <0, 2^16 - 1> - // should be converted to range <0, 2^8 - 1>. src_address += sizeof(T); } // Fill empty data for unused output components if needed. @@ -302,12 +311,128 @@ class GeometryAttribute { return true; } +#ifdef DRACO_TRANSCODER_SUPPORTED + // Function that converts input |value| from type T to the internal attribute + // representation defined by OutT and |num_components_|. + template + Status ConvertAndSetAttributeTypedValue(AttributeValueIndex avi, + int8_t input_num_components, + const T *value) { + uint8_t *address = GetAddress(avi); + + // Convert all components available in both the original and output formats. + for (int i = 0; i < num_components_; ++i) { + if (!IsAddressValid(address)) { + return ErrorStatus("GeometryAttribute: Invalid address."); + } + OutT *const out_value = reinterpret_cast(address); + if (i < input_num_components) { + if (!ConvertComponentValue(*(value + i), normalized_, + out_value)) { + return ErrorStatus( + "GeometryAttribute: Failed to convert component value."); + } + } else { + *out_value = static_cast(0); + } + address += sizeof(OutT); + } + return OkStatus(); + } +#endif // DRACO_TRANSCODER_SUPPORTED + + // Converts |in_value| of type T into |out_value| of type OutT. If + // |normalized| is true, any conversion between floating point and integer + // values will be treating integers as normalized types (the entire integer + // range will be used to represent 0-1 floating point range). + template + static bool ConvertComponentValue(const T &in_value, bool normalized, + OutT *out_value) { + // Make sure the |in_value| can be represented as an integral type OutT. + if (std::is_integral::value) { + // Make sure the |in_value| fits within the range of values that OutT + // is able to represent. Perform the check only for integral types. + if (!std::is_same::value && std::is_integral::value) { + static constexpr OutT kOutMin = + std::is_signed::value ? std::numeric_limits::min() : 0; + if (in_value < kOutMin || in_value > std::numeric_limits::max()) { + return false; + } + } + + // Check conversion of floating point |in_value| to integral value OutT. + if (std::is_floating_point::value) { + // Make sure the floating point |in_value| is not NaN and not Inf as + // integral type OutT is unable to represent these values. + if (sizeof(in_value) > sizeof(double)) { + if (std::isnan(static_cast(in_value)) || + std::isinf(static_cast(in_value))) { + return false; + } + } else if (sizeof(in_value) > sizeof(float)) { + if (std::isnan(static_cast(in_value)) || + std::isinf(static_cast(in_value))) { + return false; + } + } else { + if (std::isnan(static_cast(in_value)) || + std::isinf(static_cast(in_value))) { + return false; + } + } + + // Make sure the floating point |in_value| fits within the range of + // values that integral type OutT is able to represent. + if (in_value < std::numeric_limits::min() || + in_value >= std::numeric_limits::max()) { + return false; + } + } + } + + if (std::is_integral::value && std::is_floating_point::value && + normalized) { + // When converting integer to floating point, normalize the value if + // necessary. + *out_value = static_cast(in_value); + *out_value /= static_cast(std::numeric_limits::max()); + } else if (std::is_floating_point::value && + std::is_integral::value && normalized) { + // Converting from floating point to a normalized integer. + if (in_value > 1 || in_value < 0) { + // Normalized float values need to be between 0 and 1. + return false; + } + // TODO(ostava): Consider allowing float to normalized integer conversion + // for 64-bit integer types. Currently it doesn't work because we don't + // have a floating point type that could store all 64 bit integers. + if (sizeof(OutT) > 4) { + return false; + } + // Expand the float to the range of the output integer and round it to the + // nearest representable value. Use doubles for the math to ensure the + // integer values are represented properly during the conversion process. + *out_value = static_cast(std::floor( + in_value * static_cast(std::numeric_limits::max()) + + 0.5)); + } else { + *out_value = static_cast(in_value); + } + + // TODO(ostava): Add handling of normalized attributes when converting + // between different integer representations. If the attribute is + // normalized, integer values should be converted as if they represent 0-1 + // range. E.g. when we convert uint16 to uint8, the range <0, 2^16 - 1> + // should be converted to range <0, 2^8 - 1>. + return true; + } + DataBuffer *buffer_; // The buffer descriptor is stored at the time the buffer is attached to this // attribute. The purpose is to detect if any changes happened to the buffer // since the time it was attached. DataBufferDescriptor buffer_descriptor_; - int8_t num_components_; + uint8_t num_components_; DataType data_type_; bool normalized_; int64_t byte_stride_; @@ -323,6 +448,54 @@ class GeometryAttribute { friend struct GeometryAttributeHasher; }; +#ifdef DRACO_TRANSCODER_SUPPORTED +template +Status GeometryAttribute::ConvertAndSetAttributeValue(AttributeValueIndex avi, + int input_num_components, + const InputT *value) { + switch (this->data_type()) { + case DT_INT8: + return ConvertAndSetAttributeTypedValue( + avi, input_num_components, value); + case DT_UINT8: + return ConvertAndSetAttributeTypedValue( + avi, input_num_components, value); + case DT_INT16: + return ConvertAndSetAttributeTypedValue( + avi, input_num_components, value); + case DT_UINT16: + return ConvertAndSetAttributeTypedValue( + avi, input_num_components, value); + case DT_INT32: + return ConvertAndSetAttributeTypedValue( + avi, input_num_components, value); + case DT_UINT32: + return ConvertAndSetAttributeTypedValue( + avi, input_num_components, value); + case DT_INT64: + return ConvertAndSetAttributeTypedValue( + avi, input_num_components, value); + case DT_UINT64: + return ConvertAndSetAttributeTypedValue( + avi, input_num_components, value); + case DT_FLOAT32: + return ConvertAndSetAttributeTypedValue( + avi, input_num_components, value); + case DT_FLOAT64: + return ConvertAndSetAttributeTypedValue( + avi, input_num_components, value); + case DT_BOOL: + return ConvertAndSetAttributeTypedValue( + avi, input_num_components, value); + default: + break; + } + return ErrorStatus( + "GeometryAttribute::SetAndConvertAttributeValue: Unsupported " + "attribute type."); +} +#endif + // Hashing support // Function object for using Attribute as a hash key. diff --git a/contrib/draco/src/draco/attributes/point_attribute.cc b/contrib/draco/src/draco/attributes/point_attribute.cc index b28f860c1..e54ab5427 100644 --- a/contrib/draco/src/draco/attributes/point_attribute.cc +++ b/contrib/draco/src/draco/attributes/point_attribute.cc @@ -222,4 +222,47 @@ AttributeValueIndex::ValueType PointAttribute::DeduplicateFormattedValues( } #endif +#ifdef DRACO_TRANSCODER_SUPPORTED +void PointAttribute::RemoveUnusedValues() { + if (is_mapping_identity()) { + return; // For identity mapping, all values are always used. + } + // For explicit mapping we need to check if any point is mapped to a value. + // If not we can delete the value. + IndexTypeVector is_value_used(size(), false); + int num_used_values = 0; + for (PointIndex pi(0); pi < indices_map_.size(); ++pi) { + const AttributeValueIndex avi = indices_map_[pi]; + if (!is_value_used[avi]) { + is_value_used[avi] = true; + num_used_values++; + } + } + if (num_used_values == size()) { + return; // All values are used. + } + + // Remap the values and update the point to value mapping. + IndexTypeVector + old_to_new_value_map(size(), kInvalidAttributeValueIndex); + AttributeValueIndex new_avi(0); + for (AttributeValueIndex avi(0); avi < size(); ++avi) { + if (!is_value_used[avi]) { + continue; + } + if (avi != new_avi) { + SetAttributeValue(new_avi, GetAddress(avi)); + } + old_to_new_value_map[avi] = new_avi++; + } + + // Remap all points to the new attribute values. + for (PointIndex pi(0); pi < indices_map_.size(); ++pi) { + indices_map_[pi] = old_to_new_value_map[indices_map_[pi]]; + } + + num_unique_entries_ = num_used_values; +} +#endif + } // namespace draco diff --git a/contrib/draco/src/draco/attributes/point_attribute.h b/contrib/draco/src/draco/attributes/point_attribute.h index ee3662031..d55c50c8a 100644 --- a/contrib/draco/src/draco/attributes/point_attribute.h +++ b/contrib/draco/src/draco/attributes/point_attribute.h @@ -133,6 +133,12 @@ class PointAttribute : public GeometryAttribute { return attribute_transform_data_.get(); } +#ifdef DRACO_TRANSCODER_SUPPORTED + // Removes unused values from the attribute. Value is unused when no point + // is mapped to the value. Only applicable when the mapping is not identity. + void RemoveUnusedValues(); +#endif + private: #ifdef DRACO_ATTRIBUTE_VALUES_DEDUPLICATION_SUPPORTED template diff --git a/contrib/draco/src/draco/compression/attributes/attributes_encoder.cc b/contrib/draco/src/draco/compression/attributes/attributes_encoder.cc index 797c62f30..480e3ff34 100644 --- a/contrib/draco/src/draco/compression/attributes/attributes_encoder.cc +++ b/contrib/draco/src/draco/compression/attributes/attributes_encoder.cc @@ -15,14 +15,16 @@ #include "draco/compression/attributes/attributes_encoder.h" #include "draco/core/varint_encoding.h" +#include "draco/draco_features.h" namespace draco { AttributesEncoder::AttributesEncoder() : point_cloud_encoder_(nullptr), point_cloud_(nullptr) {} -AttributesEncoder::AttributesEncoder(int att_id) : AttributesEncoder() { - AddAttributeId(att_id); +AttributesEncoder::AttributesEncoder(int point_attrib_id) + : AttributesEncoder() { + AddAttributeId(point_attrib_id); } bool AttributesEncoder::Init(PointCloudEncoder *encoder, const PointCloud *pc) { @@ -37,7 +39,15 @@ bool AttributesEncoder::EncodeAttributesEncoderData(EncoderBuffer *out_buffer) { for (uint32_t i = 0; i < num_attributes(); ++i) { const int32_t att_id = point_attribute_ids_[i]; const PointAttribute *const pa = point_cloud_->attribute(att_id); - out_buffer->Encode(static_cast(pa->attribute_type())); + GeometryAttribute::Type type = pa->attribute_type(); +#ifdef DRACO_TRANSCODER_SUPPORTED + // Attribute types TANGENT, MATERIAL, JOINTS, and WEIGHTS are not supported + // in the official bitstream. They will be encoded as GENERIC. + if (type > GeometryAttribute::GENERIC) { + type = GeometryAttribute::GENERIC; + } +#endif + out_buffer->Encode(static_cast(type)); out_buffer->Encode(static_cast(pa->data_type())); out_buffer->Encode(static_cast(pa->num_components())); out_buffer->Encode(static_cast(pa->normalized())); diff --git a/contrib/draco/src/draco/compression/attributes/kd_tree_attributes_decoder.cc b/contrib/draco/src/draco/compression/attributes/kd_tree_attributes_decoder.cc index e4d53485d..51c41cf7a 100644 --- a/contrib/draco/src/draco/compression/attributes/kd_tree_attributes_decoder.cc +++ b/contrib/draco/src/draco/compression/attributes/kd_tree_attributes_decoder.cc @@ -72,16 +72,19 @@ class PointAttributeVectorOutputIterator { Self &operator*() { return *this; } // Still needed in some cases. - // TODO(hemmer): remove. + // TODO(b/199760123): Remove. // hardcoded to 3 based on legacy usage. const Self &operator=(const VectorD &val) { DRACO_DCHECK_EQ(attributes_.size(), 1); // Expect only ONE attribute. AttributeTuple &att = attributes_[0]; PointAttribute *attribute = std::get<0>(att); + const AttributeValueIndex avi = attribute->mapped_index(point_id_); + if (avi >= static_cast(attribute->size())) { + return *this; + } const uint32_t &offset = std::get<1>(att); DRACO_DCHECK_EQ(offset, 0); // expected to be zero - attribute->SetAttributeValue(attribute->mapped_index(point_id_), - &val[0] + offset); + attribute->SetAttributeValue(avi, &val[0] + offset); return *this; } // Additional operator taking std::vector as argument. @@ -89,6 +92,10 @@ class PointAttributeVectorOutputIterator { for (auto index = 0; index < attributes_.size(); index++) { AttributeTuple &att = attributes_[index]; PointAttribute *attribute = std::get<0>(att); + const AttributeValueIndex avi = attribute->mapped_index(point_id_); + if (avi >= static_cast(attribute->size())) { + return *this; + } const uint32_t &offset = std::get<1>(att); const uint32_t &data_size = std::get<3>(att); const uint32_t &num_components = std::get<4>(att); @@ -103,10 +110,6 @@ class PointAttributeVectorOutputIterator { // redirect to copied data data_source = reinterpret_cast(data_); } - const AttributeValueIndex avi = attribute->mapped_index(point_id_); - if (avi >= static_cast(attribute->size())) { - return *this; - } attribute->SetAttributeValue(avi, data_source); } return *this; @@ -195,54 +198,55 @@ bool KdTreeAttributesDecoder::DecodePortableAttributes( data_size, num_components); total_dimensionality += num_components; } - PointAttributeVectorOutputIterator out_it(atts); + typedef PointAttributeVectorOutputIterator OutIt; + OutIt out_it(atts); switch (compression_level) { case 0: { - DynamicIntegerPointsKdTreeDecoder<0> decoder(total_dimensionality); - if (!decoder.DecodePoints(in_buffer, out_it)) { + if (!DecodePoints<0, OutIt>(total_dimensionality, num_points, in_buffer, + &out_it)) { return false; } break; } case 1: { - DynamicIntegerPointsKdTreeDecoder<1> decoder(total_dimensionality); - if (!decoder.DecodePoints(in_buffer, out_it)) { + if (!DecodePoints<1, OutIt>(total_dimensionality, num_points, in_buffer, + &out_it)) { return false; } break; } case 2: { - DynamicIntegerPointsKdTreeDecoder<2> decoder(total_dimensionality); - if (!decoder.DecodePoints(in_buffer, out_it)) { + if (!DecodePoints<2, OutIt>(total_dimensionality, num_points, in_buffer, + &out_it)) { return false; } break; } case 3: { - DynamicIntegerPointsKdTreeDecoder<3> decoder(total_dimensionality); - if (!decoder.DecodePoints(in_buffer, out_it)) { + if (!DecodePoints<3, OutIt>(total_dimensionality, num_points, in_buffer, + &out_it)) { return false; } break; } case 4: { - DynamicIntegerPointsKdTreeDecoder<4> decoder(total_dimensionality); - if (!decoder.DecodePoints(in_buffer, out_it)) { + if (!DecodePoints<4, OutIt>(total_dimensionality, num_points, in_buffer, + &out_it)) { return false; } break; } case 5: { - DynamicIntegerPointsKdTreeDecoder<5> decoder(total_dimensionality); - if (!decoder.DecodePoints(in_buffer, out_it)) { + if (!DecodePoints<5, OutIt>(total_dimensionality, num_points, in_buffer, + &out_it)) { return false; } break; } case 6: { - DynamicIntegerPointsKdTreeDecoder<6> decoder(total_dimensionality); - if (!decoder.DecodePoints(in_buffer, out_it)) { + if (!DecodePoints<6, OutIt>(total_dimensionality, num_points, in_buffer, + &out_it)) { return false; } break; @@ -253,6 +257,19 @@ bool KdTreeAttributesDecoder::DecodePortableAttributes( return true; } +template +bool KdTreeAttributesDecoder::DecodePoints(int total_dimensionality, + int num_expected_points, + DecoderBuffer *in_buffer, + OutIteratorT *out_iterator) { + DynamicIntegerPointsKdTreeDecoder decoder(total_dimensionality); + if (!decoder.DecodePoints(in_buffer, *out_iterator, num_expected_points) || + decoder.num_decoded_points() != num_expected_points) { + return false; + } + return true; +} + bool KdTreeAttributesDecoder::DecodeDataNeededByPortableTransforms( DecoderBuffer *in_buffer) { if (in_buffer->bitstream_version() >= DRACO_BITSTREAM_VERSION(2, 3)) { @@ -336,6 +353,10 @@ bool KdTreeAttributesDecoder::DecodeDataNeededByPortableTransforms( return false; } if (method == KdTreeAttributesEncodingMethod::kKdTreeQuantizationEncoding) { + // This method only supports one attribute with exactly three components. + if (atts.size() != 1 || std::get<4>(atts[0]) != 3) { + return false; + } uint8_t compression_level = 0; if (!in_buffer->Decode(&compression_level)) { return false; @@ -376,7 +397,7 @@ bool KdTreeAttributesDecoder::DecodeDataNeededByPortableTransforms( GetDecoder()->point_cloud()->attribute(att_id); attr->Reset(num_points); attr->SetIdentityMapping(); - }; + } PointAttributeVectorOutputIterator out_it(atts); @@ -455,7 +476,11 @@ bool KdTreeAttributesDecoder::TransformAttributeBackToSignedType( att->GetValue(avi, &unsigned_val[0]); for (int c = 0; c < att->num_components(); ++c) { // Up-cast |unsigned_val| to int32_t to ensure we don't overflow it for - // smaller data types. + // smaller data types. But first check that the up-casting does not cause + // signed integer overflow. + if (unsigned_val[c] > std::numeric_limits::max()) { + return false; + } signed_val[c] = static_cast( static_cast(unsigned_val[c]) + min_signed_values_[num_processed_signed_components + c]); diff --git a/contrib/draco/src/draco/compression/attributes/kd_tree_attributes_decoder.h b/contrib/draco/src/draco/compression/attributes/kd_tree_attributes_decoder.h index 87338d6b0..4af367a1a 100644 --- a/contrib/draco/src/draco/compression/attributes/kd_tree_attributes_decoder.h +++ b/contrib/draco/src/draco/compression/attributes/kd_tree_attributes_decoder.h @@ -31,6 +31,10 @@ class KdTreeAttributesDecoder : public AttributesDecoder { bool TransformAttributesToOriginalFormat() override; private: + template + bool DecodePoints(int total_dimensionality, int num_expected_points, + DecoderBuffer *in_buffer, OutIteratorT *out_iterator); + template bool TransformAttributeBackToSignedType(PointAttribute *att, int num_processed_signed_components); diff --git a/contrib/draco/src/draco/compression/attributes/normal_compression_utils.h b/contrib/draco/src/draco/compression/attributes/normal_compression_utils.h index 8a6f25b66..b717d0dbe 100644 --- a/contrib/draco/src/draco/compression/attributes/normal_compression_utils.h +++ b/contrib/draco/src/draco/compression/attributes/normal_compression_utils.h @@ -61,7 +61,7 @@ class OctahedronToolBox { return false; } quantization_bits_ = q; - max_quantized_value_ = (1 << quantization_bits_) - 1; + max_quantized_value_ = (1u << quantization_bits_) - 1; max_value_ = max_quantized_value_ - 1; dequantization_scale_ = 2.f / max_value_; center_value_ = max_value_ / 2; @@ -208,7 +208,9 @@ class OctahedronToolBox { DRACO_DCHECK_LE(t, center_value_); DRACO_DCHECK_GE(s, -center_value_); DRACO_DCHECK_GE(t, -center_value_); - return std::abs(s) + std::abs(t) <= center_value_; + const uint32_t st = + static_cast(std::abs(s)) + static_cast(std::abs(t)); + return st <= center_value_; } void InvertDiamond(int32_t *s, int32_t *t) const { @@ -230,19 +232,29 @@ class OctahedronToolBox { sign_t = (*t > 0) ? 1 : -1; } - const int32_t corner_point_s = sign_s * center_value_; - const int32_t corner_point_t = sign_t * center_value_; - *s = 2 * *s - corner_point_s; - *t = 2 * *t - corner_point_t; + // Perform the addition and subtraction using unsigned integers to avoid + // signed integer overflows for bad data. Note that the result will be + // unchanged for non-overflowing cases. + const uint32_t corner_point_s = sign_s * center_value_; + const uint32_t corner_point_t = sign_t * center_value_; + uint32_t us = *s; + uint32_t ut = *t; + us = us + us - corner_point_s; + ut = ut + ut - corner_point_t; if (sign_s * sign_t >= 0) { - int32_t temp = *s; - *s = -*t; - *t = -temp; + uint32_t temp = us; + us = -ut; + ut = -temp; } else { - std::swap(*s, *t); + std::swap(us, ut); } - *s = (*s + corner_point_s) / 2; - *t = (*t + corner_point_t) / 2; + us = us + corner_point_s; + ut = ut + corner_point_t; + + *s = us; + *t = ut; + *s /= 2; + *t /= 2; } void InvertDirection(int32_t *s, int32_t *t) const { @@ -318,7 +330,7 @@ class OctahedronToolBox { // Remaining coordinate can be computed by projecting the (y, z) values onto // the surface of the octahedron. - const float x = 1.f - abs(y) - abs(z); + const float x = 1.f - std::abs(y) - std::abs(z); // |x| is essentially a signed distance from the diagonal edges of the // diamond shown on the figure above. It is positive for all points in the diff --git a/contrib/draco/src/draco/compression/attributes/point_d_vector.h b/contrib/draco/src/draco/compression/attributes/point_d_vector.h index 3b115d500..6ceb454ae 100644 --- a/contrib/draco/src/draco/compression/attributes/point_d_vector.h +++ b/contrib/draco/src/draco/compression/attributes/point_d_vector.h @@ -16,7 +16,9 @@ #ifndef DRACO_COMPRESSION_ATTRIBUTES_POINT_D_VECTOR_H_ #define DRACO_COMPRESSION_ATTRIBUTES_POINT_D_VECTOR_H_ +#include #include +#include #include #include @@ -99,11 +101,17 @@ class PointDVector { data_(n_items * dimensionality), data0_(data_.data()) {} // random access iterator - class PointDVectorIterator - : public std::iterator { + class PointDVectorIterator { friend class PointDVector; public: + // Iterator traits expected by std libraries. + using iterator_category = std::random_access_iterator_tag; + using value_type = size_t; + using difference_type = size_t; + using pointer = PointDVector *; + using reference = PointDVector &; + // std::iter_swap is called inside of std::partition and needs this // specialized support PseudoPointD operator*() const { diff --git a/contrib/draco/src/draco/compression/attributes/prediction_schemes/mesh_prediction_scheme_constrained_multi_parallelogram_decoder.h b/contrib/draco/src/draco/compression/attributes/prediction_schemes/mesh_prediction_scheme_constrained_multi_parallelogram_decoder.h index 36c124baa..17899d054 100644 --- a/contrib/draco/src/draco/compression/attributes/prediction_schemes/mesh_prediction_scheme_constrained_multi_parallelogram_decoder.h +++ b/contrib/draco/src/draco/compression/attributes/prediction_schemes/mesh_prediction_scheme_constrained_multi_parallelogram_decoder.h @@ -22,6 +22,7 @@ #include "draco/compression/attributes/prediction_schemes/mesh_prediction_scheme_decoder.h" #include "draco/compression/attributes/prediction_schemes/mesh_prediction_scheme_parallelogram_shared.h" #include "draco/compression/bit_coders/rans_bit_decoder.h" +#include "draco/core/math_utils.h" #include "draco/core/varint_decoding.h" #include "draco/draco_features.h" @@ -161,7 +162,8 @@ bool MeshPredictionSchemeConstrainedMultiParallelogramDecoder< if (!is_crease) { ++num_used_parallelograms; for (int j = 0; j < num_components; ++j) { - multi_pred_vals[j] += pred_vals[i][j]; + multi_pred_vals[j] = + AddAsUnsigned(multi_pred_vals[j], pred_vals[i][j]); } } } @@ -210,6 +212,9 @@ bool MeshPredictionSchemeConstrainedMultiParallelogramDecoder< if (!DecodeVarint(&num_flags, buffer)) { return false; } + if (num_flags > this->mesh_data().corner_table()->num_corners()) { + return false; + } if (num_flags > 0) { is_crease_edge_[i].resize(num_flags); RAnsBitDecoder decoder; diff --git a/contrib/draco/src/draco/compression/attributes/prediction_schemes/mesh_prediction_scheme_constrained_multi_parallelogram_encoder.h b/contrib/draco/src/draco/compression/attributes/prediction_schemes/mesh_prediction_scheme_constrained_multi_parallelogram_encoder.h index 77df8ee24..736598b15 100644 --- a/contrib/draco/src/draco/compression/attributes/prediction_schemes/mesh_prediction_scheme_constrained_multi_parallelogram_encoder.h +++ b/contrib/draco/src/draco/compression/attributes/prediction_schemes/mesh_prediction_scheme_constrained_multi_parallelogram_encoder.h @@ -392,7 +392,7 @@ bool MeshPredictionSchemeConstrainedMultiParallelogramEncoder< RAnsBitEncoder encoder; encoder.StartEncoding(); // Encode the crease edge flags in the reverse vertex order that is needed - // be the decoder. Note that for the currently supported mode, each vertex + // by the decoder. Note that for the currently supported mode, each vertex // has exactly |num_used_parallelograms| edges that need to be encoded. for (int j = static_cast(is_crease_edge_[i].size()) - num_used_parallelograms; diff --git a/contrib/draco/src/draco/compression/attributes/prediction_schemes/mesh_prediction_scheme_multi_parallelogram_decoder.h b/contrib/draco/src/draco/compression/attributes/prediction_schemes/mesh_prediction_scheme_multi_parallelogram_decoder.h index fc82e0a8f..9825c7261 100644 --- a/contrib/draco/src/draco/compression/attributes/prediction_schemes/mesh_prediction_scheme_multi_parallelogram_decoder.h +++ b/contrib/draco/src/draco/compression/attributes/prediction_schemes/mesh_prediction_scheme_multi_parallelogram_decoder.h @@ -18,6 +18,7 @@ #include "draco/compression/attributes/prediction_schemes/mesh_prediction_scheme_decoder.h" #include "draco/compression/attributes/prediction_schemes/mesh_prediction_scheme_parallelogram_shared.h" +#include "draco/core/math_utils.h" #include "draco/draco_features.h" namespace draco { @@ -89,7 +90,8 @@ bool MeshPredictionSchemeMultiParallelogramDecoder(data[data_offset + 1])); } - void ComputePredictedValue(CornerIndex corner_id, const DataTypeT *data, + bool ComputePredictedValue(CornerIndex corner_id, const DataTypeT *data, int data_id); private: @@ -123,6 +123,10 @@ bool MeshPredictionSchemeTexCoordsDecoder:: ComputeOriginalValues(const CorrType *in_corr, DataTypeT *out_data, int /* size */, int num_components, const PointIndex *entry_to_point_id_map) { + if (num_components != 2) { + // Corrupt/malformed input. Two output components are req'd. + return false; + } num_components_ = num_components; entry_to_point_id_map_ = entry_to_point_id_map; predicted_value_ = @@ -133,7 +137,9 @@ bool MeshPredictionSchemeTexCoordsDecoder:: static_cast(this->mesh_data().data_to_corner_map()->size()); for (int p = 0; p < corner_map_size; ++p) { const CornerIndex corner_id = this->mesh_data().data_to_corner_map()->at(p); - ComputePredictedValue(corner_id, out_data, p); + if (!ComputePredictedValue(corner_id, out_data, p)) { + return false; + } const int dst_offset = p * num_components; this->transform().ComputeOriginalValue( @@ -159,6 +165,11 @@ bool MeshPredictionSchemeTexCoordsDecoder:: if (num_orientations == 0) { return false; } + if (num_orientations > this->mesh_data().corner_table()->num_corners()) { + // We can't have more orientations than the maximum number of decoded + // values. + return false; + } orientations_.resize(num_orientations); bool last_orientation = true; RAnsBitDecoder decoder; @@ -177,7 +188,7 @@ bool MeshPredictionSchemeTexCoordsDecoder:: } template -void MeshPredictionSchemeTexCoordsDecoder:: +bool MeshPredictionSchemeTexCoordsDecoder:: ComputePredictedValue(CornerIndex corner_id, const DataTypeT *data, int data_id) { // Compute the predicted UV coordinate from the positions on all corners @@ -206,9 +217,17 @@ void MeshPredictionSchemeTexCoordsDecoder:: const Vector2f p_uv = GetTexCoordForEntryId(prev_data_id, data); if (p_uv == n_uv) { // We cannot do a reliable prediction on degenerated UV triangles. - predicted_value_[0] = static_cast(p_uv[0]); - predicted_value_[1] = static_cast(p_uv[1]); - return; + // Technically floats > INT_MAX are undefined, but compilers will + // convert those values to INT_MIN. We are being explicit here for asan. + for (const int i : {0, 1}) { + if (std::isnan(p_uv[i]) || static_cast(p_uv[i]) > INT_MAX || + static_cast(p_uv[i]) < INT_MIN) { + predicted_value_[i] = INT_MIN; + } else { + predicted_value_[i] = static_cast(p_uv[i]); + } + } + return true; } // Get positions at all corners. @@ -282,32 +301,40 @@ void MeshPredictionSchemeTexCoordsDecoder:: const float pnvs = pn_uv[1] * s + n_uv[1]; const float pnvt = pn_uv[1] * t; Vector2f predicted_uv; + if (orientations_.empty()) { + return false; + } // When decoding the data, we already know which orientation to use. const bool orientation = orientations_.back(); orientations_.pop_back(); - if (orientation) + if (orientation) { predicted_uv = Vector2f(pnus - pnvt, pnvs + pnut); - else + } else { predicted_uv = Vector2f(pnus + pnvt, pnvs - pnut); - + } if (std::is_integral::value) { // Round the predicted value for integer types. - if (std::isnan(predicted_uv[0])) { + // Technically floats > INT_MAX are undefined, but compilers will + // convert those values to INT_MIN. We are being explicit here for asan. + const double u = floor(predicted_uv[0] + 0.5); + if (std::isnan(u) || u > INT_MAX || u < INT_MIN) { predicted_value_[0] = INT_MIN; } else { - predicted_value_[0] = static_cast(floor(predicted_uv[0] + 0.5)); + predicted_value_[0] = static_cast(u); } - if (std::isnan(predicted_uv[1])) { + const double v = floor(predicted_uv[1] + 0.5); + if (std::isnan(v) || v > INT_MAX || v < INT_MIN) { predicted_value_[1] = INT_MIN; } else { - predicted_value_[1] = static_cast(floor(predicted_uv[1] + 0.5)); + predicted_value_[1] = static_cast(v); } } else { predicted_value_[0] = static_cast(predicted_uv[0]); predicted_value_[1] = static_cast(predicted_uv[1]); } - return; + + return true; } // Else we don't have available textures on both corners. For such case we // can't use positions for predicting the uv value and we resort to delta @@ -330,12 +357,13 @@ void MeshPredictionSchemeTexCoordsDecoder:: for (int i = 0; i < num_components_; ++i) { predicted_value_[i] = 0; } - return; + return true; } } for (int i = 0; i < num_components_; ++i) { predicted_value_[i] = data[data_offset + i]; } + return true; } } // namespace draco diff --git a/contrib/draco/src/draco/compression/attributes/prediction_schemes/mesh_prediction_scheme_tex_coords_portable_encoder.h b/contrib/draco/src/draco/compression/attributes/prediction_schemes/mesh_prediction_scheme_tex_coords_portable_encoder.h index 741ec66dc..44fcc7a6a 100644 --- a/contrib/draco/src/draco/compression/attributes/prediction_schemes/mesh_prediction_scheme_tex_coords_portable_encoder.h +++ b/contrib/draco/src/draco/compression/attributes/prediction_schemes/mesh_prediction_scheme_tex_coords_portable_encoder.h @@ -98,7 +98,10 @@ bool MeshPredictionSchemeTexCoordsPortableEncoder(this->mesh_data().data_to_corner_map()->size() - 1); p >= 0; --p) { const CornerIndex corner_id = this->mesh_data().data_to_corner_map()->at(p); - predictor_.template ComputePredictedValue(corner_id, in_data, p); + if (!predictor_.template ComputePredictedValue(corner_id, in_data, + p)) { + return false; + } const int dst_offset = p * num_components; this->transform().ComputeCorrection(in_data + dst_offset, diff --git a/contrib/draco/src/draco/compression/attributes/prediction_schemes/mesh_prediction_scheme_tex_coords_portable_predictor.h b/contrib/draco/src/draco/compression/attributes/prediction_schemes/mesh_prediction_scheme_tex_coords_portable_predictor.h index f05e5ddd7..26262fb13 100644 --- a/contrib/draco/src/draco/compression/attributes/prediction_schemes/mesh_prediction_scheme_tex_coords_portable_predictor.h +++ b/contrib/draco/src/draco/compression/attributes/prediction_schemes/mesh_prediction_scheme_tex_coords_portable_predictor.h @@ -17,6 +17,9 @@ #include +#include +#include + #include "draco/attributes/point_attribute.h" #include "draco/core/math_utils.h" #include "draco/core/vector_d.h" @@ -105,10 +108,14 @@ bool MeshPredictionSchemeTexCoordsPortablePredictor< next_data_id = mesh_data_.vertex_to_data_map()->at(next_vert_id); prev_data_id = mesh_data_.vertex_to_data_map()->at(prev_vert_id); + typedef VectorD Vec2; + typedef VectorD Vec3; + typedef VectorD Vec2u; + if (prev_data_id < data_id && next_data_id < data_id) { // Both other corners have available UV coordinates for prediction. - const VectorD n_uv = GetTexCoordForEntryId(next_data_id, data); - const VectorD p_uv = GetTexCoordForEntryId(prev_data_id, data); + const Vec2 n_uv = GetTexCoordForEntryId(next_data_id, data); + const Vec2 p_uv = GetTexCoordForEntryId(prev_data_id, data); if (p_uv == n_uv) { // We cannot do a reliable prediction on degenerated UV triangles. predicted_value_[0] = p_uv[0]; @@ -117,9 +124,9 @@ bool MeshPredictionSchemeTexCoordsPortablePredictor< } // Get positions at all corners. - const VectorD tip_pos = GetPositionForEntryId(data_id); - const VectorD next_pos = GetPositionForEntryId(next_data_id); - const VectorD prev_pos = GetPositionForEntryId(prev_data_id); + const Vec3 tip_pos = GetPositionForEntryId(data_id); + const Vec3 next_pos = GetPositionForEntryId(next_data_id); + const Vec3 prev_pos = GetPositionForEntryId(prev_data_id); // We use the positions of the above triangle to predict the texture // coordinate on the tip corner C. // To convert the triangle into the UV coordinate system we first compute @@ -135,17 +142,17 @@ bool MeshPredictionSchemeTexCoordsPortablePredictor< // Where next_pos is point (N), prev_pos is point (P) and tip_pos is the // position of predicted coordinate (C). // - const VectorD pn = prev_pos - next_pos; + const Vec3 pn = prev_pos - next_pos; const uint64_t pn_norm2_squared = pn.SquaredNorm(); if (pn_norm2_squared != 0) { // Compute the projection of C onto PN by computing dot product of CN with // PN and normalizing it by length of PN. This gives us a factor |s| where // |s = PN.Dot(CN) / PN.SquaredNorm2()|. This factor can be used to // compute X in UV space |X_UV| as |X_UV = N_UV + s * PN_UV|. - const VectorD cn = tip_pos - next_pos; + const Vec3 cn = tip_pos - next_pos; const int64_t cn_dot_pn = pn.Dot(cn); - const VectorD pn_uv = p_uv - n_uv; + const Vec2 pn_uv = p_uv - n_uv; // Because we perform all computations with integers, we don't explicitly // compute the normalized factor |s|, but rather we perform all operations // over UV vectors in a non-normalized coordinate system scaled with a @@ -153,19 +160,30 @@ bool MeshPredictionSchemeTexCoordsPortablePredictor< // // x_uv = X_UV * PN.Norm2Squared() // - const VectorD x_uv = - n_uv * pn_norm2_squared + (cn_dot_pn * pn_uv); - + const int64_t n_uv_absmax_element = + std::max(std::abs(n_uv[0]), std::abs(n_uv[1])); + if (n_uv_absmax_element > + std::numeric_limits::max() / pn_norm2_squared) { + // Return false if the below multiplication would overflow. + return false; + } + const int64_t pn_uv_absmax_element = + std::max(std::abs(pn_uv[0]), std::abs(pn_uv[1])); + if (cn_dot_pn > + std::numeric_limits::max() / pn_uv_absmax_element) { + // Return false if squared length calculation would overflow. + return false; + } + const Vec2 x_uv = n_uv * pn_norm2_squared + (cn_dot_pn * pn_uv); const int64_t pn_absmax_element = std::max(std::max(std::abs(pn[0]), std::abs(pn[1])), std::abs(pn[2])); if (cn_dot_pn > std::numeric_limits::max() / pn_absmax_element) { - // return false if squared length calculation would overflow. + // Return false if squared length calculation would overflow. return false; } // Compute squared length of vector CX in position coordinate system: - const VectorD x_pos = - next_pos + (cn_dot_pn * pn) / pn_norm2_squared; + const Vec3 x_pos = next_pos + (cn_dot_pn * pn) / pn_norm2_squared; const uint64_t cx_norm2_squared = (tip_pos - x_pos).SquaredNorm(); // Compute vector CX_UV in the uv space by rotating vector PN_UV by 90 @@ -182,7 +200,7 @@ bool MeshPredictionSchemeTexCoordsPortablePredictor< // // cx_uv = CX.Norm2() * PN.Norm2() * Rot(PN_UV) // - VectorD cx_uv(pn_uv[1], -pn_uv[0]); // Rotated PN_UV. + Vec2 cx_uv(pn_uv[1], -pn_uv[0]); // Rotated PN_UV. // Compute CX.Norm2() * PN.Norm2() const uint64_t norm_squared = IntSqrt(cx_norm2_squared * pn_norm2_squared); @@ -191,17 +209,15 @@ bool MeshPredictionSchemeTexCoordsPortablePredictor< // Predicted uv coordinate is then computed by either adding or // subtracting CX_UV to/from X_UV. - VectorD predicted_uv; + Vec2 predicted_uv; if (is_encoder_t) { // When encoding, compute both possible vectors and determine which one // results in a better prediction. // Both vectors need to be transformed back from the scaled space to // the real UV coordinate space. - const VectorD predicted_uv_0((x_uv + cx_uv) / - pn_norm2_squared); - const VectorD predicted_uv_1((x_uv - cx_uv) / - pn_norm2_squared); - const VectorD c_uv = GetTexCoordForEntryId(data_id, data); + const Vec2 predicted_uv_0((x_uv + cx_uv) / pn_norm2_squared); + const Vec2 predicted_uv_1((x_uv - cx_uv) / pn_norm2_squared); + const Vec2 c_uv = GetTexCoordForEntryId(data_id, data); if ((c_uv - predicted_uv_0).SquaredNorm() < (c_uv - predicted_uv_1).SquaredNorm()) { predicted_uv = predicted_uv_0; @@ -217,10 +233,12 @@ bool MeshPredictionSchemeTexCoordsPortablePredictor< } const bool orientation = orientations_.back(); orientations_.pop_back(); + // Perform operations in unsigned type to avoid signed integer overflow. + // Note that the result will be the same (for non-overflowing values). if (orientation) { - predicted_uv = (x_uv + cx_uv) / pn_norm2_squared; + predicted_uv = Vec2(Vec2u(x_uv) + Vec2u(cx_uv)) / pn_norm2_squared; } else { - predicted_uv = (x_uv - cx_uv) / pn_norm2_squared; + predicted_uv = Vec2(Vec2u(x_uv) - Vec2u(cx_uv)) / pn_norm2_squared; } } predicted_value_[0] = static_cast(predicted_uv[0]); diff --git a/contrib/draco/src/draco/compression/attributes/prediction_schemes/prediction_scheme_encoder_factory.cc b/contrib/draco/src/draco/compression/attributes/prediction_schemes/prediction_scheme_encoder_factory.cc index f410a6cd2..2338f2f76 100644 --- a/contrib/draco/src/draco/compression/attributes/prediction_schemes/prediction_scheme_encoder_factory.cc +++ b/contrib/draco/src/draco/compression/attributes/prediction_schemes/prediction_scheme_encoder_factory.cc @@ -18,22 +18,58 @@ namespace draco { PredictionSchemeMethod SelectPredictionMethod( int att_id, const PointCloudEncoder *encoder) { - if (encoder->options()->GetSpeed() >= 10) { + return SelectPredictionMethod(att_id, *encoder->options(), encoder); +} + +PredictionSchemeMethod SelectPredictionMethod( + int att_id, const EncoderOptions &options, + const PointCloudEncoder *encoder) { + if (options.GetSpeed() >= 10) { // Selected fastest, though still doing some compression. return PREDICTION_DIFFERENCE; } if (encoder->GetGeometryType() == TRIANGULAR_MESH) { // Use speed setting to select the best encoding method. + const int att_quant = + options.GetAttributeInt(att_id, "quantization_bits", -1); const PointAttribute *const att = encoder->point_cloud()->attribute(att_id); - if (att->attribute_type() == GeometryAttribute::TEX_COORD) { - if (encoder->options()->GetSpeed() < 4) { + if (att_quant != -1 && + att->attribute_type() == GeometryAttribute::TEX_COORD && + att->num_components() == 2) { + // Texture coordinate predictor needs a position attribute that is either + // integer or quantized. For numerical reasons, we require the position + // quantization to be at most 21 bits and the 2*position_quantization + + // uv_quantization < 64 (TODO(b/231259902)). + const PointAttribute *const pos_att = + encoder->point_cloud()->GetNamedAttribute( + GeometryAttribute::POSITION); + bool is_pos_att_valid = false; + if (pos_att) { + if (IsDataTypeIntegral(pos_att->data_type())) { + is_pos_att_valid = true; + } else { + // Check quantization of the position attribute. + const int pos_att_id = encoder->point_cloud()->GetNamedAttributeId( + GeometryAttribute::POSITION); + const int pos_quant = + options.GetAttributeInt(pos_att_id, "quantization_bits", -1); + // Must be quantized but the quantization is restricted to 21 bits and + // 2*|pos_quant|+|att_quant| must be smaller than 64 bits. + if (pos_quant > 0 && pos_quant <= 21 && + 2 * pos_quant + att_quant < 64) { + is_pos_att_valid = true; + } + } + } + + if (is_pos_att_valid && options.GetSpeed() < 4) { // Use texture coordinate prediction for speeds 0, 1, 2, 3. return MESH_PREDICTION_TEX_COORDS_PORTABLE; } } if (att->attribute_type() == GeometryAttribute::NORMAL) { #ifdef DRACO_NORMAL_ENCODING_SUPPORTED - if (encoder->options()->GetSpeed() < 4) { + if (options.GetSpeed() < 4) { // Use geometric normal prediction for speeds 0, 1, 2, 3. // For this prediction, the position attribute needs to be either // integer or quantized as well. @@ -43,8 +79,8 @@ PredictionSchemeMethod SelectPredictionMethod( encoder->point_cloud()->GetNamedAttribute( GeometryAttribute::POSITION); if (pos_att && (IsDataTypeIntegral(pos_att->data_type()) || - encoder->options()->GetAttributeInt( - pos_att_id, "quantization_bits", -1) > 0)) { + options.GetAttributeInt(pos_att_id, "quantization_bits", + -1) > 0)) { return MESH_PREDICTION_GEOMETRIC_NORMAL; } } @@ -52,11 +88,10 @@ PredictionSchemeMethod SelectPredictionMethod( return PREDICTION_DIFFERENCE; // default } // Handle other attribute types. - if (encoder->options()->GetSpeed() >= 8) { + if (options.GetSpeed() >= 8) { return PREDICTION_DIFFERENCE; } - if (encoder->options()->GetSpeed() >= 2 || - encoder->point_cloud()->num_points() < 40) { + if (options.GetSpeed() >= 2 || encoder->point_cloud()->num_points() < 40) { // Parallelogram prediction is used for speeds 2 - 7 or when the overhead // of using constrained multi-parallelogram would be too high. return MESH_PREDICTION_PARALLELOGRAM; diff --git a/contrib/draco/src/draco/compression/attributes/prediction_schemes/prediction_scheme_encoder_factory.h b/contrib/draco/src/draco/compression/attributes/prediction_schemes/prediction_scheme_encoder_factory.h index 40a7683aa..11db5a62e 100644 --- a/contrib/draco/src/draco/compression/attributes/prediction_schemes/prediction_scheme_encoder_factory.h +++ b/contrib/draco/src/draco/compression/attributes/prediction_schemes/prediction_scheme_encoder_factory.h @@ -38,6 +38,10 @@ namespace draco { PredictionSchemeMethod SelectPredictionMethod(int att_id, const PointCloudEncoder *encoder); +PredictionSchemeMethod SelectPredictionMethod(int att_id, + const EncoderOptions &options, + const PointCloudEncoder *encoder); + // Factory class for creating mesh prediction schemes. template struct MeshPredictionSchemeEncoderFactory { @@ -97,10 +101,11 @@ CreatePredictionSchemeForEncoder(PredictionSchemeMethod method, int att_id, // template nature of the prediction schemes). const MeshEncoder *const mesh_encoder = static_cast(encoder); + const uint16_t bitstream_version = kDracoMeshBitstreamVersion; auto ret = CreateMeshPredictionScheme< MeshEncoder, PredictionSchemeEncoder, MeshPredictionSchemeEncoderFactory>( - mesh_encoder, method, att_id, transform, kDracoMeshBitstreamVersion); + mesh_encoder, method, att_id, transform, bitstream_version); if (ret) { return ret; } diff --git a/contrib/draco/src/draco/compression/attributes/prediction_schemes/prediction_scheme_normal_octahedron_canonicalized_decoding_transform.h b/contrib/draco/src/draco/compression/attributes/prediction_schemes/prediction_scheme_normal_octahedron_canonicalized_decoding_transform.h index 5a6c7c2dd..e9e345343 100644 --- a/contrib/draco/src/draco/compression/attributes/prediction_schemes/prediction_scheme_normal_octahedron_canonicalized_decoding_transform.h +++ b/contrib/draco/src/draco/compression/attributes/prediction_schemes/prediction_scheme_normal_octahedron_canonicalized_decoding_transform.h @@ -21,6 +21,7 @@ #include "draco/compression/attributes/prediction_schemes/prediction_scheme_normal_octahedron_canonicalized_transform_base.h" #include "draco/core/decoder_buffer.h" #include "draco/core/macros.h" +#include "draco/core/math_utils.h" #include "draco/core/vector_d.h" namespace draco { @@ -98,9 +99,8 @@ class PredictionSchemeNormalOctahedronCanonicalizedDecodingTransform if (!pred_is_in_bottom_left) { pred = this->RotatePoint(pred, rotation_count); } - Point2 orig = pred + corr; - orig[0] = this->ModMax(orig[0]); - orig[1] = this->ModMax(orig[1]); + Point2 orig(this->ModMax(AddAsUnsigned(pred[0], corr[0])), + this->ModMax(AddAsUnsigned(pred[1], corr[1]))); if (!pred_is_in_bottom_left) { const int32_t reverse_rotation_count = (4 - rotation_count) % 4; orig = this->RotatePoint(orig, reverse_rotation_count); diff --git a/contrib/draco/src/draco/compression/attributes/prediction_schemes/prediction_scheme_normal_octahedron_canonicalized_transform_test.cc b/contrib/draco/src/draco/compression/attributes/prediction_schemes/prediction_scheme_normal_octahedron_canonicalized_transform_test.cc index 8c8932f77..298758d8c 100644 --- a/contrib/draco/src/draco/compression/attributes/prediction_schemes/prediction_scheme_normal_octahedron_canonicalized_transform_test.cc +++ b/contrib/draco/src/draco/compression/attributes/prediction_schemes/prediction_scheme_normal_octahedron_canonicalized_transform_test.cc @@ -25,10 +25,10 @@ class PredictionSchemeNormalOctahedronCanonicalizedTransformTest Transform; typedef Transform::Point2 Point2; - void TestComputeCorrection(const Transform &transform, const int32_t &ox, - const int32_t &oy, const int32_t &px, - const int32_t &py, const int32_t &cx, - const int32_t &cy) { + void TestComputeCorrection(const Transform &transform, const int32_t ox, + const int32_t oy, const int32_t px, + const int32_t py, const int32_t cx, + const int32_t cy) { const int32_t o[2] = {ox + 7, oy + 7}; const int32_t p[2] = {px + 7, py + 7}; int32_t corr[2] = {500, 500}; @@ -38,7 +38,7 @@ class PredictionSchemeNormalOctahedronCanonicalizedTransformTest } void TestGetRotationCount(const Transform &transform, const Point2 &pred, - const int32_t &rot_dir) { + const int32_t rot_dir) { const int32_t rotation_count = transform.GetRotationCount(pred); ASSERT_EQ(rot_dir, rotation_count); } diff --git a/contrib/draco/src/draco/compression/attributes/prediction_schemes/prediction_scheme_normal_octahedron_decoding_transform.h b/contrib/draco/src/draco/compression/attributes/prediction_schemes/prediction_scheme_normal_octahedron_decoding_transform.h index a1bc4a327..d3705c8ad 100644 --- a/contrib/draco/src/draco/compression/attributes/prediction_schemes/prediction_scheme_normal_octahedron_decoding_transform.h +++ b/contrib/draco/src/draco/compression/attributes/prediction_schemes/prediction_scheme_normal_octahedron_decoding_transform.h @@ -80,19 +80,31 @@ class PredictionSchemeNormalOctahedronDecodingTransform private: Point2 ComputeOriginalValue(Point2 pred, const Point2 &corr) const { const Point2 t(this->center_value(), this->center_value()); - pred = pred - t; + typedef typename std::make_unsigned::type UnsignedDataTypeT; + typedef VectorD Point2u; + + // Perform the addition in unsigned type to avoid signed integer overflow. + // Note that the result will be the same (for non-overflowing values). + pred = Point2(Point2u(pred) - Point2u(t)); const bool pred_is_in_diamond = this->IsInDiamond(pred[0], pred[1]); if (!pred_is_in_diamond) { this->InvertDiamond(&pred[0], &pred[1]); } - Point2 orig = pred + corr; + + // Perform the addition in unsigned type to avoid signed integer overflow. + // Note that the result will be the same (for non-overflowing values). + Point2 orig(Point2u(pred) + Point2u(corr)); + orig[0] = this->ModMax(orig[0]); orig[1] = this->ModMax(orig[1]); if (!pred_is_in_diamond) { this->InvertDiamond(&orig[0], &orig[1]); } - orig = orig + t; + + // Perform the addition in unsigned type to avoid signed integer overflow. + // Note that the result will be the same (for non-overflowing values). + orig = Point2(Point2u(orig) + Point2u(t)); return orig; } }; diff --git a/contrib/draco/src/draco/compression/attributes/prediction_schemes/prediction_scheme_normal_octahedron_transform_test.cc b/contrib/draco/src/draco/compression/attributes/prediction_schemes/prediction_scheme_normal_octahedron_transform_test.cc index 1001b19fa..1403973c4 100644 --- a/contrib/draco/src/draco/compression/attributes/prediction_schemes/prediction_scheme_normal_octahedron_transform_test.cc +++ b/contrib/draco/src/draco/compression/attributes/prediction_schemes/prediction_scheme_normal_octahedron_transform_test.cc @@ -23,10 +23,10 @@ class PredictionSchemeNormalOctahedronTransformTest : public ::testing::Test { Transform; typedef Transform::Point2 Point2; - void TestComputeCorrection(const Transform &transform, const int32_t &ox, - const int32_t &oy, const int32_t &px, - const int32_t &py, const int32_t &cx, - const int32_t &cy) { + void TestComputeCorrection(const Transform &transform, const int32_t ox, + const int32_t oy, const int32_t px, + const int32_t py, const int32_t cx, + const int32_t cy) { const int32_t o[2] = {ox + 7, oy + 7}; const int32_t p[2] = {px + 7, py + 7}; int32_t corr[2] = {500, 500}; diff --git a/contrib/draco/src/draco/compression/attributes/prediction_schemes/prediction_scheme_wrap_transform_base.h b/contrib/draco/src/draco/compression/attributes/prediction_schemes/prediction_scheme_wrap_transform_base.h index 26f61fbaf..bba3de09c 100644 --- a/contrib/draco/src/draco/compression/attributes/prediction_schemes/prediction_scheme_wrap_transform_base.h +++ b/contrib/draco/src/draco/compression/attributes/prediction_schemes/prediction_scheme_wrap_transform_base.h @@ -70,10 +70,10 @@ class PredictionSchemeWrapTransformBase { clamped_value_[i] = predicted_val[i]; } } - return &clamped_value_[0]; + return clamped_value_.data(); } - // TODO(hemmer): Consider refactoring to avoid this dummy. + // TODO(b/199760123): Consider refactoring to avoid this dummy. int quantization_bits() const { DRACO_DCHECK(false); return -1; diff --git a/contrib/draco/src/draco/compression/attributes/sequential_integer_attribute_decoder.cc b/contrib/draco/src/draco/compression/attributes/sequential_integer_attribute_decoder.cc index 83f42125a..17f32fc16 100644 --- a/contrib/draco/src/draco/compression/attributes/sequential_integer_attribute_decoder.cc +++ b/contrib/draco/src/draco/compression/attributes/sequential_integer_attribute_decoder.cc @@ -148,8 +148,9 @@ bool SequentialIntegerAttributeDecoder::DecodeIntegerValues( return false; } for (size_t i = 0; i < num_values; ++i) { - if (!in_buffer->Decode(portable_attribute_data + i, num_bytes)) + if (!in_buffer->Decode(portable_attribute_data + i, num_bytes)) { return false; + } } } } @@ -228,12 +229,13 @@ void SequentialIntegerAttributeDecoder::StoreTypedValues(uint32_t num_values) { void SequentialIntegerAttributeDecoder::PreparePortableAttribute( int num_entries, int num_components) { - GeometryAttribute va; - va.Init(attribute()->attribute_type(), nullptr, num_components, DT_INT32, + GeometryAttribute ga; + ga.Init(attribute()->attribute_type(), nullptr, num_components, DT_INT32, false, num_components * DataTypeLength(DT_INT32), 0); - std::unique_ptr port_att(new PointAttribute(va)); + std::unique_ptr port_att(new PointAttribute(ga)); port_att->SetIdentityMapping(); port_att->Reset(num_entries); + port_att->set_unique_id(attribute()->unique_id()); SetPortableAttribute(std::move(port_att)); } diff --git a/contrib/draco/src/draco/compression/attributes/sequential_integer_attribute_encoder.cc b/contrib/draco/src/draco/compression/attributes/sequential_integer_attribute_encoder.cc index e66a0a8a4..5f673be42 100644 --- a/contrib/draco/src/draco/compression/attributes/sequential_integer_attribute_encoder.cc +++ b/contrib/draco/src/draco/compression/attributes/sequential_integer_attribute_encoder.cc @@ -138,9 +138,11 @@ bool SequentialIntegerAttributeEncoder::EncodeValues( // All integer values are initialized. Process them using the prediction // scheme if we have one. if (prediction_scheme_) { - prediction_scheme_->ComputeCorrectionValues( - portable_attribute_data, &encoded_data[0], num_values, num_components, - point_ids.data()); + if (!prediction_scheme_->ComputeCorrectionValues( + portable_attribute_data, &encoded_data[0], num_values, + num_components, point_ids.data())) { + return false; + } } if (prediction_scheme_ == nullptr || diff --git a/contrib/draco/src/draco/compression/attributes/sequential_normal_attribute_encoder.cc b/contrib/draco/src/draco/compression/attributes/sequential_normal_attribute_encoder.cc index 2e20e89e6..3c5ef0ebc 100644 --- a/contrib/draco/src/draco/compression/attributes/sequential_normal_attribute_encoder.cc +++ b/contrib/draco/src/draco/compression/attributes/sequential_normal_attribute_encoder.cc @@ -20,8 +20,9 @@ namespace draco { bool SequentialNormalAttributeEncoder::Init(PointCloudEncoder *encoder, int attribute_id) { - if (!SequentialIntegerAttributeEncoder::Init(encoder, attribute_id)) + if (!SequentialIntegerAttributeEncoder::Init(encoder, attribute_id)) { return false; + } // Currently this encoder works only for 3-component normal vectors. if (attribute()->num_components() != 3) { return false; diff --git a/contrib/draco/src/draco/compression/bit_coders/direct_bit_decoder.h b/contrib/draco/src/draco/compression/bit_coders/direct_bit_decoder.h index b9fbc2d6f..6273692a2 100644 --- a/contrib/draco/src/draco/compression/bit_coders/direct_bit_decoder.h +++ b/contrib/draco/src/draco/compression/bit_coders/direct_bit_decoder.h @@ -47,14 +47,13 @@ class DirectBitDecoder { // Decode the next |nbits| and return the sequence in |value|. |nbits| must be // > 0 and <= 32. - void DecodeLeastSignificantBits32(int nbits, uint32_t *value) { + bool DecodeLeastSignificantBits32(int nbits, uint32_t *value) { DRACO_DCHECK_EQ(true, nbits <= 32); DRACO_DCHECK_EQ(true, nbits > 0); const int remaining = 32 - num_used_bits_; if (nbits <= remaining) { if (pos_ == bits_.end()) { - *value = 0; - return; + return false; } *value = (*pos_ << num_used_bits_) >> (32 - nbits); num_used_bits_ += nbits; @@ -64,8 +63,7 @@ class DirectBitDecoder { } } else { if (pos_ + 1 == bits_.end()) { - *value = 0; - return; + return false; } const uint32_t value_l = ((*pos_) << num_used_bits_); num_used_bits_ = nbits - remaining; @@ -73,6 +71,7 @@ class DirectBitDecoder { const uint32_t value_r = (*pos_) >> (32 - num_used_bits_); *value = (value_l >> (32 - num_used_bits_ - remaining)) | value_r; } + return true; } void EndDecoding() {} diff --git a/contrib/draco/src/draco/compression/config/encoder_options.h b/contrib/draco/src/draco/compression/config/encoder_options.h index ed1b02068..e8a55bbba 100644 --- a/contrib/draco/src/draco/compression/config/encoder_options.h +++ b/contrib/draco/src/draco/compression/config/encoder_options.h @@ -65,6 +65,10 @@ class EncoderOptionsBase : public DracoOptions { this->SetGlobalInt("encoding_speed", encoding_speed); this->SetGlobalInt("decoding_speed", decoding_speed); } + bool IsSpeedSet() const { + return this->IsGlobalOptionSet("encoding_speed") || + this->IsGlobalOptionSet("decoding_speed"); + } // Sets a given feature as supported or unsupported by the target decoder. // Encoder will always use only supported features when encoding the input diff --git a/contrib/draco/src/draco/compression/decode_test.cc b/contrib/draco/src/draco/compression/decode_test.cc index 198714690..8f3e7f4e9 100644 --- a/contrib/draco/src/draco/compression/decode_test.cc +++ b/contrib/draco/src/draco/compression/decode_test.cc @@ -17,9 +17,11 @@ #include #include +#include "draco/compression/encode.h" #include "draco/core/draco_test_base.h" #include "draco/core/draco_test_utils.h" #include "draco/io/file_utils.h" +#include "draco/io/obj_encoder.h" namespace { @@ -166,4 +168,78 @@ TEST_F(DecodeTest, TestSkipAttributeTransformWithNoQuantization) { ASSERT_EQ(pos_att->GetAttributeTransformData(), nullptr); } +TEST_F(DecodeTest, TestSkipAttributeTransformUniqueId) { + // Tests that decoders preserve unique id of attributes even when their + // attribute transforms are skipped. + const std::string file_name = "cube_att.obj"; + auto src_mesh = draco::ReadMeshFromTestFile(file_name); + ASSERT_NE(src_mesh, nullptr); + + constexpr int kPosUniqueId = 7; + constexpr int kNormUniqueId = 42; + // Set unique ids for some of the attributes. + src_mesh + ->attribute( + src_mesh->GetNamedAttributeId(draco::GeometryAttribute::POSITION)) + ->set_unique_id(kPosUniqueId); + src_mesh + ->attribute( + src_mesh->GetNamedAttributeId(draco::GeometryAttribute::NORMAL)) + ->set_unique_id(kNormUniqueId); + + draco::EncoderBuffer encoder_buffer; + draco::Encoder encoder; + encoder.SetAttributeQuantization(draco::GeometryAttribute::POSITION, 10); + encoder.SetAttributeQuantization(draco::GeometryAttribute::NORMAL, 11); + encoder.EncodeMeshToBuffer(*src_mesh, &encoder_buffer); + + // Create a draco decoding buffer. + draco::DecoderBuffer buffer; + buffer.Init(encoder_buffer.data(), encoder_buffer.size()); + + // First we decode the mesh without skipping the attribute transforms. + draco::Decoder decoder_no_skip; + std::unique_ptr mesh_no_skip = + decoder_no_skip.DecodeMeshFromBuffer(&buffer).value(); + ASSERT_NE(mesh_no_skip, nullptr); + + // Now we decode it again while skipping some attributes. + draco::Decoder decoder_skip; + // Make sure we skip dequantization for the position and normal attribute. + decoder_skip.SetSkipAttributeTransform(draco::GeometryAttribute::POSITION); + decoder_skip.SetSkipAttributeTransform(draco::GeometryAttribute::NORMAL); + + // Decode the input data into a geometry. + buffer.Init(encoder_buffer.data(), encoder_buffer.size()); + std::unique_ptr mesh_skip = + decoder_skip.DecodeMeshFromBuffer(&buffer).value(); + ASSERT_NE(mesh_skip, nullptr); + + // Compare the unique ids. + const draco::PointAttribute *const pos_att_no_skip = + mesh_no_skip->GetNamedAttribute(draco::GeometryAttribute::POSITION); + ASSERT_NE(pos_att_no_skip, nullptr); + ASSERT_EQ(pos_att_no_skip->data_type(), draco::DataType::DT_FLOAT32); + + const draco::PointAttribute *const pos_att_skip = + mesh_skip->GetNamedAttribute(draco::GeometryAttribute::POSITION); + ASSERT_NE(pos_att_skip, nullptr); + ASSERT_EQ(pos_att_skip->data_type(), draco::DataType::DT_INT32); + + const draco::PointAttribute *const norm_att_no_skip = + mesh_no_skip->GetNamedAttribute(draco::GeometryAttribute::NORMAL); + ASSERT_NE(norm_att_no_skip, nullptr); + ASSERT_EQ(norm_att_no_skip->data_type(), draco::DataType::DT_FLOAT32); + + const draco::PointAttribute *const norm_att_skip = + mesh_skip->GetNamedAttribute(draco::GeometryAttribute::NORMAL); + ASSERT_NE(norm_att_skip, nullptr); + ASSERT_EQ(norm_att_skip->data_type(), draco::DataType::DT_INT32); + + ASSERT_EQ(pos_att_skip->unique_id(), pos_att_no_skip->unique_id()); + ASSERT_EQ(norm_att_skip->unique_id(), norm_att_no_skip->unique_id()); + std::cout << pos_att_skip->unique_id() << " " << norm_att_skip->unique_id() + << std::endl; +} + } // namespace diff --git a/contrib/draco/src/draco/compression/draco_compression_options.cc b/contrib/draco/src/draco/compression/draco_compression_options.cc new file mode 100644 index 000000000..08171c678 --- /dev/null +++ b/contrib/draco/src/draco/compression/draco_compression_options.cc @@ -0,0 +1,59 @@ +// Copyright 2022 The Draco Authors. +// +// 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 "draco/compression/draco_compression_options.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED + +namespace draco { + +SpatialQuantizationOptions::SpatialQuantizationOptions(int quantization_bits) { + SetQuantizationBits(quantization_bits); +} + +void SpatialQuantizationOptions::SetQuantizationBits(int quantization_bits) { + mode_ = LOCAL_QUANTIZATION_BITS; + quantization_bits_ = quantization_bits; +} + +bool SpatialQuantizationOptions::AreQuantizationBitsDefined() const { + return mode_ == LOCAL_QUANTIZATION_BITS; +} + +SpatialQuantizationOptions &SpatialQuantizationOptions::SetGrid(float spacing) { + mode_ = GLOBAL_GRID; + spacing_ = spacing; + return *this; +} + +bool SpatialQuantizationOptions::operator==( + const SpatialQuantizationOptions &other) const { + if (mode_ != other.mode_) { + return false; + } + if (mode_ == LOCAL_QUANTIZATION_BITS) { + if (quantization_bits_ != other.quantization_bits_) { + return false; + } + } else if (mode_ == GLOBAL_GRID) { + if (spacing_ != other.spacing_) { + return false; + } + } + return true; +} + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED diff --git a/contrib/draco/src/draco/compression/draco_compression_options.h b/contrib/draco/src/draco/compression/draco_compression_options.h new file mode 100644 index 000000000..31a4418ed --- /dev/null +++ b/contrib/draco/src/draco/compression/draco_compression_options.h @@ -0,0 +1,141 @@ +// Copyright 2019 The Draco Authors. +// +// 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 DRACO_COMPRESSION_DRACO_COMPRESSION_OPTIONS_H_ +#define DRACO_COMPRESSION_DRACO_COMPRESSION_OPTIONS_H_ + +#include "draco/draco_features.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED +#include "draco/core/status.h" + +namespace draco { + +// Quantization options for positions. Currently there are two modes for +// quantizing positions: +// +// 1. Quantization bits: +// - User defined number of quantization bits that is evenly distributed +// to cover the compressed geometry. +// 2. Grid: +// - Positions are snapped to a global grid defined by grid spacing. +// - This method is primarily intended to be used when the location of +// quantized vertices needs to be consistent between multiple +// geometries. +class SpatialQuantizationOptions { + public: + explicit SpatialQuantizationOptions(int quantization_bits); + + // Sets quantization bits that are going to be used for the compressed + // geometry. If the geometry is a scene, the same number of quantization bits + // is going to be applied to each mesh of the scene. Quantized values are + // going to be distributed within the bounds of individual meshes. + void SetQuantizationBits(int quantization_bits); + + // If this returns true, quantization_bits() should be used to get the + // desired number of quantization bits for compression. Otherwise the grid + // mode is selected and spacing() should be used to get the desired grid + // spacing. + bool AreQuantizationBitsDefined() const; + const int quantization_bits() const { return quantization_bits_; } + + // Defines quantization grid used for the compressed geometry. All vertices + // are going to be snapped to the nearest grid vertex that corresponds to an + // integer quantized position. |spacing| defines the distance between two grid + // vertices. E.g. a grid with |spacing| = 10 would have grid vertices at + // locations {10 * i, 10 * j, 10 * k} where i, j, k are integer numbers. + SpatialQuantizationOptions &SetGrid(float spacing); + + const float spacing() const { return spacing_; } + + bool operator==(const SpatialQuantizationOptions &other) const; + + private: + enum Mode { LOCAL_QUANTIZATION_BITS, GLOBAL_GRID }; + Mode mode_ = LOCAL_QUANTIZATION_BITS; + int quantization_bits_; // Default quantization bits for positions. + float spacing_ = 0.f; +}; + +// TODO(fgalligan): Add support for unified_position_quantization. +// Struct to hold Draco compression options. +struct DracoCompressionOptions { + int compression_level = 7; // compression level [0-10], most=10, least=0. + SpatialQuantizationOptions quantization_position{11}; + int quantization_bits_normal = 8; + int quantization_bits_tex_coord = 10; + int quantization_bits_color = 8; + int quantization_bits_generic = 8; + int quantization_bits_tangent = 8; + int quantization_bits_weight = 8; + bool find_non_degenerate_texture_quantization = false; + + bool operator==(const DracoCompressionOptions &other) const { + return compression_level == other.compression_level && + quantization_position == other.quantization_position && + quantization_bits_normal == other.quantization_bits_normal && + quantization_bits_tex_coord == other.quantization_bits_tex_coord && + quantization_bits_color == other.quantization_bits_color && + quantization_bits_generic == other.quantization_bits_generic && + quantization_bits_tangent == other.quantization_bits_tangent && + quantization_bits_weight == other.quantization_bits_weight && + find_non_degenerate_texture_quantization == + other.find_non_degenerate_texture_quantization; + } + + bool operator!=(const DracoCompressionOptions &other) const { + return !(*this == other); + } + + Status Check() const { + DRACO_RETURN_IF_ERROR( + Validate("Compression level", compression_level, 0, 10)); + if (quantization_position.AreQuantizationBitsDefined()) { + DRACO_RETURN_IF_ERROR(Validate("Position quantization", + quantization_position.quantization_bits(), + 0, 30)); + } else { + if (quantization_position.spacing() <= 0.f) { + return ErrorStatus("Position quantization spacing is invalid."); + } + } + DRACO_RETURN_IF_ERROR( + Validate("Normals quantization", quantization_bits_normal, 0, 30)); + DRACO_RETURN_IF_ERROR( + Validate("Tex coord quantization", quantization_bits_tex_coord, 0, 30)); + DRACO_RETURN_IF_ERROR( + Validate("Color quantization", quantization_bits_color, 0, 30)); + DRACO_RETURN_IF_ERROR( + Validate("Generic quantization", quantization_bits_generic, 0, 30)); + DRACO_RETURN_IF_ERROR( + Validate("Tangent quantization", quantization_bits_tangent, 0, 30)); + DRACO_RETURN_IF_ERROR( + Validate("Weights quantization", quantization_bits_weight, 0, 30)); + return OkStatus(); + } + + static Status Validate(const std::string &name, int value, int min, int max) { + if (value < min || value > max) { + const std::string range = + "[" + std::to_string(min) + "-" + std::to_string(max) + "]."; + return Status(Status::DRACO_ERROR, name + " is out of range " + range); + } + return OkStatus(); + } +}; + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED +#endif // DRACO_COMPRESSION_DRACO_COMPRESSION_OPTIONS_H_ diff --git a/contrib/draco/src/draco/compression/draco_compression_options_test.cc b/contrib/draco/src/draco/compression/draco_compression_options_test.cc new file mode 100644 index 000000000..415295211 --- /dev/null +++ b/contrib/draco/src/draco/compression/draco_compression_options_test.cc @@ -0,0 +1,45 @@ +#include "draco/compression/draco_compression_options.h" + +#include "draco/core/draco_test_utils.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED + +namespace { + +TEST(DracoCompressionOptionsTest, TestPositionQuantizationBits) { + // Test verifies that we can define draco compression options using + // quantization bits. + draco::SpatialQuantizationOptions options(10); + + // Quantization bits should be used by default. + ASSERT_TRUE(options.AreQuantizationBitsDefined()); + ASSERT_EQ(options.quantization_bits(), 10); + + // Change the quantization bits. + options.SetQuantizationBits(9); + ASSERT_TRUE(options.AreQuantizationBitsDefined()); + ASSERT_EQ(options.quantization_bits(), 9); + + // If we select the grid, quantization bits should not be used. + options.SetGrid(0.5f); + ASSERT_FALSE(options.AreQuantizationBitsDefined()); +} + +TEST(DracoCompressionOptionsTest, TestPositionQuantizationGrid) { + // Test verifies that we can define draco compression options using + // quantization grid. + draco::SpatialQuantizationOptions options(10); + + // Quantization bits should be used by default. + ASSERT_TRUE(options.AreQuantizationBitsDefined()); + + // Set the grid parameters. + options.SetGrid(0.25f); + ASSERT_FALSE(options.AreQuantizationBitsDefined()); + + ASSERT_EQ(options.spacing(), 0.25f); +} + +} // namespace + +#endif // DRACO_TRANSCODER_SUPPORTED diff --git a/contrib/draco/src/draco/compression/encode.h b/contrib/draco/src/draco/compression/encode.h index bce8b34c2..00ccb9b2e 100644 --- a/contrib/draco/src/draco/compression/encode.h +++ b/contrib/draco/src/draco/compression/encode.h @@ -129,7 +129,6 @@ class Encoder // call of EncodePointCloudToBuffer or EncodeMeshToBuffer is going to fail. void SetEncodingMethod(int encoding_method); - protected: // Creates encoder options for the expert encoder used during the actual // encoding. EncoderOptions CreateExpertEncoderOptions(const PointCloud &pc) const; diff --git a/contrib/draco/src/draco/compression/encode_base.h b/contrib/draco/src/draco/compression/encode_base.h index c501bc4fa..6211efc22 100644 --- a/contrib/draco/src/draco/compression/encode_base.h +++ b/contrib/draco/src/draco/compression/encode_base.h @@ -98,7 +98,7 @@ class EncoderBase { "Invalid prediction scheme for attribute type."); } } - // TODO(hemmer): Try to enable more prediction schemes for normals. + // TODO(b/199760123): Try to enable more prediction schemes for normals. if (att_type == GeometryAttribute::NORMAL) { if (!(prediction_scheme == PREDICTION_DIFFERENCE || prediction_scheme == MESH_PREDICTION_GEOMETRIC_NORMAL)) { diff --git a/contrib/draco/src/draco/compression/encode_test.cc b/contrib/draco/src/draco/compression/encode_test.cc index fde4f6f5b..00d834703 100644 --- a/contrib/draco/src/draco/compression/encode_test.cc +++ b/contrib/draco/src/draco/compression/encode_test.cc @@ -26,6 +26,7 @@ #include "draco/core/draco_test_base.h" #include "draco/core/draco_test_utils.h" #include "draco/core/vector_d.h" +#include "draco/io/file_utils.h" #include "draco/io/obj_decoder.h" #include "draco/mesh/triangle_soup_mesh_builder.h" #include "draco/point_cloud/point_cloud_builder.h" @@ -213,16 +214,14 @@ class EncodeTest : public ::testing::Test { draco::Decoder decoder; if (mesh) { - auto maybe_mesh = decoder.DecodeMeshFromBuffer(&decoder_buffer); - ASSERT_TRUE(maybe_mesh.ok()); - auto decoded_mesh = std::move(maybe_mesh).value(); + DRACO_ASSIGN_OR_ASSERT(auto decoded_mesh, + decoder.DecodeMeshFromBuffer(&decoder_buffer)); ASSERT_NE(decoded_mesh, nullptr); ASSERT_EQ(decoded_mesh->num_points(), encoder.num_encoded_points()); ASSERT_EQ(decoded_mesh->num_faces(), encoder.num_encoded_faces()); } else { - auto maybe_pc = decoder.DecodePointCloudFromBuffer(&decoder_buffer); - ASSERT_TRUE(maybe_pc.ok()); - auto decoded_pc = std::move(maybe_pc).value(); + DRACO_ASSIGN_OR_ASSERT( + auto decoded_pc, decoder.DecodePointCloudFromBuffer(&decoder_buffer)); ASSERT_EQ(decoded_pc->num_points(), encoder.num_encoded_points()); } } @@ -274,7 +273,7 @@ TEST_F(EncodeTest, TestLinesObj) { encoder.SetAttributeQuantization(draco::GeometryAttribute::POSITION, 16); draco::EncoderBuffer buffer; - ASSERT_TRUE(encoder.EncodePointCloudToBuffer(*pc, &buffer).ok()); + DRACO_ASSERT_OK(encoder.EncodePointCloudToBuffer(*pc, &buffer)); } TEST_F(EncodeTest, TestQuantizedInfinity) { @@ -315,7 +314,7 @@ TEST_F(EncodeTest, TestUnquantizedInfinity) { encoder.SetEncodingMethod(draco::POINT_CLOUD_SEQUENTIAL_ENCODING); draco::EncoderBuffer buffer; - ASSERT_TRUE(encoder.EncodePointCloudToBuffer(*pc, &buffer).ok()); + DRACO_ASSERT_OK(encoder.EncodePointCloudToBuffer(*pc, &buffer)); } TEST_F(EncodeTest, TestQuantizedAndUnquantizedAttributes) { @@ -330,7 +329,7 @@ TEST_F(EncodeTest, TestQuantizedAndUnquantizedAttributes) { encoder.SetAttributeQuantization(draco::GeometryAttribute::POSITION, 11); encoder.SetAttributeQuantization(draco::GeometryAttribute::NORMAL, 0); draco::EncoderBuffer buffer; - ASSERT_TRUE(encoder.EncodePointCloudToBuffer(*pc, &buffer).ok()); + DRACO_ASSERT_OK(encoder.EncodePointCloudToBuffer(*pc, &buffer)); } TEST_F(EncodeTest, TestKdTreeEncoding) { @@ -348,7 +347,7 @@ TEST_F(EncodeTest, TestKdTreeEncoding) { // Now set quantization for the position attribute which should make // the encoder happy. encoder.SetAttributeQuantization(draco::GeometryAttribute::POSITION, 16); - ASSERT_TRUE(encoder.EncodePointCloudToBuffer(*pc, &buffer).ok()); + DRACO_ASSERT_OK(encoder.EncodePointCloudToBuffer(*pc, &buffer)); } TEST_F(EncodeTest, TestTrackingOfNumberOfEncodedEntries) { @@ -373,7 +372,7 @@ TEST_F(EncodeTest, TestTrackingOfNumberOfEncodedEntriesNotSet) { draco::EncoderBuffer buffer; draco::Encoder encoder; - ASSERT_TRUE(encoder.EncodeMeshToBuffer(*mesh, &buffer).ok()); + DRACO_ASSERT_OK(encoder.EncodeMeshToBuffer(*mesh, &buffer)); ASSERT_EQ(encoder.num_encoded_points(), 0); ASSERT_EQ(encoder.num_encoded_faces(), 0); } @@ -404,4 +403,170 @@ TEST_F(EncodeTest, TestNoPosQuantizationNormalCoding) { ASSERT_NE(decoded_mesh, nullptr); } +#ifdef DRACO_TRANSCODER_SUPPORTED +TEST_F(EncodeTest, TestDracoCompressionOptions) { + // This test verifies that we can set the encoder's compression options via + // draco::Mesh's compression options. + const auto mesh = draco::ReadMeshFromTestFile("test_nm.obj"); + ASSERT_NE(mesh, nullptr); + + // First set compression level and quantization manually. + draco::Encoder encoder_manual; + draco::EncoderBuffer buffer_manual; + encoder_manual.SetAttributeQuantization(draco::GeometryAttribute::POSITION, + 8); + encoder_manual.SetAttributeQuantization(draco::GeometryAttribute::NORMAL, 7); + encoder_manual.SetSpeedOptions(4, 4); + + DRACO_ASSERT_OK(encoder_manual.EncodeMeshToBuffer(*mesh, &buffer_manual)); + + // Now do the same with options provided via DracoCompressionOptions. + draco::DracoCompressionOptions compression_options; + compression_options.compression_level = 6; + compression_options.quantization_position.SetQuantizationBits(8); + compression_options.quantization_bits_normal = 7; + mesh->SetCompressionOptions(compression_options); + mesh->SetCompressionEnabled(true); + + draco::Encoder encoder_auto; + draco::EncoderBuffer buffer_auto; + DRACO_ASSERT_OK(encoder_auto.EncodeMeshToBuffer(*mesh, &buffer_auto)); + + // Ensure that both encoders produce the same result. + ASSERT_EQ(buffer_manual.size(), buffer_auto.size()); + + // Now change some of the mesh's compression settings and ensure the + // compression changes as well. + compression_options.compression_level = 7; + mesh->SetCompressionOptions(compression_options); + buffer_auto.Clear(); + DRACO_ASSERT_OK(encoder_auto.EncodeMeshToBuffer(*mesh, &buffer_auto)); + ASSERT_NE(buffer_manual.size(), buffer_auto.size()); + + // Check that |mesh| compression options do not override the encoder options. + mesh->GetCompressionOptions().compression_level = 10; + mesh->GetCompressionOptions().quantization_position.SetQuantizationBits(10); + mesh->GetCompressionOptions().quantization_bits_normal = 10; + draco::EncoderBuffer buffer; + DRACO_ASSERT_OK(encoder_manual.EncodeMeshToBuffer(*mesh, &buffer)); + ASSERT_EQ(buffer.size(), buffer_manual.size()); +} + +TEST_F(EncodeTest, TestDracoCompressionOptionsManualOverride) { + // This test verifies that we can use encoder's option to override compression + // options provided in draco::Mesh's compression options. + const auto mesh = draco::ReadMeshFromTestFile("test_nm.obj"); + ASSERT_NE(mesh, nullptr); + + // Set some compression options. + draco::DracoCompressionOptions compression_options; + compression_options.compression_level = 6; + compression_options.quantization_position.SetQuantizationBits(8); + compression_options.quantization_bits_normal = 7; + mesh->SetCompressionOptions(compression_options); + mesh->SetCompressionEnabled(true); + + draco::Encoder encoder; + draco::EncoderBuffer buffer_no_override; + DRACO_ASSERT_OK(encoder.EncodeMeshToBuffer(*mesh, &buffer_no_override)); + + // Now override some options and ensure the compression is different. + encoder.SetAttributeQuantization(draco::GeometryAttribute::POSITION, 5); + draco::EncoderBuffer buffer_with_override; + DRACO_ASSERT_OK(encoder.EncodeMeshToBuffer(*mesh, &buffer_with_override)); + ASSERT_LT(buffer_with_override.size(), buffer_no_override.size()); +} + +TEST_F(EncodeTest, TestDracoCompressionOptionsGridQuantization) { + // Test verifies that we can set position quantization via grid spacing. + + // 1x1x1 cube. + const auto mesh = draco::ReadMeshFromTestFile("cube_att.obj"); + ASSERT_NE(mesh, nullptr); + mesh->SetCompressionEnabled(true); + + // Set grid quantization for positions. + draco::DracoCompressionOptions compression_options; + // This should result in 10x10x10 quantization. + compression_options.quantization_position.SetGrid(0.1); + mesh->SetCompressionOptions(compression_options); + + draco::ExpertEncoder encoder(*mesh); + draco::EncoderBuffer buffer; + DRACO_ASSERT_OK(encoder.EncodeToBuffer(&buffer)); + + // The grid options should be reflected in the |encoder|. Check that the + // computed values are correct. + const int pos_att_id = + mesh->GetNamedAttributeId(draco::GeometryAttribute::POSITION); + draco::Vector3f origin; + encoder.options().GetAttributeVector(pos_att_id, "quantization_origin", 3, + &origin[0]); + ASSERT_EQ(origin, draco::Vector3f(0.f, 0.f, 0.f)); + + // We need 4 quantization bits (for 10 values). + ASSERT_EQ( + encoder.options().GetAttributeInt(pos_att_id, "quantization_bits", -1), + 4); + + // The quantization range should be ((1 << quantization_bits) - 1) * spacing. + ASSERT_NEAR(encoder.options().GetAttributeFloat(pos_att_id, + "quantization_range", 0.f), + 15.f * 0.1f, 1e-6f); +} + +TEST_F(EncodeTest, TestDracoCompressionOptionsGridQuantizationWithOffset) { + // Test verifies that we can set position quantization via grid spacing when + // the geometry is not perfectly aligned with the quantization grid. + + // 1x1x1 cube. + const auto mesh = draco::ReadMeshFromTestFile("cube_att.obj"); + ASSERT_NE(mesh, nullptr); + + // Move all positions a bit. + auto *pos_att = mesh->attribute( + mesh->GetNamedAttributeId(draco::GeometryAttribute::POSITION)); + for (draco::AttributeValueIndex avi(0); avi < pos_att->size(); ++avi) { + draco::Vector3f pos; + pos_att->GetValue(avi, &pos[0]); + pos = pos + draco::Vector3f(-0.55f, 0.65f, 10.75f); + pos_att->SetAttributeValue(avi, &pos[0]); + } + + mesh->SetCompressionEnabled(true); + + // Set grid quantization for positions. + draco::DracoCompressionOptions compression_options; + // This should result in 16x16x16 quantization if the grid was perfectly + // aligned but since it is not we should expect 17 or 18 values per component. + compression_options.quantization_position.SetGrid(0.0625f); + mesh->SetCompressionOptions(compression_options); + + draco::ExpertEncoder encoder(*mesh); + draco::EncoderBuffer buffer; + DRACO_ASSERT_OK(encoder.EncodeToBuffer(&buffer)); + + // The grid options should be reflected in the |encoder|. Check that the + // computed values are correct. + const int pos_att_id = + mesh->GetNamedAttributeId(draco::GeometryAttribute::POSITION); + draco::Vector3f origin; + encoder.options().GetAttributeVector(pos_att_id, "quantization_origin", 3, + &origin[0]); + // The origin is the first lower value on the quantization grid for each + // component of the mesh. + ASSERT_EQ(origin, draco::Vector3f(-0.5625f, 0.625f, 10.75f)); + + // We need 5 quantization bits (for 17-18 values). + ASSERT_EQ( + encoder.options().GetAttributeInt(pos_att_id, "quantization_bits", -1), + 5); + + // The quantization range should be ((1 << quantization_bits) - 1) * spacing. + ASSERT_NEAR(encoder.options().GetAttributeFloat(pos_att_id, + "quantization_range", 0.f), + 31.f * 0.0625f, 1e-6f); +} +#endif // DRACO_TRANSCODER_SUPPORTED + } // namespace diff --git a/contrib/draco/src/draco/compression/entropy/ans.h b/contrib/draco/src/draco/compression/entropy/ans.h index c71d58975..313546fee 100644 --- a/contrib/draco/src/draco/compression/entropy/ans.h +++ b/contrib/draco/src/draco/compression/entropy/ans.h @@ -391,7 +391,6 @@ class RAnsEncoder { ans_.buf[ans_.buf_offset++] = ans_.state % DRACO_ANS_IO_BASE; ans_.state /= DRACO_ANS_IO_BASE; } - // TODO(ostava): The division and multiplication should be optimized. ans_.state = (ans_.state / p) * rans_precision + ans_.state % p + sym->cum_prob; } diff --git a/contrib/draco/src/draco/compression/entropy/rans_symbol_decoder.h b/contrib/draco/src/draco/compression/entropy/rans_symbol_decoder.h index 10cdc6781..3b408c079 100644 --- a/contrib/draco/src/draco/compression/entropy/rans_symbol_decoder.h +++ b/contrib/draco/src/draco/compression/entropy/rans_symbol_decoder.h @@ -75,6 +75,13 @@ bool RAnsSymbolDecoder::Create( return false; } } + // Check that decoded number of symbols is not unreasonably high. Remaining + // buffer size must be at least |num_symbols| / 64 bytes to contain the + // probability table. The |prob_data| below is one byte but it can be + // theoretically stored for each 64th symbol. + if (num_symbols_ / 64 > buffer->remaining_size()) { + return false; + } probability_table_.resize(num_symbols_); if (num_symbols_ == 0) { return true; diff --git a/contrib/draco/src/draco/compression/entropy/rans_symbol_encoder.h b/contrib/draco/src/draco/compression/entropy/rans_symbol_encoder.h index 4e07ec871..4b738b50a 100644 --- a/contrib/draco/src/draco/compression/entropy/rans_symbol_encoder.h +++ b/contrib/draco/src/draco/compression/entropy/rans_symbol_encoder.h @@ -125,8 +125,8 @@ bool RAnsSymbolEncoder::Create( for (int i = 0; i < num_symbols; ++i) { sorted_probabilities[i] = i; } - std::sort(sorted_probabilities.begin(), sorted_probabilities.end(), - ProbabilityLess(&probability_table_)); + std::stable_sort(sorted_probabilities.begin(), sorted_probabilities.end(), + ProbabilityLess(&probability_table_)); if (total_rans_prob < rans_precision_) { // This happens rather infrequently, just add the extra needed precision // to the most frequent symbol. diff --git a/contrib/draco/src/draco/compression/entropy/symbol_decoding.cc b/contrib/draco/src/draco/compression/entropy/symbol_decoding.cc index 93d29971c..79e811818 100644 --- a/contrib/draco/src/draco/compression/entropy/symbol_decoding.cc +++ b/contrib/draco/src/draco/compression/entropy/symbol_decoding.cc @@ -72,7 +72,7 @@ bool DecodeTaggedSymbols(uint32_t num_values, int num_components, int value_id = 0; for (uint32_t i = 0; i < num_values; i += num_components) { // Decode the tag. - const int bit_length = tag_decoder.DecodeSymbol(); + const uint32_t bit_length = tag_decoder.DecodeSymbol(); // Decode the actual value. for (int j = 0; j < num_components; ++j) { uint32_t val; diff --git a/contrib/draco/src/draco/compression/expert_encode.cc b/contrib/draco/src/draco/compression/expert_encode.cc index f9aec15eb..a3e649193 100644 --- a/contrib/draco/src/draco/compression/expert_encode.cc +++ b/contrib/draco/src/draco/compression/expert_encode.cc @@ -14,6 +14,12 @@ // #include "draco/compression/expert_encode.h" +#include +#include +#include +#include +#include + #include "draco/compression/mesh/mesh_edgebreaker_encoder.h" #include "draco/compression/mesh/mesh_sequential_encoder.h" #ifdef DRACO_POINT_CLOUD_COMPRESSION_SUPPORTED @@ -21,6 +27,9 @@ #include "draco/compression/point_cloud/point_cloud_sequential_encoder.h" #endif +#ifdef DRACO_TRANSCODER_SUPPORTED +#include "draco/core/bit_utils.h" +#endif namespace draco { ExpertEncoder::ExpertEncoder(const PointCloud &point_cloud) @@ -101,6 +110,11 @@ Status ExpertEncoder::EncodePointCloudToBuffer(const PointCloud &pc, Status ExpertEncoder::EncodeMeshToBuffer(const Mesh &m, EncoderBuffer *out_buffer) { +#ifdef DRACO_TRANSCODER_SUPPORTED + // Apply DracoCompressionOptions associated with the mesh. + DRACO_RETURN_IF_ERROR(ApplyCompressionOptions(m)); +#endif // DRACO_TRANSCODER_SUPPORTED + std::unique_ptr encoder; // Select the encoding method only based on the provided options. int encoding_method = options().GetGlobalInt("encoding_method", -1); @@ -118,6 +132,7 @@ Status ExpertEncoder::EncodeMeshToBuffer(const Mesh &m, encoder = std::unique_ptr(new MeshSequentialEncoder()); } encoder->SetMesh(m); + DRACO_RETURN_IF_ERROR(encoder->Encode(options(), out_buffer)); set_num_encoded_points(encoder->num_encoded_points()); @@ -179,4 +194,107 @@ Status ExpertEncoder::SetAttributePredictionScheme( return status; } +#ifdef DRACO_TRANSCODER_SUPPORTED +Status ExpertEncoder::ApplyCompressionOptions(const Mesh &mesh) { + if (!mesh.IsCompressionEnabled()) { + return OkStatus(); + } + const auto &compression_options = mesh.GetCompressionOptions(); + + // Set any encoder options that haven't been explicitly set by users (don't + // override existing options). + if (!options().IsSpeedSet()) { + options().SetSpeed(10 - compression_options.compression_level, + 10 - compression_options.compression_level); + } + + for (int ai = 0; ai < mesh.num_attributes(); ++ai) { + if (options().IsAttributeOptionSet(ai, "quantization_bits")) { + continue; // Don't override options that have been set. + } + int quantization_bits = 0; + const auto type = mesh.attribute(ai)->attribute_type(); + switch (type) { + case GeometryAttribute::POSITION: + if (compression_options.quantization_position + .AreQuantizationBitsDefined()) { + quantization_bits = + compression_options.quantization_position.quantization_bits(); + } else { + DRACO_RETURN_IF_ERROR(ApplyGridQuantization(mesh, ai)); + } + break; + case GeometryAttribute::TEX_COORD: + quantization_bits = compression_options.quantization_bits_tex_coord; + break; + case GeometryAttribute::NORMAL: + quantization_bits = compression_options.quantization_bits_normal; + break; + case GeometryAttribute::COLOR: + quantization_bits = compression_options.quantization_bits_color; + break; + case GeometryAttribute::TANGENT: + quantization_bits = compression_options.quantization_bits_tangent; + break; + case GeometryAttribute::WEIGHTS: + quantization_bits = compression_options.quantization_bits_weight; + break; + case GeometryAttribute::GENERIC: + quantization_bits = compression_options.quantization_bits_generic; + break; + default: + break; + } + if (quantization_bits > 0) { + options().SetAttributeInt(ai, "quantization_bits", quantization_bits); + } + } + return OkStatus(); +} + +Status ExpertEncoder::ApplyGridQuantization(const Mesh &mesh, + int attribute_index) { + const auto compression_options = mesh.GetCompressionOptions(); + if (mesh.attribute(attribute_index)->num_components() != 3) { + return ErrorStatus( + "Invalid number of components: Grid quantization is currently " + "supported only for 3D positions."); + } + const float spacing = compression_options.quantization_position.spacing(); + // Compute quantization properties based on the grid spacing. + const auto &bbox = mesh.ComputeBoundingBox(); + // Snap min and max points of the |bbox| to the quantization grid vertices. + Vector3f min_pos; + int num_values = 0; // Number of values that we need to encode. + for (int c = 0; c < 3; ++c) { + // Min / max position on grid vertices in grid coordinates. + const float min_grid_pos = floor(bbox.GetMinPoint()[c] / spacing); + const float max_grid_pos = ceil(bbox.GetMaxPoint()[c] / spacing); + + // Min pos on grid vertex in mesh coordinates. + min_pos[c] = min_grid_pos * spacing; + + const float component_num_values = + static_cast(max_grid_pos) - static_cast(min_grid_pos) + 1; + if (component_num_values > num_values) { + num_values = component_num_values; + } + } + // Now compute the number of bits needed to encode |num_values|. + int bits = MostSignificantBit(num_values); + if ((1 << bits) < num_values) { + // If the |num_values| is larger than number of values representable by + // |bits|, we need to use one more bit. This will be almost always true + // unless |num_values| was equal to 1 << |bits|. + bits++; + } + // Compute the range in mesh coordinates that matches the quantization bits. + // Note there are n-1 intervals between the |n| quantization values. + const float range = ((1 << bits) - 1) * spacing; + SetAttributeExplicitQuantization(attribute_index, bits, 3, min_pos.data(), + range); + return OkStatus(); +} +#endif // DRACO_TRANSCODER_SUPPORTED + } // namespace draco diff --git a/contrib/draco/src/draco/compression/expert_encode.h b/contrib/draco/src/draco/compression/expert_encode.h index ea59393d3..5c1485e1e 100644 --- a/contrib/draco/src/draco/compression/expert_encode.h +++ b/contrib/draco/src/draco/compression/expert_encode.h @@ -138,6 +138,12 @@ class ExpertEncoder : public EncoderBase { Status EncodeMeshToBuffer(const Mesh &m, EncoderBuffer *out_buffer); +#ifdef DRACO_TRANSCODER_SUPPORTED + // Applies compression options stored in |mesh|. + Status ApplyCompressionOptions(const Mesh &mesh); + Status ApplyGridQuantization(const Mesh &mesh, int attribute_index); +#endif // DRACO_TRANSCODER_SUPPORTED + const PointCloud *point_cloud_; const Mesh *mesh_; }; diff --git a/contrib/draco/src/draco/compression/mesh/mesh_edgebreaker_decoder_impl.cc b/contrib/draco/src/draco/compression/mesh/mesh_edgebreaker_decoder_impl.cc index 0bbbea4af..21ad9959c 100644 --- a/contrib/draco/src/draco/compression/mesh/mesh_edgebreaker_decoder_impl.cc +++ b/contrib/draco/src/draco/compression/mesh/mesh_edgebreaker_decoder_impl.cc @@ -454,7 +454,7 @@ bool MeshEdgebreakerDecoderImpl::DecodeConnectivity() { #endif // Decode connectivity of non-position attributes. - if (attribute_data_.size() > 0) { + if (!attribute_data_.empty()) { #ifdef DRACO_BACKWARDS_COMPATIBILITY_SUPPORTED if (decoder_->bitstream_version() < DRACO_BITSTREAM_VERSION(2, 1)) { for (CornerIndex ci(0); ci < corner_table_->num_corners(); ci += 3) { @@ -484,7 +484,10 @@ bool MeshEdgebreakerDecoderImpl::DecodeConnectivity() { attribute_data_[i].connectivity_data.AddSeamEdge(CornerIndex(c)); } // Recompute vertices from the newly added seam edges. - attribute_data_[i].connectivity_data.RecomputeVertices(nullptr, nullptr); + if (!attribute_data_[i].connectivity_data.RecomputeVertices(nullptr, + nullptr)) { + return false; + } } pos_encoding_data_.Init(corner_table_->num_vertices()); @@ -574,6 +577,17 @@ int MeshEdgebreakerDecoderImpl::DecodeConnectivity( const CornerIndex corner_b = corner_table_->Next(corner_table_->LeftMostCorner(vertex_x)); + if (corner_a == corner_b) { + // All matched corners must be different. + return -1; + } + if (corner_table_->Opposite(corner_a) != kInvalidCornerIndex || + corner_table_->Opposite(corner_b) != kInvalidCornerIndex) { + // One of the corners is already opposite to an existing face, which + // should not happen unless the input was tampered with. + return -1; + } + // New tip corner. const CornerIndex corner(3 * face.value()); // Update opposite corner mappings. @@ -616,6 +630,11 @@ int MeshEdgebreakerDecoderImpl::DecodeConnectivity( return -1; } const CornerIndex corner_a = active_corner_stack.back(); + if (corner_table_->Opposite(corner_a) != kInvalidCornerIndex) { + // Active corner is already opposite to an existing face, which should + // not happen unless the input was tampered with. + return -1; + } // First corner on the new face is either corner "l" or "r". const CornerIndex corner(3 * face.value()); @@ -681,10 +700,14 @@ int MeshEdgebreakerDecoderImpl::DecodeConnectivity( } const CornerIndex corner_a = active_corner_stack.back(); + if (corner_a == corner_b) { + // All matched corners must be different. + return -1; + } if (corner_table_->Opposite(corner_a) != kInvalidCornerIndex || corner_table_->Opposite(corner_b) != kInvalidCornerIndex) { // One of the corners is already opposite to an existing face, which - // should not happen unless the input was tempered with. + // should not happen unless the input was tampered with. return -1; } @@ -713,9 +736,15 @@ int MeshEdgebreakerDecoderImpl::DecodeConnectivity( // Also update the vertex id at corner "n" and all corners that are // connected to it in the CCW direction. + const CornerIndex first_corner = corner_n; while (corner_n != kInvalidCornerIndex) { corner_table_->MapCornerToVertex(corner_n, vertex_p); corner_n = corner_table_->SwingLeft(corner_n); + if (corner_n == first_corner) { + // We reached the start again which should not happen for split + // symbols. + return -1; + } } // Make sure the old vertex n is now mapped to an invalid corner (make it // isolated). @@ -800,7 +829,7 @@ int MeshEdgebreakerDecoderImpl::DecodeConnectivity( return -1; // Unexpected number of decoded vertices. } // Decode start faces and connect them to the faces from the active stack. - while (active_corner_stack.size() > 0) { + while (!active_corner_stack.empty()) { const CornerIndex corner = active_corner_stack.back(); active_corner_stack.pop_back(); const bool interior_face = @@ -842,6 +871,18 @@ int MeshEdgebreakerDecoderImpl::DecodeConnectivity( const CornerIndex corner_c = corner_table_->Next(corner_table_->LeftMostCorner(vert_x)); + if (corner == corner_b || corner == corner_c || corner_b == corner_c) { + // All matched corners must be different. + return -1; + } + if (corner_table_->Opposite(corner) != kInvalidCornerIndex || + corner_table_->Opposite(corner_b) != kInvalidCornerIndex || + corner_table_->Opposite(corner_c) != kInvalidCornerIndex) { + // One of the corners is already opposite to an existing face, which + // should not happen unless the input was tampered with. + return -1; + } + const VertexIndex vert_p = corner_table_->Vertex(corner_table_->Next(corner_c)); @@ -894,6 +935,11 @@ int MeshEdgebreakerDecoderImpl::DecodeConnectivity( VertexCornersIterator vcit(corner_table_.get(), src_vert); for (; !vcit.End(); ++vcit) { const CornerIndex cid = vcit.Corner(); + if (corner_table_->Vertex(cid) != src_vert) { + // Vertex mapped to |cid| was not |src_vert|. This indicates corrupted + // data and we should terminate the decoding. + return -1; + } corner_table_->MapCornerToVertex(cid, invalid_vert); } corner_table_->SetLeftMostCorner(invalid_vert, diff --git a/contrib/draco/src/draco/compression/mesh/mesh_edgebreaker_encoder.cc b/contrib/draco/src/draco/compression/mesh/mesh_edgebreaker_encoder.cc index 5aff5d8cc..a7f381480 100644 --- a/contrib/draco/src/draco/compression/mesh/mesh_edgebreaker_encoder.cc +++ b/contrib/draco/src/draco/compression/mesh/mesh_edgebreaker_encoder.cc @@ -31,7 +31,6 @@ bool MeshEdgebreakerEncoder::InitializeEncoder() { impl_ = nullptr; // For tiny meshes it's usually better to use the basic edgebreaker as the // overhead of the predictive one may turn out to be too big. - // TODO(b/111065939): Check if this can be improved. const bool is_tiny_mesh = mesh()->num_faces() < 1000; int selected_edgebreaker_method = diff --git a/contrib/draco/src/draco/compression/mesh/mesh_edgebreaker_encoder_impl.cc b/contrib/draco/src/draco/compression/mesh/mesh_edgebreaker_encoder_impl.cc index 0791dc670..4bf6aa920 100644 --- a/contrib/draco/src/draco/compression/mesh/mesh_edgebreaker_encoder_impl.cc +++ b/contrib/draco/src/draco/compression/mesh/mesh_edgebreaker_encoder_impl.cc @@ -408,7 +408,7 @@ Status MeshEdgebreakerEncoderImpl::EncodeConnectivity() { init_face_connectivity_corners.begin(), init_face_connectivity_corners.end()); // Encode connectivity for all non-position attributes. - if (attribute_data_.size() > 0) { + if (!attribute_data_.empty()) { // Use the same order of corner that will be used by the decoder. visited_faces_.assign(mesh_->num_faces(), false); for (CornerIndex ci : processed_connectivity_corners_) { diff --git a/contrib/draco/src/draco/compression/mesh/mesh_edgebreaker_encoder_impl.h b/contrib/draco/src/draco/compression/mesh/mesh_edgebreaker_encoder_impl.h index fb3377163..979e1d373 100644 --- a/contrib/draco/src/draco/compression/mesh/mesh_edgebreaker_encoder_impl.h +++ b/contrib/draco/src/draco/compression/mesh/mesh_edgebreaker_encoder_impl.h @@ -177,7 +177,6 @@ class MeshEdgebreakerEncoderImpl : public MeshEdgebreakerEncoderImplInterface { uint32_t num_split_symbols_; // Struct holding data used for encoding each non-position attribute. - // TODO(ostava): This should be probably renamed to something better. struct AttributeData { AttributeData() : attribute_index(-1), is_connectivity_used(true) {} int attribute_index; diff --git a/contrib/draco/src/draco/compression/mesh/mesh_edgebreaker_encoding_test.cc b/contrib/draco/src/draco/compression/mesh/mesh_edgebreaker_encoding_test.cc index 831388245..523303b09 100644 --- a/contrib/draco/src/draco/compression/mesh/mesh_edgebreaker_encoding_test.cc +++ b/contrib/draco/src/draco/compression/mesh/mesh_edgebreaker_encoding_test.cc @@ -44,7 +44,7 @@ class MeshEdgebreakerEncodingTest : public ::testing::Test { EncoderOptions encoder_options = EncoderOptions::CreateDefaultOptions(); encoder_options.SetSpeed(10 - compression_level, 10 - compression_level); encoder.SetMesh(*mesh); - ASSERT_TRUE(encoder.Encode(encoder_options, &buffer).ok()); + DRACO_ASSERT_OK(encoder.Encode(encoder_options, &buffer)); DecoderBuffer dec_buffer; dec_buffer.Init(buffer.data(), buffer.size()); @@ -52,15 +52,14 @@ class MeshEdgebreakerEncodingTest : public ::testing::Test { std::unique_ptr decoded_mesh(new Mesh()); DecoderOptions dec_options; - ASSERT_TRUE( - decoder.Decode(dec_options, &dec_buffer, decoded_mesh.get()).ok()); + DRACO_ASSERT_OK( + decoder.Decode(dec_options, &dec_buffer, decoded_mesh.get())); // Cleanup the input mesh to make sure that input and output can be // compared (edgebreaker method discards degenerated triangles and isolated // vertices). const MeshCleanupOptions options; - MeshCleanup cleanup; - ASSERT_TRUE(cleanup(mesh, options)) << "Failed to clean the input mesh."; + DRACO_ASSERT_OK(MeshCleanup::Cleanup(mesh, options)); MeshAreEquivalent eq; ASSERT_TRUE(eq(*mesh, *decoded_mesh.get())) @@ -102,8 +101,8 @@ TEST_F(MeshEdgebreakerEncodingTest, TestEncoderReuse) { EncoderOptions encoder_options = EncoderOptions::CreateDefaultOptions(); encoder.SetMesh(*mesh); EncoderBuffer buffer_0, buffer_1; - ASSERT_TRUE(encoder.Encode(encoder_options, &buffer_0).ok()); - ASSERT_TRUE(encoder.Encode(encoder_options, &buffer_1).ok()); + DRACO_ASSERT_OK(encoder.Encode(encoder_options, &buffer_0)); + DRACO_ASSERT_OK(encoder.Encode(encoder_options, &buffer_1)); // Make sure both buffer are identical. ASSERT_EQ(buffer_0.size(), buffer_1.size()); @@ -123,7 +122,7 @@ TEST_F(MeshEdgebreakerEncodingTest, TestDecoderReuse) { EncoderOptions encoder_options = EncoderOptions::CreateDefaultOptions(); encoder.SetMesh(*mesh); EncoderBuffer buffer; - ASSERT_TRUE(encoder.Encode(encoder_options, &buffer).ok()); + DRACO_ASSERT_OK(encoder.Encode(encoder_options, &buffer)); DecoderBuffer dec_buffer; dec_buffer.Init(buffer.data(), buffer.size()); @@ -133,13 +132,13 @@ TEST_F(MeshEdgebreakerEncodingTest, TestDecoderReuse) { // Decode the mesh two times. std::unique_ptr decoded_mesh_0(new Mesh()); DecoderOptions dec_options; - ASSERT_TRUE( - decoder.Decode(dec_options, &dec_buffer, decoded_mesh_0.get()).ok()); + DRACO_ASSERT_OK( + decoder.Decode(dec_options, &dec_buffer, decoded_mesh_0.get())); dec_buffer.Init(buffer.data(), buffer.size()); std::unique_ptr decoded_mesh_1(new Mesh()); - ASSERT_TRUE( - decoder.Decode(dec_options, &dec_buffer, decoded_mesh_1.get()).ok()); + DRACO_ASSERT_OK( + decoder.Decode(dec_options, &dec_buffer, decoded_mesh_1.get())); // Make sure both of the meshes are identical. MeshAreEquivalent eq; @@ -169,7 +168,7 @@ TEST_F(MeshEdgebreakerEncodingTest, TestSingleConnectivityEncoding) { encoder.SetAttributeQuantization(GeometryAttribute::TEX_COORD, 8); encoder.SetAttributeQuantization(GeometryAttribute::NORMAL, 8); encoder.SetEncodingMethod(MESH_EDGEBREAKER_ENCODING); - ASSERT_TRUE(encoder.EncodeMeshToBuffer(*mesh, &buffer).ok()); + DRACO_ASSERT_OK(encoder.EncodeMeshToBuffer(*mesh, &buffer)); DecoderBuffer dec_buffer; dec_buffer.Init(buffer.data(), buffer.size()); @@ -216,7 +215,7 @@ TEST_F(MeshEdgebreakerEncodingTest, TestWrongAttributeOrder) { encoder.SetAttributeQuantization(GeometryAttribute::POSITION, 8); encoder.SetAttributeQuantization(GeometryAttribute::NORMAL, 8); encoder.SetEncodingMethod(MESH_EDGEBREAKER_ENCODING); - ASSERT_TRUE(encoder.EncodeMeshToBuffer(*mesh, &buffer).ok()); + DRACO_ASSERT_OK(encoder.EncodeMeshToBuffer(*mesh, &buffer)); DecoderBuffer dec_buffer; dec_buffer.Init(buffer.data(), buffer.size()); diff --git a/contrib/draco/src/draco/compression/mesh/mesh_edgebreaker_shared.h b/contrib/draco/src/draco/compression/mesh/mesh_edgebreaker_shared.h index cb3c29dd6..c650bc352 100644 --- a/contrib/draco/src/draco/compression/mesh/mesh_edgebreaker_shared.h +++ b/contrib/draco/src/draco/compression/mesh/mesh_edgebreaker_shared.h @@ -50,8 +50,6 @@ namespace draco { // \ / S \ / / E \ // *-------* *-------* // -// TODO(ostava): Get rid of the topology bit pattern. It's important only for -// encoding but the algorithms should use EdgebreakerSymbol instead. enum EdgebreakerTopologyBitPattern { TOPOLOGY_C = 0x0, // 0 TOPOLOGY_S = 0x1, // 1 0 0 diff --git a/contrib/draco/src/draco/compression/mesh/mesh_edgebreaker_traversal_valence_decoder.h b/contrib/draco/src/draco/compression/mesh/mesh_edgebreaker_traversal_valence_decoder.h index c00373727..89553e909 100644 --- a/contrib/draco/src/draco/compression/mesh/mesh_edgebreaker_traversal_valence_decoder.h +++ b/contrib/draco/src/draco/compression/mesh/mesh_edgebreaker_traversal_valence_decoder.h @@ -129,7 +129,11 @@ class MeshEdgebreakerTraversalValenceDecoder if (context_counter < 0) { return TOPOLOGY_INVALID; } - const int symbol_id = context_symbols_[active_context_][context_counter]; + const uint32_t symbol_id = + context_symbols_[active_context_][context_counter]; + if (symbol_id > 4) { + return TOPOLOGY_INVALID; + } last_symbol_ = edge_breaker_symbol_to_topology_id[symbol_id]; } else { #ifdef DRACO_BACKWARDS_COMPATIBILITY_SUPPORTED diff --git a/contrib/draco/src/draco/compression/mesh/mesh_encoder_test.cc b/contrib/draco/src/draco/compression/mesh/mesh_encoder_test.cc index 55f683696..2dfdb58ef 100644 --- a/contrib/draco/src/draco/compression/mesh/mesh_encoder_test.cc +++ b/contrib/draco/src/draco/compression/mesh/mesh_encoder_test.cc @@ -78,9 +78,10 @@ class MeshEncoderTest : public ::testing::TestWithParam { encoder.SetAttributeQuantization(i, 12); } EncoderBuffer buffer; - ASSERT_TRUE(encoder.EncodeToBuffer(&buffer).ok()) - << "Failed encoding test mesh " << file_name << " with method " - << GetParam().encoding_method; + const Status status = encoder.EncodeToBuffer(&buffer); + EXPECT_TRUE(status.ok()) << "Failed encoding test mesh " << file_name + << " with method " << GetParam().encoding_method; + DRACO_ASSERT_OK(status); // Check that the encoded mesh was really encoded with the selected method. DecoderBuffer decoder_buffer; decoder_buffer.Init(buffer.data(), buffer.size()); @@ -88,6 +89,7 @@ class MeshEncoderTest : public ::testing::TestWithParam { uint8_t encoded_method; ASSERT_TRUE(decoder_buffer.Decode(&encoded_method)); ASSERT_EQ(encoded_method, method); + if (!FLAGS_update_golden_files) { EXPECT_TRUE( CompareGoldenFile(golden_file_name, buffer.data(), buffer.size())) diff --git a/contrib/draco/src/draco/compression/mesh/mesh_sequential_decoder.cc b/contrib/draco/src/draco/compression/mesh/mesh_sequential_decoder.cc index be349f543..595a487a4 100644 --- a/contrib/draco/src/draco/compression/mesh/mesh_sequential_decoder.cc +++ b/contrib/draco/src/draco/compression/mesh/mesh_sequential_decoder.cc @@ -96,7 +96,7 @@ bool MeshSequentialDecoder::DecodeConnectivity() { } mesh()->AddFace(face); } - } else if (mesh()->num_points() < (1 << 21) && + } else if (num_points < (1 << 21) && bitstream_version() >= DRACO_BITSTREAM_VERSION(2, 2)) { // Decode indices as uint32_t. for (uint32_t i = 0; i < num_faces; ++i) { @@ -158,6 +158,10 @@ bool MeshSequentialDecoder::DecodeAndDecompressIndices(uint32_t num_faces) { index_diff = -index_diff; } const int32_t index_value = index_diff + last_index_value; + if (index_value < 0) { + // Negative indices are not allowed. + return false; + } face[j] = index_value; last_index_value = index_value; } diff --git a/contrib/draco/src/draco/compression/mesh/mesh_sequential_encoder.cc b/contrib/draco/src/draco/compression/mesh/mesh_sequential_encoder.cc index 02ac7779e..fd8b11392 100644 --- a/contrib/draco/src/draco/compression/mesh/mesh_sequential_encoder.cc +++ b/contrib/draco/src/draco/compression/mesh/mesh_sequential_encoder.cc @@ -32,8 +32,6 @@ Status MeshSequentialEncoder::EncodeConnectivity() { EncodeVarint(static_cast(mesh()->num_points()), buffer()); // We encode all attributes in the original (possibly duplicated) format. - // TODO(ostava): This may not be optimal if we have only one attribute or if - // all attributes share the same index mapping. if (options()->GetGlobalBool("compress_connectivity", false)) { // 0 = Encode compressed indices. buffer()->Encode(static_cast(0)); @@ -44,8 +42,6 @@ Status MeshSequentialEncoder::EncodeConnectivity() { // 1 = Encode indices directly. buffer()->Encode(static_cast(1)); // Store vertex indices using a smallest data type that fits their range. - // TODO(ostava): This can be potentially improved by using a tighter - // fit that is not bound by a bit-length of any particular data type. if (mesh()->num_points() < 256) { // Serialize indices as uint8_t. for (FaceIndex i(0); i < num_faces; ++i) { diff --git a/contrib/draco/src/draco/compression/mesh/mesh_sequential_encoder.h b/contrib/draco/src/draco/compression/mesh/mesh_sequential_encoder.h index 672609642..6e2b05877 100644 --- a/contrib/draco/src/draco/compression/mesh/mesh_sequential_encoder.h +++ b/contrib/draco/src/draco/compression/mesh/mesh_sequential_encoder.h @@ -33,7 +33,6 @@ namespace draco { // Class that encodes mesh data using a simple binary representation of mesh's // connectivity and geometry. -// TODO(ostava): Use a better name. class MeshSequentialEncoder : public MeshEncoder { public: MeshSequentialEncoder(); diff --git a/contrib/draco/src/draco/compression/mesh/traverser/mesh_attribute_indices_encoding_observer.h b/contrib/draco/src/draco/compression/mesh/traverser/mesh_attribute_indices_encoding_observer.h index e66dd14b2..dd9738ba2 100644 --- a/contrib/draco/src/draco/compression/mesh/traverser/mesh_attribute_indices_encoding_observer.h +++ b/contrib/draco/src/draco/compression/mesh/traverser/mesh_attribute_indices_encoding_observer.h @@ -25,7 +25,7 @@ namespace draco { // values based on the traversal of the encoded mesh. The class should be used // as the TraversalObserverT member of a Traverser class such as the // DepthFirstTraverser (depth_first_traverser.h). -// TODO(hemmer): rename to AttributeIndicesCodingTraverserObserver +// TODO(b/199760123): Rename to AttributeIndicesCodingTraverserObserver. template class MeshAttributeIndicesEncodingObserver { public: diff --git a/contrib/draco/src/draco/compression/mesh/traverser/mesh_traversal_sequencer.h b/contrib/draco/src/draco/compression/mesh/traverser/mesh_traversal_sequencer.h index ebe1d5f7a..e55c93a79 100644 --- a/contrib/draco/src/draco/compression/mesh/traverser/mesh_traversal_sequencer.h +++ b/contrib/draco/src/draco/compression/mesh/traverser/mesh_traversal_sequencer.h @@ -25,7 +25,7 @@ namespace draco { // Sequencer that generates point sequence in an order given by a deterministic // traversal on the mesh surface. Note that all attributes encoded with this // sequence must share the same connectivity. -// TODO(hemmer): Consider refactoring such that this is an observer. +// TODO(b/199760123): Consider refactoring such that this is an observer. template class MeshTraversalSequencer : public PointsSequencer { public: diff --git a/contrib/draco/src/draco/compression/point_cloud/algorithms/dynamic_integer_points_kd_tree_decoder.h b/contrib/draco/src/draco/compression/point_cloud/algorithms/dynamic_integer_points_kd_tree_decoder.h index 87bc2b7ef..55bafe7c4 100644 --- a/contrib/draco/src/draco/compression/point_cloud/algorithms/dynamic_integer_points_kd_tree_decoder.h +++ b/contrib/draco/src/draco/compression/point_cloud/algorithms/dynamic_integer_points_kd_tree_decoder.h @@ -18,8 +18,10 @@ #define DRACO_COMPRESSION_POINT_CLOUD_ALGORITHMS_DYNAMIC_INTEGER_POINTS_KD_TREE_DECODER_H_ #include +#include #include #include +#include #include "draco/compression/bit_coders/adaptive_rans_bit_decoder.h" #include "draco/compression/bit_coders/direct_bit_decoder.h" @@ -92,17 +94,29 @@ class DynamicIntegerPointsKdTreeDecoder { base_stack_(32 * dimension + 1, VectorUint32(dimension, 0)), levels_stack_(32 * dimension + 1, VectorUint32(dimension, 0)) {} - // Decodes a integer point cloud from |buffer|. + // Decodes an integer point cloud from |buffer|. Optional |oit_max_points| can + // be used to tell the decoder the maximum number of points accepted by the + // iterator. template bool DecodePoints(DecoderBuffer *buffer, OutputIteratorT &oit); + template + bool DecodePoints(DecoderBuffer *buffer, OutputIteratorT &oit, + uint32_t oit_max_points); + #ifndef DRACO_OLD_GCC template bool DecodePoints(DecoderBuffer *buffer, OutputIteratorT &&oit); + template + bool DecodePoints(DecoderBuffer *buffer, OutputIteratorT &&oit, + uint32_t oit_max_points); #endif // DRACO_OLD_GCC const uint32_t dimension() const { return dimension_; } + // Returns the number of decoded points. Must be called after DecodePoints(). + uint32_t num_decoded_points() const { return num_decoded_points_; } + private: uint32_t GetAxis(uint32_t num_remaining_points, const VectorUint32 &levels, uint32_t last_axis); @@ -146,8 +160,15 @@ template template bool DynamicIntegerPointsKdTreeDecoder::DecodePoints( DecoderBuffer *buffer, OutputIteratorT &&oit) { + return DecodePoints(buffer, oit, std::numeric_limits::max()); +} + +template +template +bool DynamicIntegerPointsKdTreeDecoder::DecodePoints( + DecoderBuffer *buffer, OutputIteratorT &&oit, uint32_t oit_max_points) { OutputIteratorT local = std::forward(oit); - return DecodePoints(buffer, local); + return DecodePoints(buffer, local, oit_max_points); } #endif // DRACO_OLD_GCC @@ -155,6 +176,13 @@ template template bool DynamicIntegerPointsKdTreeDecoder::DecodePoints( DecoderBuffer *buffer, OutputIteratorT &oit) { + return DecodePoints(buffer, oit, std::numeric_limits::max()); +} + +template +template +bool DynamicIntegerPointsKdTreeDecoder::DecodePoints( + DecoderBuffer *buffer, OutputIteratorT &oit, uint32_t oit_max_points) { if (!buffer->Decode(&bit_length_)) { return false; } @@ -167,6 +195,9 @@ bool DynamicIntegerPointsKdTreeDecoder::DecodePoints( if (num_points_ == 0) { return true; } + if (num_points_ > oit_max_points) { + return false; + } num_decoded_points_ = 0; if (!numbers_decoder_.StartDecoding(buffer)) { @@ -227,7 +258,7 @@ bool DynamicIntegerPointsKdTreeDecoder::DecodeInternal( std::stack status_stack; status_stack.push(init_status); - // TODO(hemmer): use preallocated vector instead of stack. + // TODO(b/199760123): Use preallocated vector instead of stack. while (!status_stack.empty()) { const DecodingStatus status = status_stack.top(); status_stack.pop(); @@ -263,7 +294,8 @@ bool DynamicIntegerPointsKdTreeDecoder::DecodeInternal( // Fast decoding of remaining bits if number of points is 1 or 2. if (num_remaining_points <= 2) { - // TODO(hemmer): axes_ not necessary, remove would change bitstream! + // TODO(b/199760123): |axes_| not necessary, remove would change + // bitstream! axes_[0] = axis; for (uint32_t i = 1; i < dimension_; i++) { axes_[i] = DRACO_INCREMENT_MOD(axes_[i - 1], dimension_); @@ -273,8 +305,10 @@ bool DynamicIntegerPointsKdTreeDecoder::DecodeInternal( p_[axes_[j]] = 0; const uint32_t num_remaining_bits = bit_length_ - levels[axes_[j]]; if (num_remaining_bits) { - remaining_bits_decoder_.DecodeLeastSignificantBits32( - num_remaining_bits, &p_[axes_[j]]); + if (!remaining_bits_decoder_.DecodeLeastSignificantBits32( + num_remaining_bits, &p_[axes_[j]])) { + return false; + } } p_[axes_[j]] = old_base[axes_[j]] | p_[axes_[j]]; } @@ -299,7 +333,12 @@ bool DynamicIntegerPointsKdTreeDecoder::DecodeInternal( uint32_t number = 0; DecodeNumber(incoming_bits, &number); - uint32_t first_half = num_remaining_points / 2 - number; + uint32_t first_half = num_remaining_points / 2; + if (first_half < number) { + // Invalid |number|. + return false; + } + first_half -= number; uint32_t second_half = num_remaining_points - first_half; if (first_half != second_half) { diff --git a/contrib/draco/src/draco/compression/point_cloud/algorithms/dynamic_integer_points_kd_tree_encoder.h b/contrib/draco/src/draco/compression/point_cloud/algorithms/dynamic_integer_points_kd_tree_encoder.h index 14fa32d70..65b3d07a6 100644 --- a/contrib/draco/src/draco/compression/point_cloud/algorithms/dynamic_integer_points_kd_tree_encoder.h +++ b/contrib/draco/src/draco/compression/point_cloud/algorithms/dynamic_integer_points_kd_tree_encoder.h @@ -280,7 +280,7 @@ void DynamicIntegerPointsKdTreeEncoder::EncodeInternal( std::stack status_stack; status_stack.push(init_status); - // TODO(hemmer): use preallocated vector instead of stack. + // TODO(b/199760123): Use preallocated vector instead of stack. while (!status_stack.empty()) { Status status = status_stack.top(); status_stack.pop(); @@ -305,7 +305,8 @@ void DynamicIntegerPointsKdTreeEncoder::EncodeInternal( // Fast encoding of remaining bits if number of points is 1 or 2. // Doing this also for 2 gives a slight additional speed up. if (num_remaining_points <= 2) { - // TODO(hemmer): axes_ not necessary, remove would change bitstream! + // TODO(b/199760123): |axes_| not necessary, remove would change + // bitstream! axes_[0] = axis; for (uint32_t i = 1; i < dimension_; i++) { axes_[i] = DRACO_INCREMENT_MOD(axes_[i - 1], dimension_); diff --git a/contrib/draco/src/draco/compression/point_cloud/algorithms/float_points_tree_encoder.h b/contrib/draco/src/draco/compression/point_cloud/algorithms/float_points_tree_encoder.h index 26ba94f1f..44c1b3d3a 100644 --- a/contrib/draco/src/draco/compression/point_cloud/algorithms/float_points_tree_encoder.h +++ b/contrib/draco/src/draco/compression/point_cloud/algorithms/float_points_tree_encoder.h @@ -44,7 +44,7 @@ namespace draco { // there are more leading zeros, which is then compressed better by the // arithmetic encoding. -// TODO(hemmer): Remove class because it duplicates quantization code. +// TODO(b/199760123): Remove class because it duplicates quantization code. class FloatPointsTreeEncoder { public: explicit FloatPointsTreeEncoder(PointCloudCompressionMethod method); @@ -91,7 +91,7 @@ bool FloatPointsTreeEncoder::EncodePointCloud(InputIteratorT points_begin, // Collect necessary data for encoding. num_points_ = std::distance(points_begin, points_end); - // TODO(hemmer): Extend quantization tools to make this more automatic. + // TODO(b/199760123): Extend quantization tools to make this more automatic. // Compute range of points for quantization std::vector qpoints; qpoints.reserve(num_points_); diff --git a/contrib/draco/src/draco/compression/point_cloud/algorithms/integer_points_kd_tree_decoder.h b/contrib/draco/src/draco/compression/point_cloud/algorithms/integer_points_kd_tree_decoder.h index 94e523cad..bc31af586 100644 --- a/contrib/draco/src/draco/compression/point_cloud/algorithms/integer_points_kd_tree_decoder.h +++ b/contrib/draco/src/draco/compression/point_cloud/algorithms/integer_points_kd_tree_decoder.h @@ -12,7 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. // -// TODO(hemmer): Make this a wrapper using DynamicIntegerPointsKdTreeDecoder. +// TODO(b/199760123): Make this a wrapper using +// DynamicIntegerPointsKdTreeDecoder. // // See integer_points_kd_tree_encoder.h for documentation. diff --git a/contrib/draco/src/draco/compression/point_cloud/algorithms/integer_points_kd_tree_encoder.h b/contrib/draco/src/draco/compression/point_cloud/algorithms/integer_points_kd_tree_encoder.h index b8811092e..654f14a78 100644 --- a/contrib/draco/src/draco/compression/point_cloud/algorithms/integer_points_kd_tree_encoder.h +++ b/contrib/draco/src/draco/compression/point_cloud/algorithms/integer_points_kd_tree_encoder.h @@ -12,7 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. // -// TODO(hemmer): Make this a wrapper using DynamicIntegerPointsKdTreeEncoder. +// TODO(b/199760123): Make this a wrapper using +// DynamicIntegerPointsKdTreeEncoder. #ifndef DRACO_COMPRESSION_POINT_CLOUD_ALGORITHMS_INTEGER_POINTS_KD_TREE_ENCODER_H_ #define DRACO_COMPRESSION_POINT_CLOUD_ALGORITHMS_INTEGER_POINTS_KD_TREE_ENCODER_H_ diff --git a/contrib/draco/src/draco/compression/point_cloud/algorithms/quantize_points_3.h b/contrib/draco/src/draco/compression/point_cloud/algorithms/quantize_points_3.h index 01943ad9e..8ea0741da 100644 --- a/contrib/draco/src/draco/compression/point_cloud/algorithms/quantize_points_3.h +++ b/contrib/draco/src/draco/compression/point_cloud/algorithms/quantize_points_3.h @@ -22,7 +22,7 @@ namespace draco { -// TODO(hemmer): Make this a stable bounding box. +// TODO(b/199760123): Make this a stable bounding box. struct QuantizationInfo { uint32_t quantization_bits; float range; @@ -41,7 +41,7 @@ OutputIterator QuantizePoints3(const PointIterator &begin, max_range = std::max(std::fabs((*it)[2]), max_range); } - const uint32_t max_quantized_value((1 << info->quantization_bits) - 1); + const uint32_t max_quantized_value((1u << info->quantization_bits) - 1); Quantizer quantize; quantize.Init(max_range, max_quantized_value); info->range = max_range; @@ -66,7 +66,7 @@ void DequantizePoints3(const QPointIterator &begin, const QPointIterator &end, const uint32_t quantization_bits = info.quantization_bits; const float range = info.range; - const uint32_t max_quantized_value((1 << quantization_bits) - 1); + const uint32_t max_quantized_value((1u << quantization_bits) - 1); Dequantizer dequantize; dequantize.Init(range, max_quantized_value); diff --git a/contrib/draco/src/draco/compression/point_cloud/point_cloud_kd_tree_encoding_test.cc b/contrib/draco/src/draco/compression/point_cloud/point_cloud_kd_tree_encoding_test.cc index 2249bb09e..7a7b597f2 100644 --- a/contrib/draco/src/draco/compression/point_cloud/point_cloud_kd_tree_encoding_test.cc +++ b/contrib/draco/src/draco/compression/point_cloud/point_cloud_kd_tree_encoding_test.cc @@ -68,7 +68,7 @@ class PointCloudKdTreeEncodingTest : public ::testing::Test { ++compression_level) { options.SetSpeed(10 - compression_level, 10 - compression_level); encoder.SetPointCloud(pc); - ASSERT_TRUE(encoder.Encode(options, &buffer).ok()); + DRACO_ASSERT_OK(encoder.Encode(options, &buffer)); DecoderBuffer dec_buffer; dec_buffer.Init(buffer.data(), buffer.size()); @@ -76,7 +76,7 @@ class PointCloudKdTreeEncodingTest : public ::testing::Test { std::unique_ptr out_pc(new PointCloud()); DecoderOptions dec_options; - ASSERT_TRUE(decoder.Decode(dec_options, &dec_buffer, out_pc.get()).ok()); + DRACO_ASSERT_OK(decoder.Decode(dec_options, &dec_buffer, out_pc.get())); ComparePointClouds(pc, *out_pc); } diff --git a/contrib/draco/src/draco/core/bounding_box.cc b/contrib/draco/src/draco/core/bounding_box.cc index 8a0709678..8acd6687b 100644 --- a/contrib/draco/src/draco/core/bounding_box.cc +++ b/contrib/draco/src/draco/core/bounding_box.cc @@ -20,11 +20,20 @@ BoundingBox::BoundingBox() : BoundingBox(Vector3f(std::numeric_limits::max(), std::numeric_limits::max(), std::numeric_limits::max()), - Vector3f(-std::numeric_limits::max(), - -std::numeric_limits::max(), - -std::numeric_limits::max())) {} + Vector3f(std::numeric_limits::lowest(), + std::numeric_limits::lowest(), + std::numeric_limits::lowest())) {} BoundingBox::BoundingBox(const Vector3f &min_point, const Vector3f &max_point) : min_point_(min_point), max_point_(max_point) {} +const bool BoundingBox::IsValid() const { + return GetMinPoint()[0] != std::numeric_limits::max() && + GetMinPoint()[1] != std::numeric_limits::max() && + GetMinPoint()[2] != std::numeric_limits::max() && + GetMaxPoint()[0] != std::numeric_limits::lowest() && + GetMaxPoint()[1] != std::numeric_limits::lowest() && + GetMaxPoint()[2] != std::numeric_limits::lowest(); +} + } // namespace draco diff --git a/contrib/draco/src/draco/core/bounding_box.h b/contrib/draco/src/draco/core/bounding_box.h index 31ba2d683..697a73b6f 100644 --- a/contrib/draco/src/draco/core/bounding_box.h +++ b/contrib/draco/src/draco/core/bounding_box.h @@ -38,6 +38,11 @@ class BoundingBox { // Returns the maximum point of the bounding box. inline const Vector3f &GetMaxPoint() const { return max_point_; } + // Checks if the bounding box object was created with the default constructor + // then never updated. Internally, checks if the bounding box minimum and + // maximum points hold the largest positive and smallest negative values. + const bool IsValid() const; + // Conditionally updates the bounding box with a given |new_point|. void Update(const Vector3f &new_point) { for (int i = 0; i < 3; i++) { diff --git a/contrib/draco/src/draco/core/constants.h b/contrib/draco/src/draco/core/constants.h new file mode 100644 index 000000000..3e81992a1 --- /dev/null +++ b/contrib/draco/src/draco/core/constants.h @@ -0,0 +1,6 @@ +#ifndef DRACO_CORE_CONSTANTS_H_ +#define DRACO_CORE_CONSTANTS_H_ + +#define DRACO_PI 3.14159265358979323846 + +#endif // DRACO_CORE_CONSTANTS_H_ diff --git a/contrib/draco/src/draco/core/data_buffer.cc b/contrib/draco/src/draco/core/data_buffer.cc index f0b43d67d..96a378798 100644 --- a/contrib/draco/src/draco/core/data_buffer.cc +++ b/contrib/draco/src/draco/core/data_buffer.cc @@ -52,7 +52,7 @@ void DataBuffer::Resize(int64_t size) { } void DataBuffer::WriteDataToStream(std::ostream &stream) { - if (data_.size() == 0) { + if (data_.empty()) { return; } stream.write(reinterpret_cast(data_.data()), data_.size()); diff --git a/contrib/draco/src/draco/core/data_buffer.h b/contrib/draco/src/draco/core/data_buffer.h index 8ee690540..8eac0f6b4 100644 --- a/contrib/draco/src/draco/core/data_buffer.h +++ b/contrib/draco/src/draco/core/data_buffer.h @@ -67,7 +67,7 @@ class DataBuffer { int64_t update_count() const { return descriptor_.buffer_update_count; } size_t data_size() const { return data_.size(); } const uint8_t *data() const { return data_.data(); } - uint8_t *data() { return &data_[0]; } + uint8_t *data() { return data_.data(); } int64_t buffer_id() const { return descriptor_.buffer_id; } void set_buffer_id(int64_t buffer_id) { descriptor_.buffer_id = buffer_id; } diff --git a/contrib/draco/src/draco/core/decoder_buffer.h b/contrib/draco/src/draco/core/decoder_buffer.h index 0559abbe4..71189b7e7 100644 --- a/contrib/draco/src/draco/core/decoder_buffer.h +++ b/contrib/draco/src/draco/core/decoder_buffer.h @@ -54,12 +54,11 @@ class DecoderBuffer { // Decodes up to 32 bits into out_val. Can be called only in between // StartBitDecoding and EndBitDecoding. Otherwise returns false. - bool DecodeLeastSignificantBits32(int nbits, uint32_t *out_value) { + bool DecodeLeastSignificantBits32(uint32_t nbits, uint32_t *out_value) { if (!bit_decoder_active()) { return false; } - bit_decoder_.GetBits(nbits, out_value); - return true; + return bit_decoder_.GetBits(nbits, out_value); } // Decodes an arbitrary data type. @@ -158,11 +157,12 @@ class DecoderBuffer { inline void ConsumeBits(int k) { bit_offset_ += k; } // Returns |nbits| bits in |x|. - inline bool GetBits(int32_t nbits, uint32_t *x) { - DRACO_DCHECK_GE(nbits, 0); - DRACO_DCHECK_LE(nbits, 32); + inline bool GetBits(uint32_t nbits, uint32_t *x) { + if (nbits > 32) { + return false; + } uint32_t value = 0; - for (int32_t bit = 0; bit < nbits; ++bit) { + for (uint32_t bit = 0; bit < nbits; ++bit) { value |= GetBit() << bit; } *x = value; diff --git a/contrib/draco/src/draco/core/draco_index_type_vector.h b/contrib/draco/src/draco/core/draco_index_type_vector.h index aae1e7aaf..f5256ded9 100644 --- a/contrib/draco/src/draco/core/draco_index_type_vector.h +++ b/contrib/draco/src/draco/core/draco_index_type_vector.h @@ -25,25 +25,32 @@ namespace draco { // A wrapper around the standard std::vector that supports indexing of the // vector entries using the strongly typed indices as defined in -// draco_index_type.h . -// TODO(ostava): Make the interface more complete. It's currently missing -// features such as iterators. -// TODO(vytyaz): Add more unit tests for this class. +// draco_index_type.h. +// TODO(ostava): Make the interface more complete. It's currently missing some +// features. template class IndexTypeVector { public: typedef typename std::vector::const_reference const_reference; typedef typename std::vector::reference reference; + typedef typename std::vector::iterator iterator; + typedef typename std::vector::const_iterator const_iterator; IndexTypeVector() {} explicit IndexTypeVector(size_t size) : vector_(size) {} IndexTypeVector(size_t size, const ValueTypeT &val) : vector_(size, val) {} + iterator begin() { return vector_.begin(); } + const_iterator begin() const { return vector_.begin(); } + iterator end() { return vector_.end(); } + const_iterator end() const { return vector_.end(); } + void clear() { vector_.clear(); } void reserve(size_t size) { vector_.reserve(size); } void resize(size_t size) { vector_.resize(size); } void resize(size_t size, const ValueTypeT &val) { vector_.resize(size, val); } void assign(size_t size, const ValueTypeT &val) { vector_.assign(size, val); } + iterator erase(iterator position) { return vector_.erase(position); } void swap(IndexTypeVector &arg) { vector_.swap(arg.vector_); diff --git a/contrib/draco/src/draco/core/draco_test_utils.cc b/contrib/draco/src/draco/core/draco_test_utils.cc index edca9856d..a71082a86 100644 --- a/contrib/draco/src/draco/core/draco_test_utils.cc +++ b/contrib/draco/src/draco/core/draco_test_utils.cc @@ -16,9 +16,9 @@ #include +#include "draco/core/draco_test_base.h" #include "draco/core/macros.h" #include "draco/io/file_utils.h" -#include "draco_test_base.h" namespace draco { @@ -27,6 +27,8 @@ static constexpr char kTestDataDir[] = DRACO_TEST_DATA_DIR; static constexpr char kTestTempDir[] = DRACO_TEST_TEMP_DIR; } // namespace +std::string GetTestTempDir() { return std::string(kTestDataDir); } + std::string GetTestFileFullPath(const std::string &file_name) { return std::string(kTestDataDir) + std::string("/") + file_name; } @@ -55,11 +57,13 @@ bool CompareGoldenFile(const std::string &golden_file_name, const void *data, size_t remaining_data_size = data_size; int offset = 0; while ((extracted_size = in_file.read(buffer, buffer_size).gcount()) > 0) { - if (remaining_data_size <= 0) + if (remaining_data_size <= 0) { break; // Input and golden sizes are different. + } size_t size_to_check = extracted_size; - if (remaining_data_size < size_to_check) + if (remaining_data_size < size_to_check) { size_to_check = remaining_data_size; + } for (uint32_t i = 0; i < size_to_check; ++i) { if (buffer[i] != data_c8[offset++]) { LOG(INFO) << "Test output differed from golden file at byte " @@ -77,4 +81,20 @@ bool CompareGoldenFile(const std::string &golden_file_name, const void *data, return true; } +#ifdef DRACO_TRANSCODER_SUPPORTED + +template <> +std::unique_ptr ReadGeometryFromTestFile( + const std::string &file_name) { + return ReadMeshFromTestFile(file_name); +} + +template <> +std::unique_ptr ReadGeometryFromTestFile( + const std::string &file_name) { + return ReadSceneFromTestFile(file_name); +} + +#endif // DRACO_TRANSCODER_SUPPORTED + } // namespace draco diff --git a/contrib/draco/src/draco/core/draco_test_utils.h b/contrib/draco/src/draco/core/draco_test_utils.h index fa548f52d..658096fe1 100644 --- a/contrib/draco/src/draco/core/draco_test_utils.h +++ b/contrib/draco/src/draco/core/draco_test_utils.h @@ -15,12 +15,24 @@ #ifndef DRACO_CORE_DRACO_TEST_UTILS_H_ #define DRACO_CORE_DRACO_TEST_UTILS_H_ +#include +#include +#include + #include "draco/core/draco_test_base.h" +#include "draco/draco_features.h" #include "draco/io/mesh_io.h" #include "draco/io/point_cloud_io.h" +#ifdef DRACO_TRANSCODER_SUPPORTED +#include "draco/io/scene_io.h" +#endif + namespace draco { +// Returns test temporary directory. +std::string GetTestTempDir(); + // Returns the full path to a given file system entry, such as test file or test // directory. std::string GetTestFileFullPath(const std::string &entry_name); @@ -65,6 +77,47 @@ inline std::unique_ptr ReadPointCloudFromTestFile( return ReadPointCloudFromFile(path).value(); } +#ifdef DRACO_TRANSCODER_SUPPORTED +inline std::unique_ptr ReadSceneFromTestFile( + const std::string &file_name) { + const std::string path = GetTestFileFullPath(file_name); + return ReadSceneFromFile(path).value(); +} + +// Loads geometry specified by a |file_name| that is going to be automatically +// converted to the correct path available to the testing instance. Supported +// geometry types are Mesh and Scene. +template +std::unique_ptr ReadGeometryFromTestFile(const std::string &file_name); + +#endif // DRACO_TRANSCODER_SUPPORTED + +// Utility class for redirection and capture of stderr/stdout. +class CaptureStream { + public: + explicit CaptureStream(std::ostream &stream) + : old_buffer_(stream.rdbuf(buffer_.rdbuf())), stream_(stream) {} + + ~CaptureStream() { Reset(); } + + std::string GetStringAndRelease() { + Reset(); + return buffer_.str(); + } + + void Reset() { + if (old_buffer_) { + stream_.rdbuf(old_buffer_); + old_buffer_ = nullptr; + } + } + + private: + std::ostringstream buffer_; + std::streambuf *old_buffer_ = nullptr; + std::ostream &stream_; +}; + // Evaluates an expression that returns draco::Status. If the status is not OK, // the macro asserts and logs the error message. #define DRACO_ASSERT_OK(expression) \ diff --git a/contrib/draco/src/draco/core/draco_version.h b/contrib/draco/src/draco/core/draco_version.h index 14a504a50..88856447f 100644 --- a/contrib/draco/src/draco/core/draco_version.h +++ b/contrib/draco/src/draco/core/draco_version.h @@ -18,9 +18,7 @@ namespace draco { // Draco version is comprised of ... -static const char kDracoVersion[] = "1.4.1"; - -const char *Version() { return kDracoVersion; } +static const char kDracoVersion[] = "1.5.6"; } // namespace draco diff --git a/contrib/draco/src/draco/core/macros.h b/contrib/draco/src/draco/core/macros.h index 147bbaafc..a31e7c44b 100644 --- a/contrib/draco/src/draco/core/macros.h +++ b/contrib/draco/src/draco/core/macros.h @@ -15,7 +15,8 @@ #ifndef DRACO_CORE_MACROS_H_ #define DRACO_CORE_MACROS_H_ -#include "assert.h" +#include + #include "draco/draco_features.h" #ifdef ANDROID_LOGGING @@ -37,7 +38,7 @@ namespace draco { #define DISALLOW_COPY_AND_ASSIGN(TypeName) \ TypeName(const TypeName &) = delete; \ void operator=(const TypeName &) = delete; -#endif +#endif // DISALLOW_COPY_AND_ASSIGN #ifndef FALLTHROUGH_INTENDED #if defined(__clang__) && defined(__has_warning) @@ -46,7 +47,7 @@ namespace draco { #endif #elif defined(__GNUC__) && __GNUC__ >= 7 #define FALLTHROUGH_INTENDED [[gnu::fallthrough]] -#endif +#endif // FALLTHROUGH_INTENDED // If FALLTHROUGH_INTENDED is still not defined, define it. #ifndef FALLTHROUGH_INTENDED @@ -54,7 +55,7 @@ namespace draco { do { \ } while (0) #endif -#endif +#endif // FALLTHROUGH_INTENDED #ifndef LOG #define LOG(...) std::cout @@ -84,12 +85,16 @@ namespace draco { #define DRACO_DCHECK_LE(a, b) #define DRACO_DCHECK_LT(a, b) #define DRACO_DCHECK_NOTNULL(x) -#endif +#endif // DRACO_DEBUG // Helper macros for concatenating macro values. #define DRACO_MACROS_IMPL_CONCAT_INNER_(x, y) x##y #define DRACO_MACROS_IMPL_CONCAT_(x, y) DRACO_MACROS_IMPL_CONCAT_INNER_(x, y) +#define DRACO_MACROS_IMPL_CONCAT_INNER_3_(x, y, z) x##y##z +#define DRACO_MACROS_IMPL_CONCAT_3_(x, y, z) \ + DRACO_MACROS_IMPL_CONCAT_INNER_3_(x, y, z) + // Expand the n-th argument of the macro. Used to select an argument based on // the number of entries in a variadic macro argument. Example usage: // @@ -100,9 +105,9 @@ namespace draco { // #define VARIADIC_MACRO(...) // DRACO_SELECT_NTH_FROM_3(__VA_ARGS__, FUNC_3, FUNC_2, FUNC_1) __VA_ARGS__ // -#define DRACO_SELECT_NTH_FROM_2(_1, _2, NAME) NAME -#define DRACO_SELECT_NTH_FROM_3(_1, _2, _3, NAME) NAME -#define DRACO_SELECT_NTH_FROM_4(_1, _2, _3, _4, NAME) NAME +#define DRACO_SELECT_NTH_FROM_2(_1, _2, NAME, ...) NAME +#define DRACO_SELECT_NTH_FROM_3(_1, _2, _3, NAME, ...) NAME +#define DRACO_SELECT_NTH_FROM_4(_1, _2, _3, _4, NAME, ...) NAME // Macro that converts the Draco bit-stream into one uint16_t number. // Useful mostly when checking version numbers. diff --git a/contrib/draco/src/draco/core/math_utils.h b/contrib/draco/src/draco/core/math_utils.h index 7f382fa34..d7732e55d 100644 --- a/contrib/draco/src/draco/core/math_utils.h +++ b/contrib/draco/src/draco/core/math_utils.h @@ -19,6 +19,8 @@ #include "draco/core/vector_d.h" +namespace draco { + #define DRACO_INCREMENT_MOD(I, M) (((I) == ((M)-1)) ? 0 : ((I) + 1)) // Returns floor(sqrt(x)) where x is an integer number. The main intend of this @@ -52,4 +54,26 @@ inline uint64_t IntSqrt(uint64_t number) { return square_root; } +// Performs the addition in unsigned type to avoid signed integer overflow. Note +// that the result will be the same (for non-overflowing values). +template < + typename DataTypeT, + typename std::enable_if::value && + std::is_signed::value>::type * = nullptr> +inline DataTypeT AddAsUnsigned(DataTypeT a, DataTypeT b) { + typedef typename std::make_unsigned::type DataTypeUT; + return static_cast(static_cast(a) + + static_cast(b)); +} + +template ::value || + !std::is_signed::value>::type * = + nullptr> +inline DataTypeT AddAsUnsigned(DataTypeT a, DataTypeT b) { + return a + b; +} + +} // namespace draco + #endif // DRACO_CORE_MATH_UTILS_H_ diff --git a/contrib/draco/src/draco/core/math_utils_test.cc b/contrib/draco/src/draco/core/math_utils_test.cc index 8c255d046..460a67444 100644 --- a/contrib/draco/src/draco/core/math_utils_test.cc +++ b/contrib/draco/src/draco/core/math_utils_test.cc @@ -5,7 +5,7 @@ #include "draco/core/draco_test_base.h" -using draco::Vector3f; +namespace draco { TEST(MathUtils, Mod) { EXPECT_EQ(DRACO_INCREMENT_MOD(1, 1 << 1), 0); } @@ -20,3 +20,5 @@ TEST(MathUtils, IntSqrt) { ASSERT_EQ(IntSqrt(number), static_cast(floor(std::sqrt(number)))); } } + +} // namespace draco diff --git a/contrib/draco/src/draco/core/options.cc b/contrib/draco/src/draco/core/options.cc index 9b81db489..ceb87cccc 100644 --- a/contrib/draco/src/draco/core/options.cc +++ b/contrib/draco/src/draco/core/options.cc @@ -15,13 +15,12 @@ #include "draco/core/options.h" #include +#include #include #include namespace draco { -Options::Options() {} - void Options::MergeAndReplace(const Options &other_options) { for (const auto &item : other_options.options_) { options_[item.first] = item.second; diff --git a/contrib/draco/src/draco/core/options.h b/contrib/draco/src/draco/core/options.h index 1bc4dc0fb..3f15d13ba 100644 --- a/contrib/draco/src/draco/core/options.h +++ b/contrib/draco/src/draco/core/options.h @@ -19,6 +19,8 @@ #include #include +#include "draco/draco_features.h" + namespace draco { // Class for storing generic options as a pair in a string map. @@ -27,7 +29,8 @@ namespace draco { // data type. class Options { public: - Options(); + Options() = default; + ~Options() = default; // Merges |other_options| on top of the existing options of this instance // replacing all entries that are present in both options instances. @@ -71,8 +74,6 @@ class Options { private: // All entries are internally stored as strings and converted to the desired // return type based on the used Get* method. - // TODO(ostava): Consider adding type safety mechanism that would prevent - // unsafe operations such as a conversion from vector to int. std::map options_; }; diff --git a/contrib/draco/src/draco/core/status.cc b/contrib/draco/src/draco/core/status.cc new file mode 100644 index 000000000..ecb0b536e --- /dev/null +++ b/contrib/draco/src/draco/core/status.cc @@ -0,0 +1,44 @@ +// Copyright 2022 The Draco Authors. +// +// 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 "draco/core/status.h" + +#include + +namespace draco { + +std::string Status::code_string() const { + switch (code_) { + case Code::OK: + return "OK"; + case Code::DRACO_ERROR: + return "DRACO_ERROR"; + case Code::IO_ERROR: + return "IO_ERROR"; + case Code::INVALID_PARAMETER: + return "INVALID_PARAMETER"; + case Code::UNSUPPORTED_VERSION: + return "UNSUPPORTED_VERSION"; + case Code::UNKNOWN_VERSION: + return "UNKNOWN_VERSION"; + case Code::UNSUPPORTED_FEATURE: + return "UNSUPPORTED_FEATURE"; + } + return "UNKNOWN_STATUS_VALUE"; +} + +std::string Status::code_and_error_string() const { + return code_string() + ": " + error_msg_string(); +} + +} // namespace draco diff --git a/contrib/draco/src/draco/core/status.h b/contrib/draco/src/draco/core/status.h index 449ad8566..fac96046c 100644 --- a/contrib/draco/src/draco/core/status.h +++ b/contrib/draco/src/draco/core/status.h @@ -15,6 +15,7 @@ #ifndef DRACO_CORE_STATUS_H_ #define DRACO_CORE_STATUS_H_ +#include #include namespace draco { @@ -44,6 +45,8 @@ class Status { Code code() const { return code_; } const std::string &error_msg_string() const { return error_msg_; } const char *error_msg() const { return error_msg_.c_str(); } + std::string code_string() const; + std::string code_and_error_string() const; bool operator==(Code code) const { return code == code_; } bool ok() const { return code_ == OK; } @@ -61,6 +64,9 @@ inline std::ostream &operator<<(std::ostream &os, const Status &status) { } inline Status OkStatus() { return Status(Status::OK); } +inline Status ErrorStatus(const std::string &msg) { + return Status(Status::DRACO_ERROR, msg); +} // Evaluates an expression that returns draco::Status. If the status is not OK, // the macro returns the status object. diff --git a/contrib/draco/src/draco/core/status_test.cc b/contrib/draco/src/draco/core/status_test.cc index c1ad4ab30..dc36496d4 100644 --- a/contrib/draco/src/draco/core/status_test.cc +++ b/contrib/draco/src/draco/core/status_test.cc @@ -29,10 +29,17 @@ TEST_F(StatusTest, TestStatusOutput) { // Tests that the Status can be stored in a provided std::ostream. const draco::Status status(draco::Status::DRACO_ERROR, "Error msg."); ASSERT_EQ(status.code(), draco::Status::DRACO_ERROR); + ASSERT_EQ(status.code_string(), "DRACO_ERROR"); std::stringstream str; str << status; ASSERT_EQ(str.str(), "Error msg."); + + const draco::Status status2 = draco::ErrorStatus("Error msg2."); + ASSERT_EQ(status2.code(), draco::Status::DRACO_ERROR); + ASSERT_EQ(status2.error_msg_string(), "Error msg2."); + ASSERT_EQ(status2.code_string(), "DRACO_ERROR"); + ASSERT_EQ(status2.code_and_error_string(), "DRACO_ERROR: Error msg2."); } } // namespace diff --git a/contrib/draco/src/draco/core/vector_d.h b/contrib/draco/src/draco/core/vector_d.h index a3c46a46a..a0ec2dedf 100644 --- a/contrib/draco/src/draco/core/vector_d.h +++ b/contrib/draco/src/draco/core/vector_d.h @@ -34,7 +34,7 @@ class VectorD { typedef ScalarT Scalar; typedef VectorD Self; - // TODO(hemmer): Deprecate. + // TODO(b/199760123): Deprecate. typedef ScalarT CoefficientType; VectorD() { @@ -45,7 +45,7 @@ class VectorD { // The following constructor does not compile in opt mode, which for now led // to the constructors further down, which is not ideal. - // TODO(hemmer): fix constructor below and remove others. + // TODO(b/199760123): Fix constructor below and remove others. // template // explicit VectorD(Args... args) : v_({args...}) {} @@ -111,7 +111,7 @@ class VectorD { Scalar &operator[](int i) { return v_[i]; } const Scalar &operator[](int i) const { return v_[i]; } - // TODO(hemmer): remove. + // TODO(b/199760123): Remove. // Similar to interface of Eigen library. Scalar &operator()(int i) { return v_[i]; } const Scalar &operator()(int i) const { return v_[i]; } diff --git a/contrib/draco/src/draco/core/vector_d_test.cc b/contrib/draco/src/draco/core/vector_d_test.cc index d66128fb1..21c1ca4c5 100644 --- a/contrib/draco/src/draco/core/vector_d_test.cc +++ b/contrib/draco/src/draco/core/vector_d_test.cc @@ -32,16 +32,6 @@ typedef draco::Vector5ui Vector5ui; typedef draco::VectorD Vector3i; typedef draco::VectorD Vector4i; -template -void TestSquaredDistance(const draco::VectorD v1, - const draco::VectorD v2, - const CoeffT result) { - CoeffT squared_distance = SquaredDistance(v1, v2); - ASSERT_EQ(squared_distance, result); - squared_distance = SquaredDistance(v2, v1); - ASSERT_EQ(squared_distance, result); -} - TEST(VectorDTest, TestOperators) { { const Vector3f v; @@ -170,56 +160,6 @@ TEST(VectorTest, TestGetNormalizedWithZeroLengthVector) { ASSERT_EQ(normalized[2], 0); } -TEST(VectorDTest, TestSquaredDistance) { - // Test Vector2f: float, 2D. - Vector2f v1_2f(5.5, 10.5); - Vector2f v2_2f(3.5, 15.5); - float result_f = 29; - TestSquaredDistance(v1_2f, v2_2f, result_f); - - // Test Vector3f: float, 3D. - Vector3f v1_3f(5.5, 10.5, 2.3); - Vector3f v2_3f(3.5, 15.5, 0); - result_f = 34.29; - TestSquaredDistance(v1_3f, v2_3f, result_f); - - // Test Vector4f: float, 4D. - Vector4f v1_4f(5.5, 10.5, 2.3, 7.2); - Vector4f v2_4f(3.5, 15.5, 0, 9.9); - result_f = 41.58; - TestSquaredDistance(v1_4f, v2_4f, result_f); - - // Test Vector5f: float, 5D. - Vector5f v1_5f(5.5, 10.5, 2.3, 7.2, 1.0); - Vector5f v2_5f(3.5, 15.5, 0, 9.9, 0.2); - result_f = 42.22; - TestSquaredDistance(v1_5f, v2_5f, result_f); - - // Test Vector 2ui: uint32_t, 2D. - Vector2ui v1_2ui(5, 10); - Vector2ui v2_2ui(3, 15); - uint32_t result_ui = 29; - TestSquaredDistance(v1_2ui, v2_2ui, result_ui); - - // Test Vector 3ui: uint32_t, 3D. - Vector3ui v1_3ui(5, 10, 2); - Vector3ui v2_3ui(3, 15, 0); - result_ui = 33; - TestSquaredDistance(v1_3ui, v2_3ui, result_ui); - - // Test Vector 4ui: uint32_t, 4D. - Vector4ui v1_4ui(5, 10, 2, 7); - Vector4ui v2_4ui(3, 15, 0, 9); - result_ui = 37; - TestSquaredDistance(v1_4ui, v2_4ui, result_ui); - - // Test Vector 5ui: uint32_t, 5D. - Vector5ui v1_5ui(5, 10, 2, 7, 1); - Vector5ui v2_5ui(3, 15, 0, 9, 12); - result_ui = 158; - TestSquaredDistance(v1_5ui, v2_5ui, result_ui); -} - TEST(VectorDTest, TestCrossProduct3D) { const Vector3i e1(1, 0, 0); const Vector3i e2(0, 1, 0); diff --git a/contrib/draco/src/draco/io/file_reader_factory.cc b/contrib/draco/src/draco/io/file_reader_factory.cc index ac7b09288..a8f15a11f 100644 --- a/contrib/draco/src/draco/io/file_reader_factory.cc +++ b/contrib/draco/src/draco/io/file_reader_factory.cc @@ -1,5 +1,6 @@ #include "draco/io/file_reader_factory.h" +#include #include namespace draco { @@ -38,7 +39,6 @@ std::unique_ptr FileReaderFactory::OpenReader( } return reader; } - FILEREADER_LOG_ERROR("No file reader able to open input"); return nullptr; } diff --git a/contrib/draco/src/draco/io/file_utils.cc b/contrib/draco/src/draco/io/file_utils.cc index f93cbd899..694d259e8 100644 --- a/contrib/draco/src/draco/io/file_utils.cc +++ b/contrib/draco/src/draco/io/file_utils.cc @@ -14,6 +14,8 @@ // #include "draco/io/file_utils.h" +#include + #include "draco/io/file_reader_factory.h" #include "draco/io/file_reader_interface.h" #include "draco/io/file_writer_factory.h" @@ -30,7 +32,7 @@ void SplitPath(const std::string &full_path, std::string *out_folder_path, std::string ReplaceFileExtension(const std::string &in_file_name, const std::string &new_extension) { - const auto pos = in_file_name.find_last_of("."); + const auto pos = in_file_name.find_last_of('.'); if (pos == std::string::npos) { // No extension found. return in_file_name + "." + new_extension; @@ -46,6 +48,22 @@ std::string LowercaseFileExtension(const std::string &filename) { return parser::ToLower(filename.substr(pos + 1)); } +std::string LowercaseMimeTypeExtension(const std::string &mime_type) { + const size_t pos = mime_type.find_last_of('/'); + if (pos == 0 || pos == std::string::npos || pos == mime_type.length() - 1) { + return ""; + } + return parser::ToLower(mime_type.substr(pos + 1)); +} + +std::string RemoveFileExtension(const std::string &filename) { + const size_t pos = filename.find_last_of('.'); + if (pos == 0 || pos == std::string::npos || pos == filename.length() - 1) { + return filename; + } + return filename.substr(0, pos); +} + std::string GetFullPath(const std::string &input_file_relative_path, const std::string &sibling_file_full_path) { const auto pos = sibling_file_full_path.find_last_of("/\\"); @@ -76,6 +94,23 @@ bool ReadFileToBuffer(const std::string &file_name, return file_reader->ReadFileToBuffer(buffer); } +bool ReadFileToString(const std::string &file_name, std::string *contents) { + if (!contents) { + return false; + } + std::unique_ptr file_reader = + FileReaderFactory::OpenReader(file_name); + if (file_reader == nullptr) { + return false; + } + std::vector buffer; + if (!ReadFileToBuffer(file_name, &buffer)) { + return false; + } + contents->assign(buffer.begin(), buffer.end()); + return true; +} + bool WriteBufferToFile(const char *buffer, size_t buffer_size, const std::string &file_name) { std::unique_ptr file_writer = diff --git a/contrib/draco/src/draco/io/file_utils.h b/contrib/draco/src/draco/io/file_utils.h index 4b734e049..7a5fc475b 100644 --- a/contrib/draco/src/draco/io/file_utils.h +++ b/contrib/draco/src/draco/io/file_utils.h @@ -15,6 +15,7 @@ #ifndef DRACO_IO_FILE_UTILS_H_ #define DRACO_IO_FILE_UTILS_H_ +#include #include #include @@ -37,6 +38,13 @@ std::string ReplaceFileExtension(const std::string &in_file_name, // '.' (e.g. Linux hidden files), the first delimiter is ignored. std::string LowercaseFileExtension(const std::string &filename); +// Returns the mime type extension in lowercase if present, else "". Extension +// is defined as the string after the last '/ character. +std::string LowercaseMimeTypeExtension(const std::string &mime_type); + +// Returns the file name without extension. +std::string RemoveFileExtension(const std::string &filename); + // Given a path of the input file |input_file_relative_path| relative to the // parent directory of |sibling_file_full_path|, this function returns full path // to the input file. If |sibling_file_full_path| has no directory, the relative @@ -46,13 +54,18 @@ std::string LowercaseFileExtension(const std::string &filename); std::string GetFullPath(const std::string &input_file_relative_path, const std::string &sibling_file_full_path); -// Convenience method. Uses draco::FileReaderFactory internally. Reads contents +// Convenience methods. Uses draco::FileReaderFactory internally. Reads contents // of file referenced by |file_name| into |buffer| and returns true upon // success. bool ReadFileToBuffer(const std::string &file_name, std::vector *buffer); bool ReadFileToBuffer(const std::string &file_name, std::vector *buffer); +// Convenience method for reading a file into a std::string. Reads contents +// of file referenced by |file_name| into |contents| and returns true upon +// success. +bool ReadFileToString(const std::string &file_name, std::string *contents); + // Convenience method. Uses draco::FileWriterFactory internally. Writes contents // of |buffer| to file referred to by |file_name|. File is overwritten if it // exists. Returns true after successful write. diff --git a/contrib/draco/src/draco/io/file_utils_test.cc b/contrib/draco/src/draco/io/file_utils_test.cc index 4085ff0cd..b55b1e3be 100644 --- a/contrib/draco/src/draco/io/file_utils_test.cc +++ b/contrib/draco/src/draco/io/file_utils_test.cc @@ -14,6 +14,8 @@ // #include "draco/io/file_utils.h" +#include + #include "draco/core/draco_test_base.h" #include "draco/core/draco_test_utils.h" diff --git a/contrib/draco/src/draco/io/file_writer_factory.cc b/contrib/draco/src/draco/io/file_writer_factory.cc index cb6851602..8ffd5400a 100644 --- a/contrib/draco/src/draco/io/file_writer_factory.cc +++ b/contrib/draco/src/draco/io/file_writer_factory.cc @@ -1,5 +1,6 @@ #include "draco/io/file_writer_factory.h" +#include #include namespace draco { diff --git a/contrib/draco/src/draco/io/file_writer_utils.cc b/contrib/draco/src/draco/io/file_writer_utils.cc index bcadccfc6..3dab80bbf 100644 --- a/contrib/draco/src/draco/io/file_writer_utils.cc +++ b/contrib/draco/src/draco/io/file_writer_utils.cc @@ -7,6 +7,10 @@ #include "draco/draco_features.h" +#ifdef DRACO_TRANSCODER_SUPPORTED +#include "ghc/filesystem.hpp" +#endif // DRACO_TRANSCODER_SUPPORTED + namespace draco { void SplitPathPrivate(const std::string &full_path, @@ -30,8 +34,18 @@ void SplitPathPrivate(const std::string &full_path, } } -bool DirectoryExists(const std::string &path) { +bool DirectoryExists(const std::string &path_arg) { struct stat path_stat; + std::string path = path_arg; + +#if defined(_WIN32) && not defined(__MINGW32__) + // Avoid a silly windows issue: stat() will fail on a drive letter missing the + // trailing slash. + if (path.size() > 0 && path[path.size()] != '\\' && + path[path.size()] != '/') { + path.append("\\"); + } +#endif // Check if |path| exists. if (stat(path.c_str(), &path_stat) != 0) { @@ -50,7 +64,12 @@ bool CheckAndCreatePathForFile(const std::string &filename) { std::string basename; SplitPathPrivate(filename, &path, &basename); +#ifdef DRACO_TRANSCODER_SUPPORTED + const ghc::filesystem::path ghc_path(path); + ghc::filesystem::create_directories(ghc_path); +#endif // DRACO_TRANSCODER_SUPPORTED const bool directory_exists = DirectoryExists(path); + return directory_exists; } diff --git a/contrib/draco/src/draco/io/file_writer_utils_test.cc b/contrib/draco/src/draco/io/file_writer_utils_test.cc new file mode 100644 index 000000000..14a3013e0 --- /dev/null +++ b/contrib/draco/src/draco/io/file_writer_utils_test.cc @@ -0,0 +1,49 @@ +#include "draco/io/file_writer_utils.h" + +#include + +#include "draco/core/draco_test_base.h" +#include "draco/core/draco_test_utils.h" + +namespace draco { +namespace { + +TEST(FileWriterUtilsTest, SplitPathPrivateNonWindows) { + const std::string test_path = "/path/to/file"; + std::string directory; + std::string file; + SplitPathPrivate(test_path, &directory, &file); + ASSERT_EQ(directory, "/path/to"); + ASSERT_EQ(file, "file"); +} + +TEST(FileWriterUtilsTest, SplitPathPrivateWindows) { + const std::string test_path = "C:\\path\\to\\file"; + std::string directory; + std::string file; + SplitPathPrivate(test_path, &directory, &file); + ASSERT_EQ(directory, "C:\\path\\to"); + ASSERT_EQ(file, "file"); +} + +TEST(FileWriterUtilsTest, DirectoryExistsTest) { + ASSERT_TRUE(DirectoryExists(GetTestTempDir())); + ASSERT_FALSE(DirectoryExists("fake/test/subdir")); +} + +#ifdef DRACO_TRANSCODER_SUPPORTED +TEST(FileWriterUtilsTest, CheckAndCreatePathForFileTest) { + const std::string fake_file = "fake.file"; + const std::string fake_file_subdir = "a/few/dirs/down"; + const std::string test_temp_dir = GetTestTempDir(); + const std::string fake_file_directory = + test_temp_dir + "/" + fake_file_subdir; + const std::string fake_full_path = + test_temp_dir + "/" + fake_file_subdir + "/" + fake_file; + ASSERT_TRUE(CheckAndCreatePathForFile(fake_full_path)); + ASSERT_TRUE(DirectoryExists(fake_file_directory)); +} +#endif // DRACO_TRANSCODER_SUPPORTED + +} // namespace +} // namespace draco diff --git a/contrib/draco/src/draco/io/gltf_decoder.cc b/contrib/draco/src/draco/io/gltf_decoder.cc new file mode 100644 index 000000000..521b7524f --- /dev/null +++ b/contrib/draco/src/draco/io/gltf_decoder.cc @@ -0,0 +1,2893 @@ +// Copyright 2018 The Draco Authors. +// +// 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 "draco/io/gltf_decoder.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED +#include +#include +#include +#include +#include +#include +#include + +#include "draco/core/draco_types.h" +#include "draco/core/hash_utils.h" +#include "draco/core/status.h" +#include "draco/core/status_or.h" +#include "draco/io/tiny_gltf_utils.h" +#include "draco/material/material_library.h" +#include "draco/mesh/mesh.h" +#include "draco/mesh/mesh_features.h" +#include "draco/mesh/triangle_soup_mesh_builder.h" +#include "draco/metadata/property_table.h" +#include "draco/point_cloud/point_cloud_builder.h" +#include "draco/scene/scene_indices.h" +#include "draco/texture/source_image.h" +#include "draco/texture/texture_utils.h" + +namespace draco { + +namespace { +draco::DataType GltfComponentTypeToDracoType(int component_type) { + switch (component_type) { + case TINYGLTF_COMPONENT_TYPE_BYTE: + return DT_INT8; + case TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE: + return DT_UINT8; + case TINYGLTF_COMPONENT_TYPE_SHORT: + return DT_INT16; + case TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT: + return DT_UINT16; + case TINYGLTF_COMPONENT_TYPE_UNSIGNED_INT: + return DT_UINT32; + case TINYGLTF_COMPONENT_TYPE_FLOAT: + return DT_FLOAT32; + } + return DT_INVALID; +} + +GeometryAttribute::Type GltfAttributeToDracoAttribute( + const std::string attribute_name) { + if (attribute_name == "POSITION") { + return GeometryAttribute::POSITION; + } else if (attribute_name == "NORMAL") { + return GeometryAttribute::NORMAL; + } else if (attribute_name == "TEXCOORD_0") { + return GeometryAttribute::TEX_COORD; + } else if (attribute_name == "TEXCOORD_1") { + return GeometryAttribute::TEX_COORD; + } else if (attribute_name == "TANGENT") { + return GeometryAttribute::TANGENT; + } else if (attribute_name == "COLOR_0") { + return GeometryAttribute::COLOR; + } else if (attribute_name == "JOINTS_0") { + return GeometryAttribute::JOINTS; + } else if (attribute_name == "WEIGHTS_0") { + return GeometryAttribute::WEIGHTS; + } else if (attribute_name.rfind("_FEATURE_ID_") == 0) { + // Feature ID attribute like _FEATURE_ID_5 from EXT_mesh_features extension. + return GeometryAttribute::GENERIC; + } + return GeometryAttribute::INVALID; +} + +StatusOr TinyGltfToDracoAxisWrappingMode( + int wrap_mode) { + switch (wrap_mode) { + case TINYGLTF_TEXTURE_WRAP_CLAMP_TO_EDGE: + return TextureMap::CLAMP_TO_EDGE; + case TINYGLTF_TEXTURE_WRAP_MIRRORED_REPEAT: + return TextureMap::MIRRORED_REPEAT; + case TINYGLTF_TEXTURE_WRAP_REPEAT: + return TextureMap::REPEAT; + default: + return Status(Status::UNSUPPORTED_FEATURE, "Unsupported wrapping mode."); + } +} + +StatusOr TinyGltfToDracoFilterType(int filter_type) { + switch (filter_type) { + case -1: + return TextureMap::UNSPECIFIED; + case TINYGLTF_TEXTURE_FILTER_NEAREST: + return TextureMap::NEAREST; + case TINYGLTF_TEXTURE_FILTER_LINEAR: + return TextureMap::LINEAR; + case TINYGLTF_TEXTURE_FILTER_NEAREST_MIPMAP_NEAREST: + return TextureMap::NEAREST_MIPMAP_NEAREST; + case TINYGLTF_TEXTURE_FILTER_LINEAR_MIPMAP_NEAREST: + return TextureMap::LINEAR_MIPMAP_NEAREST; + case TINYGLTF_TEXTURE_FILTER_NEAREST_MIPMAP_LINEAR: + return TextureMap::NEAREST_MIPMAP_LINEAR; + case TINYGLTF_TEXTURE_FILTER_LINEAR_MIPMAP_LINEAR: + return TextureMap::LINEAR_MIPMAP_LINEAR; + default: + return Status(Status::DRACO_ERROR, "Unsupported texture filter type."); + } +} + +StatusOr> CopyDataAsUint32( + const tinygltf::Model &model, const tinygltf::Accessor &accessor) { + if (accessor.componentType == TINYGLTF_COMPONENT_TYPE_BYTE) { + return Status(Status::DRACO_ERROR, "Byte cannot be converted to Uint32."); + } + if (accessor.componentType == TINYGLTF_COMPONENT_TYPE_SHORT) { + return Status(Status::DRACO_ERROR, "Short cannot be converted to Uint32."); + } + if (accessor.componentType == TINYGLTF_COMPONENT_TYPE_INT) { + return Status(Status::DRACO_ERROR, "Int cannot be converted to Uint32."); + } + if (accessor.componentType == TINYGLTF_COMPONENT_TYPE_FLOAT) { + return Status(Status::DRACO_ERROR, "Float cannot be converted to Uint32."); + } + if (accessor.componentType == TINYGLTF_COMPONENT_TYPE_DOUBLE) { + return Status(Status::DRACO_ERROR, "Double cannot be converted to Uint32."); + } + if (accessor.bufferView < 0) { + return Status(Status::DRACO_ERROR, + "Error CopyDataAsUint32() bufferView < 0."); + } + + const tinygltf::BufferView &buffer_view = + model.bufferViews[accessor.bufferView]; + if (buffer_view.buffer < 0) { + return Status(Status::DRACO_ERROR, "Error CopyDataAsUint32() buffer < 0."); + } + + const tinygltf::Buffer &buffer = model.buffers[buffer_view.buffer]; + + const uint8_t *const data_start = + buffer.data.data() + buffer_view.byteOffset + accessor.byteOffset; + const int byte_stride = accessor.ByteStride(buffer_view); + const int component_size = + tinygltf::GetComponentSizeInBytes(accessor.componentType); + const int num_components = + TinyGltfUtils::GetNumComponentsForType(accessor.type); + const int num_elements = accessor.count * num_components; + + std::vector output; + output.resize(num_elements); + + int out_index = 0; + const uint8_t *data = data_start; + for (int i = 0; i < accessor.count; ++i) { + for (int c = 0; c < num_components; ++c) { + uint32_t value = 0; + memcpy(&value, data + (c * component_size), component_size); + output[out_index++] = value; + } + + data += byte_stride; + } + + return output; +} + +// Specialization for arithmetic types. +template < + typename TypeT, + typename std::enable_if::value>::type * = nullptr> +StatusOr> CopyDataAs(const tinygltf::Model &model, + const tinygltf::Accessor &accessor) { + if (std::is_same::value) { + if (TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE != accessor.componentType) { + return ErrorStatus("Accessor data cannot be converted to Uint8."); + } + } else if (std::is_same::value) { + if (TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE != accessor.componentType && + TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT != accessor.componentType) { + return ErrorStatus("Accessor data cannot be converted to Uint16."); + } + } else if (std::is_same::value) { + if (TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE != accessor.componentType && + TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT != accessor.componentType && + TINYGLTF_COMPONENT_TYPE_UNSIGNED_INT != accessor.componentType) { + return ErrorStatus("Accessor data cannot be converted to Uint32."); + } + } else if (std::is_same::value) { + if (TINYGLTF_COMPONENT_TYPE_FLOAT != accessor.componentType) { + return ErrorStatus("Accessor data cannot be converted to Float."); + } + } + if (accessor.bufferView < 0) { + return Status(Status::DRACO_ERROR, "Error CopyDataAs() bufferView < 0."); + } + + const tinygltf::BufferView &buffer_view = + model.bufferViews[accessor.bufferView]; + if (buffer_view.buffer < 0) { + return Status(Status::DRACO_ERROR, "Error CopyDataAs() buffer < 0."); + } + + const tinygltf::Buffer &buffer = model.buffers[buffer_view.buffer]; + + const uint8_t *const data_start = + buffer.data.data() + buffer_view.byteOffset + accessor.byteOffset; + const int byte_stride = accessor.ByteStride(buffer_view); + const int component_size = + tinygltf::GetComponentSizeInBytes(accessor.componentType); + + std::vector output; + output.resize(accessor.count); + + const int num_components = + TinyGltfUtils::GetNumComponentsForType(accessor.type); + int out_index = 0; + const uint8_t *data = data_start; + for (int i = 0; i < accessor.count; ++i) { + for (int c = 0; c < num_components; ++c) { + TypeT value = 0; + memcpy(&value, data + (c * component_size), component_size); + output[out_index++] = value; + } + data += byte_stride; + } + return output; +} + +// Specialization for remaining types is used for draco::VectorD. +template ::value>::type * = + nullptr> +StatusOr> CopyDataAs(const tinygltf::Model &model, + const tinygltf::Accessor &accessor) { + const int num_components = + TinyGltfUtils::GetNumComponentsForType(accessor.type); + if (num_components != TypeT::dimension) { + return Status(Status::DRACO_ERROR, + "Dimension does not equal num components."); + } + if (accessor.bufferView < 0) { + return Status(Status::DRACO_ERROR, "Error CopyDataAs() bufferView < 0."); + } + + const tinygltf::BufferView &buffer_view = + model.bufferViews[accessor.bufferView]; + if (buffer_view.buffer < 0) { + return Status(Status::DRACO_ERROR, "Error CopyDataAs() buffer < 0."); + } + + const tinygltf::Buffer &buffer = model.buffers[buffer_view.buffer]; + + const uint8_t *const data_start = + buffer.data.data() + buffer_view.byteOffset + accessor.byteOffset; + const int byte_stride = accessor.ByteStride(buffer_view); + const int component_size = + tinygltf::GetComponentSizeInBytes(accessor.componentType); + + std::vector output; + output.resize(accessor.count); + + const uint8_t *data = data_start; + for (int i = 0; i < accessor.count; ++i) { + TypeT values; + for (int c = 0; c < num_components; ++c) { + memcpy(&values[c], data + (c * component_size), component_size); + } + output[i] = values; + data += byte_stride; + } + return output; +} + +// Copies the data referenced from |buffer_view_id| into |data|. Currently only +// supports a byte stride of 0. I.e. tightly packed. +Status CopyDataFromBufferView(const tinygltf::Model &model, int buffer_view_id, + std::vector *data) { + if (buffer_view_id < 0) { + return ErrorStatus("Error CopyDataFromBufferView() bufferView < 0."); + } + const tinygltf::BufferView &buffer_view = model.bufferViews[buffer_view_id]; + if (buffer_view.buffer < 0) { + return ErrorStatus("Error CopyDataFromBufferView() buffer < 0."); + } + if (buffer_view.byteStride != 0) { + return Status(Status::DRACO_ERROR, "Error buffer view byteStride != 0."); + } + + const tinygltf::Buffer &buffer = model.buffers[buffer_view.buffer]; + const uint8_t *const data_start = buffer.data.data() + buffer_view.byteOffset; + + data->resize(buffer_view.byteLength); + memcpy(&(*data)[0], data_start, buffer_view.byteLength); + return OkStatus(); +} + +// Returns a SourceImage created from |image|. +StatusOr> GetSourceImage( + const tinygltf::Model &model, const tinygltf::Image &image, + const Texture &texture) { + std::unique_ptr source_image(new SourceImage()); + // If the image is in an external file then the buffer view is < 0. + if (image.bufferView >= 0) { + DRACO_RETURN_IF_ERROR(CopyDataFromBufferView( + model, image.bufferView, &source_image->MutableEncodedData())); + } + source_image->set_filename(image.uri); + source_image->set_mime_type(image.mimeType); + + return source_image; +} + +std::unique_ptr GetNodeTrsMatrix(const tinygltf::Node &node) { + std::unique_ptr trsm(new TrsMatrix()); + if (node.matrix.size() == 16) { + Eigen::Matrix4d transformation; + // clang-format off + // |node.matrix| is in the column-major order. + transformation << + node.matrix[0], node.matrix[4], node.matrix[8], node.matrix[12], + node.matrix[1], node.matrix[5], node.matrix[9], node.matrix[13], + node.matrix[2], node.matrix[6], node.matrix[10], node.matrix[14], + node.matrix[3], node.matrix[7], node.matrix[11], node.matrix[15]; + // clang-format on + if (transformation != Eigen::Matrix4d::Identity()) { + trsm->SetMatrix(transformation); + } + } + + if (node.translation.size() == 3) { + const Eigen::Vector3d default_translation(0.0, 0.0, 0.0); + const Eigen::Vector3d node_translation( + node.translation[0], node.translation[1], node.translation[2]); + if (node_translation != default_translation) { + trsm->SetTranslation(node_translation); + } + } + if (node.scale.size() == 3) { + const Eigen::Vector3d default_scale(1.0, 1.0, 1.0); + const Eigen::Vector3d node_scale(node.scale[0], node.scale[1], + node.scale[2]); + if (node_scale != default_scale) { + trsm->SetScale(node_scale); + } + } + if (node.rotation.size() == 4) { + // Eigen quaternion is defined in (w, x, y, z) vs glTF that uses + // (x, y, z, w). + const Eigen::Quaterniond default_rotation(0.0, 0.0, 0.0, 1.0); + const Eigen::Quaterniond node_rotation(node.rotation[3], node.rotation[0], + node.rotation[1], node.rotation[2]); + if (node_rotation != default_rotation) { + trsm->SetRotation(node_rotation); + } + } + + return trsm; +} + +Eigen::Matrix4d UpdateMatrixForNormals( + const Eigen::Matrix4d &transform_matrix) { + Eigen::Matrix3d mat3x3; + // clang-format off + mat3x3 << + transform_matrix(0, 0), transform_matrix(0, 1), transform_matrix(0, 2), + transform_matrix(1, 0), transform_matrix(1, 1), transform_matrix(1, 2), + transform_matrix(2, 0), transform_matrix(2, 1), transform_matrix(2, 2); + // clang-format on + + mat3x3 = mat3x3.inverse().transpose(); + Eigen::Matrix4d mat4x4; + // clang-format off + mat4x4 << mat3x3(0, 0), mat3x3(0, 1), mat3x3(0, 2), 0.0, + mat3x3(1, 0), mat3x3(1, 1), mat3x3(1, 2), 0.0, + mat3x3(2, 0), mat3x3(2, 1), mat3x3(2, 2), 0.0, + 0.0, 0.0, 0.0, 1.0; + // clang-format on + return mat4x4; +} + +float Determinant(const Eigen::Matrix4d &transform_matrix) { + Eigen::Matrix3d mat3x3; + // clang-format off + mat3x3 << + transform_matrix(0, 0), transform_matrix(0, 1), transform_matrix(0, 2), + transform_matrix(1, 0), transform_matrix(1, 1), transform_matrix(1, 2), + transform_matrix(2, 0), transform_matrix(2, 1), transform_matrix(2, 2); + // clang-format on + return mat3x3.determinant(); +} + +bool FileExists(const std::string &filepath, void * /*user_data*/) { + return GetFileSize(filepath) != 0; +} + +bool ReadWholeFile(std::vector *out, std::string *err, + const std::string &filepath, void *user_data) { + if (!ReadFileToBuffer(filepath, out)) { + if (err) { + *err = "Unable to read: " + filepath; + } + return false; + } + if (user_data) { + auto *files_vector = + reinterpret_cast *>(user_data); + files_vector->push_back(filepath); + } + return true; +} + +bool WriteWholeFile(std::string * /*err*/, const std::string &filepath, + const std::vector &contents, + void * /*user_data*/) { + return WriteBufferToFile(contents.data(), contents.size(), filepath); +} + +} // namespace + +GltfDecoder::GltfDecoder() + : next_face_id_(0), + next_point_id_(0), + total_face_indices_count_(0), + total_point_indices_count_(0), + material_att_id_(-1) {} + +StatusOr> GltfDecoder::DecodeFromFile( + const std::string &file_name) { + return DecodeFromFile(file_name, nullptr); +} + +StatusOr> GltfDecoder::DecodeFromFile( + const std::string &file_name, std::vector *mesh_files) { + DRACO_RETURN_IF_ERROR(LoadFile(file_name, mesh_files)); + return BuildMesh(); +} + +StatusOr> GltfDecoder::DecodeFromBuffer( + DecoderBuffer *buffer) { + DRACO_RETURN_IF_ERROR(LoadBuffer(*buffer)); + return BuildMesh(); +} + +StatusOr> GltfDecoder::DecodeFromFileToScene( + const std::string &file_name) { + return DecodeFromFileToScene(file_name, nullptr); +} + +StatusOr> GltfDecoder::DecodeFromFileToScene( + const std::string &file_name, std::vector *scene_files) { + DRACO_RETURN_IF_ERROR(LoadFile(file_name, scene_files)); + scene_ = std::unique_ptr(new Scene()); + DRACO_RETURN_IF_ERROR(DecodeGltfToScene()); + return std::move(scene_); +} + +StatusOr> GltfDecoder::DecodeFromBufferToScene( + DecoderBuffer *buffer) { + DRACO_RETURN_IF_ERROR(LoadBuffer(*buffer)); + scene_ = std::unique_ptr(new Scene()); + DRACO_RETURN_IF_ERROR(DecodeGltfToScene()); + return std::move(scene_); +} + +Status GltfDecoder::LoadFile(const std::string &file_name, + std::vector *input_files) { + const std::string extension = LowercaseFileExtension(file_name); + tinygltf::TinyGLTF loader; + std::string err; + std::string warn; + + const tinygltf::FsCallbacks fs_callbacks = { + &FileExists, + // TinyGLTF's ExpandFilePath does not do filesystem i/o, so it's safe to + // use in all environments. + &tinygltf::ExpandFilePath, &ReadWholeFile, &WriteWholeFile, + reinterpret_cast(input_files)}; + + loader.SetFsCallbacks(fs_callbacks); + + if (extension == "glb") { + if (!loader.LoadBinaryFromFile(&gltf_model_, &err, &warn, file_name)) { + return Status(Status::DRACO_ERROR, + "TinyGLTF failed to load glb file: " + err); + } + } else if (extension == "gltf") { + if (!loader.LoadASCIIFromFile(&gltf_model_, &err, &warn, file_name)) { + return Status(Status::DRACO_ERROR, + "TinyGLTF failed to load glTF file: " + err); + } + } else { + return Status(Status::DRACO_ERROR, "Unknown input file extension."); + } + DRACO_RETURN_IF_ERROR(CheckUnsupportedFeatures()); + input_file_name_ = file_name; + return OkStatus(); +} + +Status GltfDecoder::LoadBuffer(const DecoderBuffer &buffer) { + tinygltf::TinyGLTF loader; + std::string err; + std::string warn; + + if (!loader.LoadBinaryFromMemory( + &gltf_model_, &err, &warn, + reinterpret_cast(buffer.data_head()), + buffer.remaining_size())) { + return Status(Status::DRACO_ERROR, + "TinyGLTF failed to load glb buffer: " + err); + } + DRACO_RETURN_IF_ERROR(CheckUnsupportedFeatures()); + input_file_name_.clear(); + return OkStatus(); +} + +StatusOr> GltfDecoder::BuildMesh() { + DRACO_RETURN_IF_ERROR(GatherAttributeAndMaterialStats()); + if (total_face_indices_count_ > 0 && total_point_indices_count_ > 0) { + return ErrorStatus( + "Decoding to mesh can't handle triangle and point primitives at the " + "same time."); + } + if (total_face_indices_count_ > 0) { + mb_.Start(total_face_indices_count_ / 3); + DRACO_RETURN_IF_ERROR(AddAttributesToDracoMesh(&mb_)); + } else { + pb_.Start(total_point_indices_count_); + DRACO_RETURN_IF_ERROR(AddAttributesToDracoMesh(&pb_)); + } + + for (const tinygltf::Scene &scene : gltf_model_.scenes) { + for (int i = 0; i < scene.nodes.size(); ++i) { + const Eigen::Matrix4d parent_matrix = Eigen::Matrix4d::Identity(); + DRACO_RETURN_IF_ERROR(DecodeNode(scene.nodes[i], parent_matrix)); + } + } + DRACO_ASSIGN_OR_RETURN( + std::unique_ptr mesh, + BuildMeshFromBuilder(total_face_indices_count_ > 0, &mb_, &pb_)); + + DRACO_RETURN_IF_ERROR(CopyTextures(mesh.get())); + SetAttributePropertiesOnDracoMesh(mesh.get()); + DRACO_RETURN_IF_ERROR(AddMaterialsToDracoMesh(mesh.get())); + DRACO_RETURN_IF_ERROR(AddMeshFeaturesToDracoMesh(mesh.get())); + DRACO_RETURN_IF_ERROR(AddStructuralMetadataToGeometry(mesh.get())); + MoveNonMaterialTextures(mesh.get()); + return mesh; +} + +Status GltfDecoder::AddMeshFeaturesToDracoMesh(Mesh *mesh) { + for (const tinygltf::Scene &scene : gltf_model_.scenes) { + for (int i = 0; i < scene.nodes.size(); ++i) { + DRACO_RETURN_IF_ERROR(AddMeshFeaturesToDracoMesh(scene.nodes[i], mesh)); + } + } + return OkStatus(); +} + +Status GltfDecoder::AddMeshFeaturesToDracoMesh(int node_index, Mesh *mesh) { + const tinygltf::Node &node = gltf_model_.nodes[node_index]; + if (node.mesh >= 0) { + const tinygltf::Mesh &gltf_mesh = gltf_model_.meshes[node.mesh]; + for (const auto &primitive : gltf_mesh.primitives) { + // Decode mesh feature ID sets if present in this primitive. + DRACO_RETURN_IF_ERROR(DecodeMeshFeatures( + primitive, &mesh->GetMaterialLibrary().MutableTextureLibrary(), + mesh)); + } + } + for (int i = 0; i < node.children.size(); ++i) { + DRACO_RETURN_IF_ERROR(AddMeshFeaturesToDracoMesh(node.children[i], mesh)); + } + return OkStatus(); +} + +Status GltfDecoder::CheckUnsupportedFeatures() { + // Check for morph targets. + for (const auto &mesh : gltf_model_.meshes) { + for (const auto &primitive : mesh.primitives) { + if (!primitive.targets.empty()) { + return Status(Status::UNSUPPORTED_FEATURE, + "Morph targets are unsupported."); + } + } + } + + // Check for sparse accessors. + for (const auto &accessor : gltf_model_.accessors) { + if (accessor.sparse.isSparse) { + return Status(Status::UNSUPPORTED_FEATURE, + "Sparse accessors are unsupported."); + } + } + + // Check for extensions. + for (const auto &extension : gltf_model_.extensionsRequired) { + if (extension != "KHR_materials_unlit" && + extension != "KHR_texture_transform" && + extension != "KHR_draco_mesh_compression") { + return Status(Status::UNSUPPORTED_FEATURE, + extension + " is unsupported."); + } + } + return OkStatus(); +} + +Status GltfDecoder::DecodeNode(int node_index, + const Eigen::Matrix4d &parent_matrix) { + const tinygltf::Node &node = gltf_model_.nodes[node_index]; + const std::unique_ptr trsm = GetNodeTrsMatrix(node); + const Eigen::Matrix4d node_matrix = + parent_matrix * trsm->ComputeTransformationMatrix(); + + if (node.mesh >= 0) { + const tinygltf::Mesh &mesh = gltf_model_.meshes[node.mesh]; + for (const auto &primitive : mesh.primitives) { + DRACO_RETURN_IF_ERROR(DecodePrimitive(primitive, node_matrix)); + } + } + for (int i = 0; i < node.children.size(); ++i) { + DRACO_RETURN_IF_ERROR(DecodeNode(node.children[i], node_matrix)); + } + return OkStatus(); +} + +StatusOr GltfDecoder::DecodePrimitiveAttributeCount( + const tinygltf::Primitive &primitive) const { + // Use the first primitive attribute as all attributes have the same entry + // count according to glTF 2.0 spec. + if (primitive.attributes.empty()) { + return Status(Status::DRACO_ERROR, "Primitive has no attributes."); + } + const tinygltf::Accessor &accessor = + gltf_model_.accessors[primitive.attributes.begin()->second]; + return accessor.count; +} + +StatusOr GltfDecoder::DecodePrimitiveIndicesCount( + const tinygltf::Primitive &primitive) const { + if (primitive.indices < 0) { + // Primitive has implicit indices [0, 1, 2, 3, ...]. Determine indices count + // based on entry count of a primitive attribute. + return DecodePrimitiveAttributeCount(primitive); + } + const tinygltf::Accessor &indices = gltf_model_.accessors[primitive.indices]; + return indices.count; +} + +StatusOr> GltfDecoder::DecodePrimitiveIndices( + const tinygltf::Primitive &primitive) const { + std::vector indices_data; + if (primitive.indices < 0) { + // Primitive has implicit indices [0, 1, 2, 3, ...]. Create indices based on + // entry count of a primitive attribute. + DRACO_ASSIGN_OR_RETURN(const int num_vertices, + DecodePrimitiveAttributeCount(primitive)); + indices_data.reserve(num_vertices); + for (int i = 0; i < num_vertices; i++) { + indices_data.push_back(i); + } + } else { + // Get indices from the primitive's indices property. + const tinygltf::Accessor &indices = + gltf_model_.accessors[primitive.indices]; + if (indices.count <= 0) { + return Status(Status::DRACO_ERROR, "Could not convert indices."); + } + DRACO_ASSIGN_OR_RETURN(indices_data, + CopyDataAsUint32(gltf_model_, indices)); + } + return indices_data; +} + +Status GltfDecoder::DecodePrimitive(const tinygltf::Primitive &primitive, + const Eigen::Matrix4d &transform_matrix) { + if (primitive.mode != TINYGLTF_MODE_TRIANGLES && + primitive.mode != TINYGLTF_MODE_POINTS) { + return Status(Status::DRACO_ERROR, + "Primitive does not contain triangles or points."); + } + + // Store the transformation scale of this primitive loading as draco::Mesh. + if (scene_ == nullptr) { + // TODO(vytyaz): Do something for non-uniform scaling. + const float scale = transform_matrix.col(0).norm(); + gltf_primitive_material_to_scales_[primitive.material].push_back(scale); + } + + // Handle indices first. + DRACO_ASSIGN_OR_RETURN(const std::vector indices_data, + DecodePrimitiveIndices(primitive)); + const int number_of_faces = indices_data.size() / 3; + const int number_of_points = indices_data.size(); + + for (const auto &attribute : primitive.attributes) { + const tinygltf::Accessor &accessor = + gltf_model_.accessors[attribute.second]; + + const int att_id = + attribute_name_to_draco_mesh_attribute_id_[attribute.first]; + if (att_id == -1) { + continue; + } + + if (primitive.mode == TINYGLTF_MODE_TRIANGLES) { + DRACO_RETURN_IF_ERROR(AddAttributeValuesToBuilder( + attribute.first, accessor, indices_data, att_id, number_of_faces, + transform_matrix, &mb_)); + } else { + DRACO_RETURN_IF_ERROR(AddAttributeValuesToBuilder( + attribute.first, accessor, indices_data, att_id, number_of_points, + transform_matrix, &pb_)); + } + } + + // Add the material data only if there is more than one material. + if (gltf_primitive_material_to_draco_material_.size() > 1) { + const int material_index = primitive.material; + const auto it = + gltf_primitive_material_to_draco_material_.find(material_index); + if (it != gltf_primitive_material_to_draco_material_.end()) { + if (primitive.mode == TINYGLTF_MODE_TRIANGLES) { + DRACO_RETURN_IF_ERROR( + AddMaterialDataToBuilder(it->second, number_of_faces, &mb_)); + } else { + DRACO_RETURN_IF_ERROR( + AddMaterialDataToBuilder(it->second, number_of_points, &pb_)); + } + } + } + + next_face_id_ += number_of_faces; + next_point_id_ += number_of_points; + return OkStatus(); +} + +Status GltfDecoder::NodeGatherAttributeAndMaterialStats( + const tinygltf::Node &node) { + if (node.mesh >= 0) { + const tinygltf::Mesh &mesh = gltf_model_.meshes[node.mesh]; + for (const auto &primitive : mesh.primitives) { + DRACO_RETURN_IF_ERROR(AccumulatePrimitiveStats(primitive)); + + const auto it = + gltf_primitive_material_to_draco_material_.find(primitive.material); + if (it == gltf_primitive_material_to_draco_material_.end()) { + gltf_primitive_material_to_draco_material_[primitive.material] = + gltf_primitive_material_to_draco_material_.size(); + } + } + } + for (int i = 0; i < node.children.size(); ++i) { + const tinygltf::Node &child = gltf_model_.nodes[node.children[i]]; + DRACO_RETURN_IF_ERROR(NodeGatherAttributeAndMaterialStats(child)); + } + + return OkStatus(); +} + +Status GltfDecoder::GatherAttributeAndMaterialStats() { + for (const auto &scene : gltf_model_.scenes) { + for (int i = 0; i < scene.nodes.size(); ++i) { + const tinygltf::Node &node = gltf_model_.nodes[scene.nodes[i]]; + DRACO_RETURN_IF_ERROR(NodeGatherAttributeAndMaterialStats(node)); + } + } + return OkStatus(); +} + +void GltfDecoder::SumAttributeStats(const std::string &attribute_name, + int count) { + // We know that there must be a valid entry for |attribute_name| at this time. + mesh_attribute_data_[attribute_name].total_attribute_counts += count; +} + +Status GltfDecoder::CheckTypes(const std::string &attribute_name, + int component_type, int type, bool normalized) { + auto it_mad = mesh_attribute_data_.find(attribute_name); + + if (it_mad == mesh_attribute_data_.end()) { + MeshAttributeData mad; + mad.component_type = component_type; + mad.attribute_type = type; + mad.normalized = normalized; + mesh_attribute_data_[attribute_name] = mad; + return OkStatus(); + } + if (it_mad->second.component_type != component_type) { + return Status( + Status::DRACO_ERROR, + attribute_name + " attribute component type does not match previous."); + } + if (it_mad->second.attribute_type != type) { + return Status(Status::DRACO_ERROR, + attribute_name + " attribute type does not match previous."); + } + if (it_mad->second.normalized != normalized) { + return Status( + Status::DRACO_ERROR, + attribute_name + + " attribute normalized property does not match previous."); + } + + return OkStatus(); +} + +Status GltfDecoder::AccumulatePrimitiveStats( + const tinygltf::Primitive &primitive) { + DRACO_ASSIGN_OR_RETURN(const int indices_count, + DecodePrimitiveIndicesCount(primitive)); + if (primitive.mode == TINYGLTF_MODE_TRIANGLES) { + total_face_indices_count_ += indices_count; + } else if (primitive.mode == TINYGLTF_MODE_POINTS) { + total_point_indices_count_ += indices_count; + } else { + return ErrorStatus("Unsupported primitive indices mode."); + } + + for (const auto &attribute : primitive.attributes) { + if (attribute.second >= gltf_model_.accessors.size()) { + return ErrorStatus("Invalid accessor."); + } + const tinygltf::Accessor &accessor = + gltf_model_.accessors[attribute.second]; + + DRACO_RETURN_IF_ERROR(CheckTypes(attribute.first, accessor.componentType, + accessor.type, accessor.normalized)); + SumAttributeStats(attribute.first, accessor.count); + } + return OkStatus(); +} + +template +Status GltfDecoder::AddAttributesToDracoMesh(BuilderT *builder) { + for (const auto &attribute : mesh_attribute_data_) { + const GeometryAttribute::Type draco_att_type = + GltfAttributeToDracoAttribute(attribute.first); + if (draco_att_type == GeometryAttribute::INVALID) { + // Map an invalid attribute to attribute id -1 that will be ignored and + // not included in the Draco mesh. + attribute_name_to_draco_mesh_attribute_id_[attribute.first] = -1; + continue; + } + DRACO_ASSIGN_OR_RETURN( + const int att_id, + AddAttribute(draco_att_type, attribute.second.component_type, + attribute.second.attribute_type, builder)); + attribute_name_to_draco_mesh_attribute_id_[attribute.first] = att_id; + } + + // Add the material attribute. + if (gltf_model_.materials.size() > 1) { + draco::DataType component_type = DT_UINT32; + if (gltf_model_.materials.size() < 256) { + component_type = DT_UINT8; + } else if (gltf_model_.materials.size() < (1 << 16)) { + component_type = DT_UINT16; + } + material_att_id_ = + builder->AddAttribute(GeometryAttribute::MATERIAL, 1, component_type); + } + + return OkStatus(); +} + +template +Status GltfDecoder::AddAttributeValuesToBuilder( + const std::string &attribute_name, const tinygltf::Accessor &accessor, + const std::vector &indices_data, int att_id, + int number_of_elements, const Eigen::Matrix4d &transform_matrix, + BuilderT *builder) { + const bool reverse_winding = Determinant(transform_matrix) < 0; + if (attribute_name == "TEXCOORD_0" || attribute_name == "TEXCOORD_1") { + DRACO_RETURN_IF_ERROR(AddTexCoordToBuilder(accessor, indices_data, att_id, + number_of_elements, + reverse_winding, builder)); + } else if (attribute_name == "TANGENT") { + const Eigen::Matrix4d matrix = UpdateMatrixForNormals(transform_matrix); + DRACO_RETURN_IF_ERROR(AddTangentToBuilder(accessor, indices_data, att_id, + number_of_elements, matrix, + reverse_winding, builder)); + } else if (attribute_name == "POSITION" || attribute_name == "NORMAL") { + const Eigen::Matrix4d matrix = + (attribute_name == "NORMAL") ? UpdateMatrixForNormals(transform_matrix) + : transform_matrix; + const bool normalize = (attribute_name == "NORMAL"); + DRACO_RETURN_IF_ERROR(AddTransformedDataToBuilder( + accessor, indices_data, att_id, number_of_elements, matrix, normalize, + reverse_winding, builder)); + } else if (attribute_name.rfind("_FEATURE_ID_") == 0) { + DRACO_RETURN_IF_ERROR(AddFeatureIdToBuilder( + accessor, indices_data, att_id, number_of_elements, reverse_winding, + attribute_name, builder)); + } else { + DRACO_RETURN_IF_ERROR(AddAttributeDataByTypes(accessor, indices_data, + att_id, number_of_elements, + reverse_winding, builder)); + } + return OkStatus(); +} + +template +Status GltfDecoder::AddTangentToBuilder( + const tinygltf::Accessor &accessor, + const std::vector &indices_data, int att_id, + int number_of_elements, const Eigen::Matrix4d &transform_matrix, + bool reverse_winding, BuilderT *builder) { + DRACO_ASSIGN_OR_RETURN( + std::vector data, + TinyGltfUtils::CopyDataAsFloat(gltf_model_, accessor)); + + for (int v = 0; v < data.size(); ++v) { + Eigen::Vector4d vec4(data[v][0], data[v][1], data[v][2], 1); + vec4 = transform_matrix * vec4; + + // Normalize the data. + Eigen::Vector3d vec3(vec4[0], vec4[1], vec4[2]); + vec3 = vec3.normalized(); + for (int i = 0; i < 3; ++i) { + vec4[i] = vec3[i]; + } + + // Add back the original w component. + vec4[3] = data[v][3]; + for (int i = 0; i < 4; ++i) { + data[v][i] = vec4[i]; + } + } + + SetValuesForBuilder(indices_data, att_id, number_of_elements, data, + reverse_winding, builder); + return OkStatus(); +} + +template +Status GltfDecoder::AddTexCoordToBuilder( + const tinygltf::Accessor &accessor, + const std::vector &indices_data, int att_id, + int number_of_elements, bool reverse_winding, BuilderT *builder) { + DRACO_ASSIGN_OR_RETURN( + std::vector data, + TinyGltfUtils::CopyDataAsFloat(gltf_model_, accessor)); + + // glTF stores texture coordinates flipped on the horizontal axis compared to + // how Draco stores texture coordinates. + for (auto &uv : data) { + uv[1] = 1.0 - uv[1]; + } + + SetValuesForBuilder(indices_data, att_id, number_of_elements, data, + reverse_winding, builder); + return OkStatus(); +} + +template +Status GltfDecoder::AddFeatureIdToBuilder( + const tinygltf::Accessor &accessor, + const std::vector &indices_data, int att_id, + int number_of_elements, bool reverse_winding, + const std::string &attribute_name, BuilderT *builder) { + // Check that the feature ID attribute has correct type. + const int num_components = + TinyGltfUtils::GetNumComponentsForType(accessor.type); + if (num_components != 1) { + return ErrorStatus("Invalid feature ID attribute type."); + } + const draco::DataType draco_component_type = + GltfComponentTypeToDracoType(accessor.componentType); + if (draco_component_type != DT_UINT8 && draco_component_type != DT_UINT16 && + draco_component_type != DT_FLOAT32) { + return ErrorStatus("Invalid feature ID attribute component type."); + } + + // Set feature ID attribute values to mesh faces. + DRACO_RETURN_IF_ERROR(AddAttributeDataByTypes(accessor, indices_data, att_id, + number_of_elements, + reverse_winding, builder)); + + // Store feature ID attribute name with index like _FEATURE_ID_5 in Draco + // attribute metadata. + std::unique_ptr metadata(new draco::AttributeMetadata()); + metadata->AddEntryString("attribute_name", attribute_name); + builder->AddAttributeMetadata(att_id, std::move(metadata)); + return OkStatus(); +} + +template +Status GltfDecoder::AddTransformedDataToBuilder( + const tinygltf::Accessor &accessor, + const std::vector &indices_data, int att_id, + int number_of_elements, const Eigen::Matrix4d &transform_matrix, + bool normalize, bool reverse_winding, BuilderT *builder) { + DRACO_ASSIGN_OR_RETURN( + std::vector data, + TinyGltfUtils::CopyDataAsFloat(gltf_model_, accessor)); + + for (int v = 0; v < data.size(); ++v) { + Eigen::Vector4d vec4(data[v][0], data[v][1], data[v][2], 1); + vec4 = transform_matrix * vec4; + Eigen::Vector3d vec3(vec4[0], vec4[1], vec4[2]); + if (normalize) { + vec3 = vec3.normalized(); + } + for (int i = 0; i < 3; ++i) { + data[v][i] = vec3[i]; + } + } + + SetValuesForBuilder(indices_data, att_id, number_of_elements, data, + reverse_winding, builder); + return OkStatus(); +} + +template +void GltfDecoder::SetValuesForBuilder(const std::vector &indices_data, + int att_id, int number_of_elements, + const std::vector &data, + bool reverse_winding, + TriangleSoupMeshBuilder *builder) { + SetValuesPerFace(indices_data, att_id, number_of_elements, data, + reverse_winding, builder); +} + +template +void GltfDecoder::SetValuesForBuilder(const std::vector &indices_data, + int att_id, int number_of_elements, + const std::vector &data, + bool reverse_winding, + PointCloudBuilder *builder) { + for (int i = 0; i < number_of_elements; ++i) { + const uint32_t v_id = indices_data[i]; + const PointIndex pi(v_id + next_point_id_); + builder->SetAttributeValueForPoint(att_id, pi, + GetDataContentAddress(data[v_id])); + } +} + +template +void GltfDecoder::SetValuesPerFace(const std::vector &indices_data, + int att_id, int number_of_faces, + const std::vector &data, + bool reverse_winding, + TriangleSoupMeshBuilder *mb) { + for (int f = 0; f < number_of_faces; ++f) { + const int base_corner = f * 3; + const uint32_t v_id = indices_data[base_corner]; + const int next_offset = reverse_winding ? 2 : 1; + const int prev_offset = reverse_winding ? 1 : 2; + const uint32_t v_next_id = indices_data[base_corner + next_offset]; + const uint32_t v_prev_id = indices_data[base_corner + prev_offset]; + + const FaceIndex face_index(f + next_face_id_); + mb->SetAttributeValuesForFace(att_id, face_index, + GetDataContentAddress(data[v_id]), + GetDataContentAddress(data[v_next_id]), + GetDataContentAddress(data[v_prev_id])); + } +} + +// Get the address of data content for arithmetic types |T|. +template +const void *GetDataContentAddressImpl(const T &data, + std::true_type /* is_arithmetic */) { + return &data; +} + +// Get the address of data content for vector types |T|. +template +const void *GetDataContentAddressImpl(const T &data, + std::false_type /* is_arithmetic */) { + return data.data(); +} + +template +const void *GltfDecoder::GetDataContentAddress(const T &data) const { + return GetDataContentAddressImpl(data, std::is_arithmetic()); +} + +template +Status GltfDecoder::AddAttributeDataByTypes( + const tinygltf::Accessor &accessor, + const std::vector &indices_data, int att_id, + int number_of_elements, bool reverse_winding, BuilderT *builder) { + typedef VectorD Vector2u8i; + typedef VectorD Vector3u8i; + typedef VectorD Vector4u8i; + typedef VectorD Vector2u16i; + typedef VectorD Vector3u16i; + typedef VectorD Vector4u16i; + switch (accessor.type) { + case TINYGLTF_TYPE_SCALAR: + switch (accessor.componentType) { + case TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE: { + DRACO_ASSIGN_OR_RETURN(std::vector data, + CopyDataAs(gltf_model_, accessor)); + SetValuesForBuilder(indices_data, att_id, number_of_elements, + data, reverse_winding, builder); + } break; + case TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT: { + DRACO_ASSIGN_OR_RETURN(std::vector data, + CopyDataAs(gltf_model_, accessor)); + SetValuesForBuilder(indices_data, att_id, + number_of_elements, data, + reverse_winding, builder); + } break; + case TINYGLTF_COMPONENT_TYPE_UNSIGNED_INT: { + DRACO_ASSIGN_OR_RETURN(std::vector data, + CopyDataAs(gltf_model_, accessor)); + SetValuesForBuilder(indices_data, att_id, + number_of_elements, data, + reverse_winding, builder); + } break; + case TINYGLTF_COMPONENT_TYPE_FLOAT: { + DRACO_ASSIGN_OR_RETURN(std::vector data, + CopyDataAs(gltf_model_, accessor)); + SetValuesForBuilder(indices_data, att_id, number_of_elements, + data, reverse_winding, builder); + } break; + default: + return ErrorStatus("Add attribute data, unknown component type."); + } + break; + case TINYGLTF_TYPE_VEC2: + switch (accessor.componentType) { + case TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE: { + DRACO_ASSIGN_OR_RETURN(std::vector data, + CopyDataAs(gltf_model_, accessor)); + SetValuesForBuilder(indices_data, att_id, + number_of_elements, data, + reverse_winding, builder); + } break; + case TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT: { + DRACO_ASSIGN_OR_RETURN( + std::vector data, + CopyDataAs(gltf_model_, accessor)); + SetValuesForBuilder(indices_data, att_id, + number_of_elements, data, + reverse_winding, builder); + } break; + case TINYGLTF_COMPONENT_TYPE_FLOAT: { + DRACO_ASSIGN_OR_RETURN( + std::vector data, + TinyGltfUtils::CopyDataAsFloat(gltf_model_, accessor)); + SetValuesForBuilder(indices_data, att_id, + number_of_elements, data, + reverse_winding, builder); + } break; + default: + return Status(Status::DRACO_ERROR, + "Add attribute data, unknown component type."); + } + break; + case TINYGLTF_TYPE_VEC3: + switch (accessor.componentType) { + case TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE: { + DRACO_ASSIGN_OR_RETURN(std::vector data, + CopyDataAs(gltf_model_, accessor)); + SetValuesForBuilder(indices_data, att_id, + number_of_elements, data, + reverse_winding, builder); + } break; + case TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT: { + DRACO_ASSIGN_OR_RETURN( + std::vector data, + CopyDataAs(gltf_model_, accessor)); + SetValuesForBuilder(indices_data, att_id, + number_of_elements, data, + reverse_winding, builder); + } break; + case TINYGLTF_COMPONENT_TYPE_FLOAT: { + DRACO_ASSIGN_OR_RETURN( + std::vector data, + TinyGltfUtils::CopyDataAsFloat(gltf_model_, accessor)); + SetValuesForBuilder(indices_data, att_id, + number_of_elements, data, + reverse_winding, builder); + } break; + default: + return Status(Status::DRACO_ERROR, + "Add attribute data, unknown component type."); + } + break; + case TINYGLTF_TYPE_VEC4: + switch (accessor.componentType) { + case TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE: { + DRACO_ASSIGN_OR_RETURN(std::vector data, + CopyDataAs(gltf_model_, accessor)); + SetValuesForBuilder(indices_data, att_id, + number_of_elements, data, + reverse_winding, builder); + } break; + case TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT: { + DRACO_ASSIGN_OR_RETURN( + std::vector data, + CopyDataAs(gltf_model_, accessor)); + SetValuesForBuilder(indices_data, att_id, + number_of_elements, data, + reverse_winding, builder); + } break; + case TINYGLTF_COMPONENT_TYPE_FLOAT: { + DRACO_ASSIGN_OR_RETURN( + std::vector data, + TinyGltfUtils::CopyDataAsFloat(gltf_model_, accessor)); + SetValuesForBuilder(indices_data, att_id, + number_of_elements, data, + reverse_winding, builder); + } break; + default: + return Status(Status::DRACO_ERROR, + "Add attribute data, unknown component type."); + } + break; + default: + return Status(Status::DRACO_ERROR, "Add attribute data, unknown type."); + } + return OkStatus(); +} + +template +Status GltfDecoder::CopyTextures(T *owner) { + for (int i = 0; i < gltf_model_.images.size(); ++i) { + const tinygltf::Image &image = gltf_model_.images[i]; + if (image.width == -1 || image.height == -1 || image.component == -1) { + // TinyGLTF does not return an error when it cannot find an image. It will + // add an image with negative values. + return Status(Status::DRACO_ERROR, "Error loading image."); + } + + std::unique_ptr draco_texture(new Texture()); + + // Update mapping between glTF images and textures in the texture library. + gltf_image_to_draco_texture_[i] = draco_texture.get(); + + DRACO_ASSIGN_OR_RETURN(std::unique_ptr source_image, + GetSourceImage(gltf_model_, image, *draco_texture)); + if (source_image->encoded_data().empty() && + !source_image->filename().empty()) { + // Update filename of source image to be relative of the glTF file. + std::string dirname; + std::string basename; + SplitPath(input_file_name_, &dirname, &basename); + source_image->set_filename(dirname + "/" + source_image->filename()); + } + draco_texture->set_source_image(*source_image); + + owner->GetMaterialLibrary().MutableTextureLibrary().PushTexture( + std::move(draco_texture)); + } + return OkStatus(); +} + +void GltfDecoder::SetAttributePropertiesOnDracoMesh(Mesh *mesh) { + for (const auto &mad : mesh_attribute_data_) { + const int att_id = attribute_name_to_draco_mesh_attribute_id_[mad.first]; + if (att_id == -1) { + continue; + } + if (mad.second.normalized) { + mesh->attribute(att_id)->set_normalized(true); + } + } +} + +Status GltfDecoder::AddMaterialsToDracoMesh(Mesh *mesh) { + bool is_normal_map_used = false; + + int default_material_index = -1; + const auto it = gltf_primitive_material_to_draco_material_.find(-1); + if (it != gltf_primitive_material_to_draco_material_.end()) { + default_material_index = it->second; + } + + int output_material_index = 0; + for (int input_material_index = 0; + input_material_index < gltf_model_.materials.size(); + ++input_material_index) { + if (default_material_index == input_material_index) { + // Insert a default material here for primitives that did not have a + // material index. + mesh->GetMaterialLibrary().MutableMaterial(output_material_index++); + } + + Material *const output_material = + mesh->GetMaterialLibrary().MutableMaterial(output_material_index++); + DRACO_RETURN_IF_ERROR( + AddGltfMaterial(input_material_index, output_material)); + if (output_material->GetTextureMapByType( + TextureMap::NORMAL_TANGENT_SPACE)) { + is_normal_map_used = true; + } + } + + return OkStatus(); +} + +template +Status GltfDecoder::AddMaterialDataToBuilder(int material_value, + int number_of_elements, + BuilderT *builder) { + if (gltf_primitive_material_to_draco_material_.size() < 256) { + const uint8_t typed_material_value = material_value; + DRACO_RETURN_IF_ERROR(AddMaterialDataToBuilderInternal( + typed_material_value, number_of_elements, builder)); + } else if (gltf_primitive_material_to_draco_material_.size() < (1 << 16)) { + const uint16_t typed_material_value = material_value; + DRACO_RETURN_IF_ERROR(AddMaterialDataToBuilderInternal( + typed_material_value, number_of_elements, builder)); + } else { + const uint32_t typed_material_value = material_value; + DRACO_RETURN_IF_ERROR(AddMaterialDataToBuilderInternal( + typed_material_value, number_of_elements, builder)); + } + return OkStatus(); +} + +template +Status GltfDecoder::AddMaterialDataToBuilderInternal( + T material_value, int number_of_faces, TriangleSoupMeshBuilder *builder) { + for (int f = 0; f < number_of_faces; ++f) { + const FaceIndex face_index(f + next_face_id_); + builder->SetPerFaceAttributeValueForFace(material_att_id_, face_index, + &material_value); + } + return OkStatus(); +} + +template +Status GltfDecoder::AddMaterialDataToBuilderInternal( + T material_value, int number_of_points, PointCloudBuilder *builder) { + for (int pi = 0; pi < number_of_points; ++pi) { + const PointIndex point_index(pi + next_point_id_); + builder->SetAttributeValueForPoint(material_att_id_, point_index, + &material_value); + } + return OkStatus(); +} + +Status GltfDecoder::CheckAndAddTextureToDracoMaterial( + int texture_index, int tex_coord_attribute_index, + const tinygltf::ExtensionMap &tex_info_ext, Material *material, + TextureMap::Type type) { + if (texture_index < 0) { + return OkStatus(); + } + + const tinygltf::Texture &input_texture = gltf_model_.textures[texture_index]; + int source_index = input_texture.source; + + const auto texture_it = gltf_image_to_draco_texture_.find(source_index); + if (texture_it != gltf_image_to_draco_texture_.end()) { + Texture *const texture = texture_it->second; + // Default GLTF 2.0 sampler uses REPEAT mode along both S and T directions. + TextureMap::WrappingMode wrapping_mode(TextureMap::REPEAT); + TextureMap::FilterType min_filter = TextureMap::UNSPECIFIED; + TextureMap::FilterType mag_filter = TextureMap::UNSPECIFIED; + + if (input_texture.sampler >= 0) { + const tinygltf::Sampler &sampler = + gltf_model_.samplers[input_texture.sampler]; + DRACO_ASSIGN_OR_RETURN(wrapping_mode.s, + TinyGltfToDracoAxisWrappingMode(sampler.wrapS)); + DRACO_ASSIGN_OR_RETURN(wrapping_mode.t, + TinyGltfToDracoAxisWrappingMode(sampler.wrapT)); + DRACO_ASSIGN_OR_RETURN(min_filter, + TinyGltfToDracoFilterType(sampler.minFilter)); + DRACO_ASSIGN_OR_RETURN(mag_filter, + TinyGltfToDracoFilterType(sampler.magFilter)); + } + if (tex_coord_attribute_index < 0 || tex_coord_attribute_index > 1) { + return Status(Status::DRACO_ERROR, "Incompatible tex coord index."); + } + TextureTransform transform; + DRACO_ASSIGN_OR_RETURN(const bool has_transform, + CheckKhrTextureTransform(tex_info_ext, &transform)); + if (has_transform) { + DRACO_RETURN_IF_ERROR(material->SetTextureMap( + texture, type, wrapping_mode, min_filter, mag_filter, transform, + tex_coord_attribute_index)); + } else { + DRACO_RETURN_IF_ERROR( + material->SetTextureMap(texture, type, wrapping_mode, min_filter, + mag_filter, tex_coord_attribute_index)); + } + } + return OkStatus(); +} + +Status GltfDecoder::DecodeGltfToScene() { + DRACO_RETURN_IF_ERROR(GatherAttributeAndMaterialStats()); + DRACO_RETURN_IF_ERROR(AddLightsToScene()); + DRACO_RETURN_IF_ERROR(AddMaterialsVariantsNamesToScene()); + DRACO_RETURN_IF_ERROR(AddStructuralMetadataToGeometry(scene_.get())); + DRACO_RETURN_IF_ERROR(CopyTextures(scene_.get())); + for (const tinygltf::Scene &scene : gltf_model_.scenes) { + for (int i = 0; i < scene.nodes.size(); ++i) { + DRACO_RETURN_IF_ERROR( + DecodeNodeForScene(scene.nodes[i], kInvalidSceneNodeIndex)); + scene_->AddRootNodeIndex(gltf_node_to_scenenode_index_[scene.nodes[i]]); + } + } + + DRACO_RETURN_IF_ERROR(AddAnimationsToScene()); + DRACO_RETURN_IF_ERROR(AddMaterialsToScene()); + DRACO_RETURN_IF_ERROR(AddSkinsToScene()); + MoveNonMaterialTextures(scene_.get()); + + return OkStatus(); +} + +Status GltfDecoder::AddLightsToScene() { + // Add all lights to Draco scene. + for (const auto &light : gltf_model_.lights) { + // Add a new light to the scene. + const LightIndex light_index = scene_->AddLight(); + Light *scene_light = scene_->GetLight(light_index); + + // Decode light type. + const std::map types = { + {"directional", Light::DIRECTIONAL}, + {"point", Light::POINT}, + {"spot", Light::SPOT}}; + if (types.count(light.type) == 0) { + return ErrorStatus("Light type is invalid."); + } + scene_light->SetType(types.at(light.type)); + + // Decode spot light properties. + if (scene_light->GetType() == Light::SPOT) { + scene_light->SetInnerConeAngle(light.spot.innerConeAngle); + scene_light->SetOuterConeAngle(light.spot.outerConeAngle); + } + + // Decode other light properties. + scene_light->SetName(light.name); + if (!light.color.empty()) { // Empty means that color is not specified. + if (light.color.size() != 3) { + return ErrorStatus("Light color is malformed."); + } + scene_light->SetColor( + Vector3f(light.color[0], light.color[1], light.color[2])); + } + scene_light->SetIntensity(light.intensity); + if (light.range != 0.0) { // Zero means that range is not specified. + if (light.range < 0.0) { + return ErrorStatus("Light range must be positive."); + } + scene_light->SetRange(light.range); + } + } + return OkStatus(); +} + +Status GltfDecoder::AddMaterialsVariantsNamesToScene() { + // Check whether the scene has materials variants. + const auto &e = gltf_model_.extensions.find("KHR_materials_variants"); + if (e == gltf_model_.extensions.end()) { + // The scene has no materials variants. + return OkStatus(); + } + + // Decode all materials variants names into Draco scene from JSON like this: + // "KHR_materials_variants": { + // "variants": [ + // {"name": "Loki" }, + // {"name": "Odin" }, + // ] + // } + const tinygltf::Value::Object &o = e->second.Get(); + const auto &variants = o.find("variants"); + if (variants == o.end()) { + return ErrorStatus("Materials variants extension with names is malformed."); + } + const tinygltf::Value &variants_array = variants->second; + if (!variants_array.IsArray()) { + return ErrorStatus("Materials variants names array is malformed."); + } + for (int i = 0; i < variants_array.Size(); i++) { + const auto &variant_object = variants_array.Get(i); + if (!variant_object.IsObject() || !variant_object.Has("name")) { + return ErrorStatus("Materials variants name is missing."); + } + const auto &name_string = variant_object.Get("name"); + if (!name_string.IsString()) { + return ErrorStatus("Materials variant name is malformed."); + } + const std::string &name = name_string.Get(); + scene_->GetMaterialLibrary().AddMaterialsVariant(name); + } + return OkStatus(); +} + +template +Status GltfDecoder::AddStructuralMetadataToGeometry(GeometryT *geometry) { + // Check whether the glTF model has structural metadata. + const auto &e = gltf_model_.extensions.find("EXT_structural_metadata"); + if (e == gltf_model_.extensions.end()) { + // The glTF model has no structural metadata. + return OkStatus(); + } + const tinygltf::Value::Object &o = e->second.Get(); + + // Decode property table schema. + { + const auto &value = o.find("schema"); + if (value == o.end()) { + return ErrorStatus("Structural metadata extension has no schema."); + } + const tinygltf::Value &object = value->second; + if (!object.IsObject()) { + return ErrorStatus("Structural metadata extension schema is malformed."); + } + + // Decodes tinygltf::Value into PropertyTable::Schema::Object. + struct SchemaParser { + static Status Parse(const tinygltf::Value &value, + PropertyTable::Schema::Object *object) { + switch (value.Type()) { + case tinygltf::OBJECT_TYPE: { + for (auto &it : value.Get()) { + object->SetObjects().emplace_back(it.first); + DRACO_RETURN_IF_ERROR( + Parse(it.second, &object->SetObjects().back())); + } + } break; + case tinygltf::ARRAY_TYPE: { + for (int i = 0; i < value.ArrayLen(); ++i) { + object->SetArray().emplace_back(); + DRACO_RETURN_IF_ERROR( + Parse(value.Get(i), &object->SetArray().back())); + } + } break; + case tinygltf::STRING_TYPE: + object->SetString(value.Get()); + break; + case tinygltf::INT_TYPE: + object->SetInteger(value.Get()); + break; + case tinygltf::BOOL_TYPE: + object->SetBoolean(value.Get()); + break; + case tinygltf::REAL_TYPE: + case tinygltf::BINARY_TYPE: + case tinygltf::NULL_TYPE: + default: + // Not used in the schema JSON. + return ErrorStatus("Unsupported JSON type in schema."); + } + return OkStatus(); + } + }; + + // Parse property table schema and set it to |geometry|. + PropertyTable::Schema schema; + DRACO_RETURN_IF_ERROR(SchemaParser::Parse(object, &schema.json)); + geometry->GetStructuralMetadata().SetPropertyTableSchema(schema); + } + + // Decode property tables. + { + const auto &tables = o.find("propertyTables"); + if (tables == o.end()) { + return ErrorStatus( + "Structural metadata extension has no property tables."); + } + const tinygltf::Value &tables_array = tables->second; + if (!tables_array.IsArray()) { + return ErrorStatus("Property tables array is malformed."); + } + + // Loop over all property tables. + for (int i = 0; i < tables_array.Size(); i++) { + // Create a property table and populate it below. + std::unique_ptr property_table(new PropertyTable()); + + const auto &object = tables_array.Get(i); + if (!object.IsObject()) { + return ErrorStatus("Property table is malformed."); + } + const auto o = object.Get(); + + // The "class" property is required. + bool success; + std::string str_value; + DRACO_ASSIGN_OR_RETURN(success, DecodeString("class", o, &str_value)); + if (success) { + property_table->SetClass(str_value); + } else { + return ErrorStatus("Property class is malformed."); + } + + // The "count" property is required. + int int_value; + DRACO_ASSIGN_OR_RETURN(success, DecodeInt("count", o, &int_value)); + if (success) { + property_table->SetCount(int_value); + } else { + return ErrorStatus("Property count is malformed."); + } + + // The "name" property is optional. + DRACO_ASSIGN_OR_RETURN(success, DecodeString("name", o, &str_value)); + if (success) { + property_table->SetName(str_value); + } + + // Decode property table properties (columns). + { + constexpr char kName[] = "properties"; + if (!object.Has(kName)) { + return ErrorStatus("Property table is malformed."); + } + const tinygltf::Value &value = object.Get(kName); + if (!value.IsObject()) { + return ErrorStatus( + "Property table properties property is malformed."); + } + + // Loop over property table properties. + for (const auto &key : value.Keys()) { + // Create a property table property and populate it below. + std::unique_ptr property( + new PropertyTable::Property()); + + const auto &property_object = value.Get(key); + if (!property_object.IsObject()) { + return ErrorStatus("Property entry is malformed."); + } + property->SetName(key); + const auto o = property_object.Get(); + + // The "values" property is required. + DRACO_ASSIGN_OR_RETURN( + success, + DecodePropertyTableData("values", o, &property->GetData())); + if (!success) { + return ErrorStatus("Property values property is malformed."); + } + + // All other properties are not required. + DRACO_ASSIGN_OR_RETURN( + success, DecodeString("stringOffsetType", o, &str_value)); + if (success) { + property->GetStringOffsets().type = str_value; + } + DRACO_ASSIGN_OR_RETURN( + success, DecodeString("arrayOffsetType", o, &str_value)); + if (success) { + property->GetArrayOffsets().type = str_value; + } + DRACO_ASSIGN_OR_RETURN( + success, + DecodePropertyTableData("arrayOffsets", o, + &property->GetArrayOffsets().data)); + DRACO_ASSIGN_OR_RETURN( + success, + DecodePropertyTableData("stringOffsets", o, + &property->GetStringOffsets().data)); + + // Add property to the property table. + property_table->AddProperty(std::move(property)); + } + } + + // Add property table to structural metadata. + geometry->GetStructuralMetadata().AddPropertyTable( + std::move(property_table)); + } + } + return OkStatus(); +} + +Status GltfDecoder::AddAnimationsToScene() { + for (const auto &animation : gltf_model_.animations) { + const AnimationIndex animation_index = scene_->AddAnimation(); + Animation *const encoder_animation = scene_->GetAnimation(animation_index); + encoder_animation->SetName(animation.name); + + for (const tinygltf::AnimationChannel &channel : animation.channels) { + const auto it = gltf_node_to_scenenode_index_.find(channel.target_node); + if (it == gltf_node_to_scenenode_index_.end()) { + return Status(Status::DRACO_ERROR, "Could not find Node in the scene."); + } + DRACO_RETURN_IF_ERROR(TinyGltfUtils::AddChannelToAnimation( + gltf_model_, animation, channel, it->second.value(), + encoder_animation)); + } + } + return OkStatus(); +} + +Status GltfDecoder::DecodeNodeForScene(int node_index, + SceneNodeIndex parent_index) { + SceneNodeIndex scene_node_index = kInvalidSceneNodeIndex; + SceneNode *scene_node = nullptr; + bool is_new_node; + if (gltf_scene_graph_mode_ == GltfSceneGraphMode::DAG && + gltf_node_to_scenenode_index_.find(node_index) != + gltf_node_to_scenenode_index_.end()) { + // Node has been decoded already. + scene_node_index = gltf_node_to_scenenode_index_[node_index]; + scene_node = scene_->GetNode(scene_node_index); + is_new_node = false; + } else { + scene_node_index = scene_->AddNode(); + // Update mapping between glTF Nodes and indices in the scene. + gltf_node_to_scenenode_index_[node_index] = scene_node_index; + + scene_node = scene_->GetNode(scene_node_index); + is_new_node = true; + } + + if (parent_index != kInvalidSceneNodeIndex) { + scene_node->AddParentIndex(parent_index); + SceneNode *const parent_node = scene_->GetNode(parent_index); + parent_node->AddChildIndex(scene_node_index); + } + + if (!is_new_node) { + return OkStatus(); + } + const tinygltf::Node &node = gltf_model_.nodes[node_index]; + if (!node.name.empty()) { + scene_node->SetName(node.name); + } + std::unique_ptr trsm = GetNodeTrsMatrix(node); + scene_node->SetTrsMatrix(*trsm); + if (node.skin >= 0) { + // Save the index to the source skins in the node. This will be updated + // later when the skins are processed. + scene_node->SetSkinIndex(SkinIndex(node.skin)); + } + if (node.mesh >= 0) { + // Check if we have already parsed this glTF Mesh. + const auto it = gltf_mesh_to_scene_mesh_group_.find(node.mesh); + if (it != gltf_mesh_to_scene_mesh_group_.end()) { + // We already processed this glTF mesh. + scene_node->SetMeshGroupIndex(it->second); + } else { + const MeshGroupIndex scene_mesh_group_index = scene_->AddMeshGroup(); + MeshGroup *const scene_mesh = + scene_->GetMeshGroup(scene_mesh_group_index); + + const tinygltf::Mesh &mesh = gltf_model_.meshes[node.mesh]; + if (!mesh.name.empty()) { + scene_mesh->SetName(mesh.name); + } + for (const auto &primitive : mesh.primitives) { + DRACO_RETURN_IF_ERROR(DecodePrimitiveForScene(primitive, scene_mesh)); + } + scene_node->SetMeshGroupIndex(scene_mesh_group_index); + gltf_mesh_to_scene_mesh_group_[node.mesh] = scene_mesh_group_index; + } + } + + // Decode light index. + const auto &e = node.extensions.find("KHR_lights_punctual"); + if (e != node.extensions.end()) { + const tinygltf::Value::Object &o = e->second.Get(); + const auto &light = o.find("light"); + if (light != o.end()) { + const tinygltf::Value &value = light->second; + if (!value.IsInt()) { + return ErrorStatus("Node light index is malformed."); + } + const int light_index = value.Get(); + if (light_index < 0 || light_index >= scene_->NumLights()) { + return ErrorStatus("Node light index is out of bounds."); + } + scene_node->SetLightIndex(LightIndex(light_index)); + } + } + + for (int i = 0; i < node.children.size(); ++i) { + DRACO_RETURN_IF_ERROR( + DecodeNodeForScene(node.children[i], scene_node_index)); + } + return OkStatus(); +} + +Status GltfDecoder::DecodePrimitiveForScene( + const tinygltf::Primitive &primitive, MeshGroup *mesh_group) { + if (primitive.mode != TINYGLTF_MODE_TRIANGLES && + primitive.mode != TINYGLTF_MODE_POINTS) { + return Status(Status::DRACO_ERROR, + "Primitive does not contain triangles or points."); + } + + // Decode materials variants mappings if present in this primitive. + std::vector mappings; + const auto &e = primitive.extensions.find("KHR_materials_variants"); + if (e != primitive.extensions.end()) { + DRACO_RETURN_IF_ERROR(DecodeMaterialsVariantsMappings( + e->second.Get(), &mappings)); + } + + const PrimitiveSignature signature(primitive); + const auto existing_mesh_index = + gltf_primitive_to_draco_mesh_index_.find(signature); + if (existing_mesh_index != gltf_primitive_to_draco_mesh_index_.end()) { + mesh_group->AddMeshInstance( + {existing_mesh_index->second, primitive.material, mappings}); + return OkStatus(); + } + + // Handle indices first. + DRACO_ASSIGN_OR_RETURN(const std::vector indices_data, + DecodePrimitiveIndices(primitive)); + const int number_of_faces = indices_data.size() / 3; + const int number_of_points = indices_data.size(); + + // Note that glTF mesh |primitive| has no name; no name is set to Draco mesh. + TriangleSoupMeshBuilder mb; + PointCloudBuilder pb; + if (primitive.mode == TINYGLTF_MODE_TRIANGLES) { + mb.Start(number_of_faces); + } else { + pb.Start(number_of_points); + } + + std::set normalized_attributes; + for (const auto &attribute : primitive.attributes) { + if (attribute.second >= gltf_model_.accessors.size()) { + return ErrorStatus("Invalid accessor."); + } + const tinygltf::Accessor &accessor = + gltf_model_.accessors[attribute.second]; + const int component_type = accessor.componentType; + const int type = accessor.type; + const bool normalized = accessor.normalized; + int att_id = -1; + if (primitive.mode == TINYGLTF_MODE_TRIANGLES) { + DRACO_ASSIGN_OR_RETURN( + att_id, AddAttribute(attribute.first, component_type, type, &mb)); + } else { + DRACO_ASSIGN_OR_RETURN( + att_id, AddAttribute(attribute.first, component_type, type, &pb)); + } + if (att_id == -1) { + continue; + } + if (normalized) { + normalized_attributes.insert(att_id); + } + + if (primitive.mode == TINYGLTF_MODE_TRIANGLES) { + DRACO_RETURN_IF_ERROR(AddAttributeValuesToBuilder( + attribute.first, accessor, indices_data, att_id, number_of_faces, + Eigen::Matrix4d::Identity(), &mb)); + } else { + DRACO_RETURN_IF_ERROR(AddAttributeValuesToBuilder( + attribute.first, accessor, indices_data, att_id, number_of_points, + Eigen::Matrix4d::Identity(), &pb)); + } + } + + int material_index = primitive.material; + + DRACO_ASSIGN_OR_RETURN( + std::unique_ptr mesh, + BuildMeshFromBuilder(primitive.mode == TINYGLTF_MODE_TRIANGLES, &mb, + &pb)); + + // Set all normalized flags for appropriate attributes. + for (const int32_t att_id : normalized_attributes) { + mesh->attribute(att_id)->set_normalized(true); + } + // Decode mesh feature ID sets if present in this primitive. + DRACO_RETURN_IF_ERROR(DecodeMeshFeatures( + primitive, &scene_->GetMaterialLibrary().MutableTextureLibrary(), + mesh.get())); + + const MeshIndex mesh_index = scene_->AddMesh(std::move(mesh)); + if (mesh_index == kInvalidMeshIndex) { + return Status(Status::DRACO_ERROR, "Could not add Draco mesh to scene."); + } + mesh_group->AddMeshInstance({mesh_index, material_index, mappings}); + + gltf_primitive_to_draco_mesh_index_[signature] = mesh_index; + return OkStatus(); +} + +Status GltfDecoder::DecodeMaterialsVariantsMappings( + const tinygltf::Value::Object &extension, + std::vector *mappings) { + // Decode all materials variants mappings from JSON like this: + // "KHR_materials_variants" : { + // "mappings": [ + // { + // "material": 2, + // "variants": [0, 2, 4] + // }, + // { + // "material": 3, + // "variants": [1, 3] + // } + // ] + // } + const auto &mappings_object = extension.find("mappings"); + if (mappings_object == extension.end()) { + return ErrorStatus("Materials variants extension is malformed."); + } + const tinygltf::Value &mappings_array = mappings_object->second; + if (!mappings_array.IsArray()) { + return ErrorStatus("Materials variants mappings array is malformed."); + } + for (int i = 0; i < mappings_array.Size(); i++) { + const auto &mapping_object = mappings_array.Get(i); + if (!mapping_object.IsObject() || !mapping_object.Has("material") || + !mapping_object.Has("variants")) { + return ErrorStatus("Materials variants mapping is malformed."); + } + const tinygltf::Value &material_int = mapping_object.Get("material"); + if (!material_int.IsInt()) { + return ErrorStatus("Materials variant mapping material is malformed."); + } + const int material = material_int.Get(); + const tinygltf::Value &variants_array = mapping_object.Get("variants"); + if (!variants_array.IsArray()) { + return ErrorStatus("Materials variant mapping variants is malformed."); + } + std::vector variants; + for (int j = 0; j < variants_array.Size(); j++) { + const tinygltf::Value &variant_int = variants_array.Get(j); + if (!variant_int.IsInt()) { + return ErrorStatus("Materials variants mapping variant is malformed."); + } + variants.push_back(variant_int.Get()); + } + mappings->push_back({material, variants}); + } + return OkStatus(); +} + +Status GltfDecoder::DecodeMeshFeatures(const tinygltf::Primitive &primitive, + TextureLibrary *texture_library, + Mesh *mesh) { + const auto &e = primitive.extensions.find("EXT_mesh_features"); + if (e == primitive.extensions.end()) { + return OkStatus(); + } + std::vector> mesh_features; + DRACO_RETURN_IF_ERROR( + DecodeMeshFeatures(e->second.Get(), + texture_library, &mesh_features)); + for (int i = 0; i < mesh_features.size(); i++) { + const MeshFeaturesIndex mfi = + mesh->AddMeshFeatures(std::move(mesh_features[i])); + if (scene_ == nullptr) { + // If we are decoding to a mesh, we need to restrict the mesh features to + // the primitive's material. + // TODO(ostava): This will not work properly when two primitives share the + // same material but have different mesh features. We will need to + // duplicate the materials in this case. + const auto mat_it = + gltf_primitive_material_to_draco_material_.find(primitive.material); + if (mat_it != gltf_primitive_material_to_draco_material_.end()) { + mesh->AddMeshFeaturesMaterialMask(mfi, mat_it->second); + } + } + } + return OkStatus(); +} + +Status GltfDecoder::DecodeMeshFeatures( + const tinygltf::Value::Object &extension, TextureLibrary *texture_library, + std::vector> *mesh_features) { + // Decode all mesh feature ID sets from JSON like this: + // "EXT_mesh_features": { + // "featureIds": [ + // { + // "label": "water", + // "featureCount": 2, + // "propertyTable": 0, + // "attribute": 0 + // }, + // { + // "featureCount": 12, + // "nullFeatureId": 100, + // "texture" : { + // "index": 0, + // "texCoord": 0, + // "channels": [0, 1, 2, 3] + // } + // } + // ] + // } + const auto &object = extension.find("featureIds"); + if (object == extension.end()) { + return ErrorStatus("Mesh features extension is malformed."); + } + const tinygltf::Value &array = object->second; + if (!array.IsArray()) { + return ErrorStatus("Mesh features array is malformed."); + } + for (int i = 0; i < array.Size(); i++) { + // Create a new feature ID set object and populate it below. + mesh_features->push_back(std::unique_ptr(new MeshFeatures())); + MeshFeatures &features = *mesh_features->back(); + + const auto &object = array.Get(i); + if (!object.IsObject()) { + return ErrorStatus("Mesh features array entry is malformed."); + } + + // The "featureCount" property is required. + { + constexpr char kName[] = "featureCount"; + if (!object.Has(kName)) { + return ErrorStatus("Mesh features is malformed."); + } + const tinygltf::Value &value = object.Get(kName); + if (!value.IsInt()) { + return ErrorStatus("Feature count property is malformed."); + } + features.SetFeatureCount(value.Get()); + } + + // All other properties are optional. + { + constexpr char kName[] = "nullFeatureId"; + if (object.Has(kName)) { + const tinygltf::Value &value = object.Get(kName); + if (!value.IsInt()) { + return ErrorStatus("Null feature ID property is malformed."); + } + features.SetNullFeatureId(value.Get()); + } + } + { + constexpr char kName[] = "label"; + if (object.Has(kName)) { + const tinygltf::Value &value = object.Get(kName); + if (!value.IsString()) { + return ErrorStatus("Label property is malformed."); + } + features.SetLabel(value.Get()); + } + } + { + constexpr char kName[] = "attribute"; + if (object.Has(kName)) { + const tinygltf::Value &value = object.Get(kName); + if (!value.IsInt()) { + return ErrorStatus("Attribute property is malformed."); + } + features.SetAttributeIndex(value.Get()); + } + } + { + constexpr char kName[] = "texture"; + if (object.Has(kName)) { + const tinygltf::Value &value = object.Get(kName); + if (!value.IsObject()) { + return ErrorStatus("Texture property is malformed."); + } + + // Decode texture contining mesh feature IDs into the |features| object + // via a temporary |material| object. + Material material(texture_library); + const auto &container_object = object.Get(); + DRACO_RETURN_IF_ERROR(DecodeTexture(kName, TextureMap::GENERIC, + container_object, &material)); + features.SetTextureMap( + *material.GetTextureMapByType(TextureMap::GENERIC)); + + // Decode array of texture channel indices. + std::vector channels; + { + constexpr char kName[] = "channels"; + if (value.Has(kName)) { + const tinygltf::Value &array = value.Get(kName); + if (!array.IsArray()) { + return ErrorStatus("Channels property is malformed."); + } + for (int i = 0; i < array.Size(); i++) { + const tinygltf::Value &value = array.Get(i); + if (!value.IsNumber()) { + return Status(Status::DRACO_ERROR, + "Channels value is malformed."); + } + channels.push_back(value.Get()); + } + } else { + channels = {0}; + } + } + features.SetTextureChannels(channels); + } + } + { + constexpr char kName[] = "propertyTable"; + if (object.Has(kName)) { + const tinygltf::Value &value = object.Get(kName); + if (!value.IsInt()) { + return ErrorStatus("Property table property is malformed."); + } + features.SetPropertyTableIndex(value.Get()); + } + } + } + return OkStatus(); +} + +template +StatusOr GltfDecoder::AddAttribute(const std::string &attribute_name, + int component_type, int type, + BuilderT *builder) { + const GeometryAttribute::Type draco_att_type = + GltfAttributeToDracoAttribute(attribute_name); + if (draco_att_type == GeometryAttribute::INVALID) { + // Return attribute id -1 that will be ignored and not included in the mesh. + return -1; + } + DRACO_ASSIGN_OR_RETURN( + const int att_id, + AddAttribute(draco_att_type, component_type, type, builder)); + return att_id; +} + +template +StatusOr GltfDecoder::AddAttribute(GeometryAttribute::Type attribute_type, + int component_type, int type, + BuilderT *builder) { + const int num_components = TinyGltfUtils::GetNumComponentsForType(type); + if (num_components == 0) { + return Status(Status::DRACO_ERROR, + "Could not add attribute with 0 components."); + } + + const draco::DataType draco_component_type = + GltfComponentTypeToDracoType(component_type); + if (draco_component_type == DT_INVALID) { + return Status(Status::DRACO_ERROR, + "Could not add attribute with invalid type."); + } + const int att_id = builder->AddAttribute(attribute_type, num_components, + draco_component_type); + if (att_id < 0) { + return Status(Status::DRACO_ERROR, "Could not add attribute."); + } + return att_id; +} + +StatusOr GltfDecoder::CheckKhrTextureTransform( + const tinygltf::ExtensionMap &extension, TextureTransform *transform) { + bool transform_set = false; + + const auto &e = extension.find("KHR_texture_transform"); + if (e == extension.end()) { + return false; + } + const tinygltf::Value::Object &o = e->second.Get(); + const auto &scale = o.find("scale"); + if (scale != o.end()) { + const tinygltf::Value &array = scale->second; + if (!array.IsArray() || array.Size() != 2) { + return Status(Status::DRACO_ERROR, + "KhrTextureTransform scale is malformed."); + } + std::array scale; + for (int i = 0; i < array.Size(); i++) { + const tinygltf::Value &value = array.Get(i); + if (!value.IsNumber()) { + return Status(Status::DRACO_ERROR, + "KhrTextureTransform scale is malformed."); + } + scale[i] = value.Get(); + transform_set = true; + } + transform->set_scale(scale); + } + const auto &rotation = o.find("rotation"); + if (rotation != o.end()) { + const tinygltf::Value &value = rotation->second; + if (!value.IsNumber()) { + return Status(Status::DRACO_ERROR, + "KhrTextureTransform rotation is malformed."); + } + transform->set_rotation(value.Get()); + transform_set = true; + } + const auto &offset = o.find("offset"); + if (offset != o.end()) { + const tinygltf::Value &array = offset->second; + if (!array.IsArray() || array.Size() != 2) { + return Status(Status::DRACO_ERROR, + "KhrTextureTransform offset is malformed."); + } + std::array offset; + for (int i = 0; i < array.Size(); i++) { + const tinygltf::Value &value = array.Get(i); + if (!value.IsNumber()) { + return Status(Status::DRACO_ERROR, + "KhrTextureTransform offset is malformed."); + } + offset[i] = value.Get(); + transform_set = true; + } + transform->set_offset(offset); + } + const auto &tex_coord = o.find("texCoord"); + if (tex_coord != o.end()) { + const tinygltf::Value &value = tex_coord->second; + if (!value.IsInt()) { + return Status(Status::DRACO_ERROR, + "KhrTextureTransform texCoord is malformed."); + } + transform->set_tex_coord(value.Get()); + transform_set = true; + } + return transform_set; +} + +Status GltfDecoder::AddGltfMaterial(int input_material_index, + Material *output_material) { + const tinygltf::Material &input_material = + gltf_model_.materials[input_material_index]; + + output_material->SetName(input_material.name); + output_material->SetTransparencyMode( + TinyGltfUtils::TextToMaterialMode(input_material.alphaMode)); + output_material->SetAlphaCutoff(input_material.alphaCutoff); + if (input_material.emissiveFactor.size() == 3) { + output_material->SetEmissiveFactor(Vector3f( + input_material.emissiveFactor[0], input_material.emissiveFactor[1], + input_material.emissiveFactor[2])); + } + const tinygltf::PbrMetallicRoughness &pbr = + input_material.pbrMetallicRoughness; + + if (pbr.baseColorFactor.size() == 4) { + output_material->SetColorFactor( + Vector4f(pbr.baseColorFactor[0], pbr.baseColorFactor[1], + pbr.baseColorFactor[2], pbr.baseColorFactor[3])); + } + output_material->SetMetallicFactor(pbr.metallicFactor); + output_material->SetRoughnessFactor(pbr.roughnessFactor); + output_material->SetDoubleSided(input_material.doubleSided); + + DRACO_RETURN_IF_ERROR(CheckAndAddTextureToDracoMaterial( + pbr.baseColorTexture.index, pbr.baseColorTexture.texCoord, + pbr.baseColorTexture.extensions, output_material, TextureMap::COLOR)); + DRACO_RETURN_IF_ERROR(CheckAndAddTextureToDracoMaterial( + pbr.metallicRoughnessTexture.index, pbr.metallicRoughnessTexture.texCoord, + pbr.metallicRoughnessTexture.extensions, output_material, + TextureMap::METALLIC_ROUGHNESS)); + + DRACO_RETURN_IF_ERROR(CheckAndAddTextureToDracoMaterial( + input_material.normalTexture.index, input_material.normalTexture.texCoord, + input_material.normalTexture.extensions, output_material, + TextureMap::NORMAL_TANGENT_SPACE)); + if (input_material.normalTexture.scale != 1.0) { + output_material->SetNormalTextureScale(input_material.normalTexture.scale); + } + DRACO_RETURN_IF_ERROR(CheckAndAddTextureToDracoMaterial( + input_material.occlusionTexture.index, + input_material.occlusionTexture.texCoord, + input_material.occlusionTexture.extensions, output_material, + TextureMap::AMBIENT_OCCLUSION)); + DRACO_RETURN_IF_ERROR(CheckAndAddTextureToDracoMaterial( + input_material.emissiveTexture.index, + input_material.emissiveTexture.texCoord, + input_material.emissiveTexture.extensions, output_material, + TextureMap::EMISSIVE)); + + // Decode material extensions. + DecodeMaterialUnlitExtension(input_material, output_material); + DRACO_RETURN_IF_ERROR( + DecodeMaterialSheenExtension(input_material, output_material)); + DRACO_RETURN_IF_ERROR( + DecodeMaterialTransmissionExtension(input_material, output_material)); + DRACO_RETURN_IF_ERROR( + DecodeMaterialClearcoatExtension(input_material, output_material)); + DRACO_RETURN_IF_ERROR(DecodeMaterialVolumeExtension( + input_material, input_material_index, output_material)); + DRACO_RETURN_IF_ERROR( + DecodeMaterialIorExtension(input_material, output_material)); + DRACO_RETURN_IF_ERROR( + DecodeMaterialSpecularExtension(input_material, output_material)); + + return OkStatus(); +} + +void GltfDecoder::DecodeMaterialUnlitExtension( + const tinygltf::Material &input_material, Material *output_material) { + // Do nothing if extension is absent. + const auto &extension_it = + input_material.extensions.find("KHR_materials_unlit"); + if (extension_it == input_material.extensions.end()) { + return; + } + + // Set the unlit property in Draco material. + output_material->SetUnlit(true); +} + +Status GltfDecoder::DecodeMaterialSheenExtension( + const tinygltf::Material &input_material, Material *output_material) { + // Do nothing if extension is absent. + const auto &extension_it = + input_material.extensions.find("KHR_materials_sheen"); + if (extension_it == input_material.extensions.end()) { + return OkStatus(); + } + + output_material->SetHasSheen(true); + const tinygltf::Value::Object &extension_object = + extension_it->second.Get(); + + // Decode sheen color factor. + Vector3f vector; + bool success; + DRACO_ASSIGN_OR_RETURN( + success, DecodeVector3f("sheenColorFactor", extension_object, &vector)); + if (success) { + output_material->SetSheenColorFactor(vector); + } + + // Decode sheen roughness factor. + float value; + DRACO_ASSIGN_OR_RETURN( + success, DecodeFloat("sheenRoughnessFactor", extension_object, &value)); + if (success) { + output_material->SetSheenRoughnessFactor(value); + } + + // Decode sheen color texture. + DRACO_RETURN_IF_ERROR(DecodeTexture("sheenColorTexture", + TextureMap::SHEEN_COLOR, extension_object, + output_material)); + + // Decode sheen roughness texture. + DRACO_RETURN_IF_ERROR(DecodeTexture("sheenRoughnessTexture", + TextureMap::SHEEN_ROUGHNESS, + extension_object, output_material)); + + return OkStatus(); +} + +Status GltfDecoder::DecodeMaterialTransmissionExtension( + const tinygltf::Material &input_material, Material *output_material) { + // Do nothing if extension is absent. + const auto &extension_it = + input_material.extensions.find("KHR_materials_transmission"); + if (extension_it == input_material.extensions.end()) { + return OkStatus(); + } + + output_material->SetHasTransmission(true); + const tinygltf::Value::Object &extension_object = + extension_it->second.Get(); + + // Decode transmission factor. + float value; + DRACO_ASSIGN_OR_RETURN( + const bool success, + DecodeFloat("transmissionFactor", extension_object, &value)); + if (success) { + output_material->SetTransmissionFactor(value); + } + + // Decode transmission texture. + DRACO_RETURN_IF_ERROR(DecodeTexture("transmissionTexture", + TextureMap::TRANSMISSION, + extension_object, output_material)); + + return OkStatus(); +} + +Status GltfDecoder::DecodeMaterialClearcoatExtension( + const tinygltf::Material &input_material, Material *output_material) { + // Do nothing if extension is absent. + const auto &extension_it = + input_material.extensions.find("KHR_materials_clearcoat"); + if (extension_it == input_material.extensions.end()) { + return OkStatus(); + } + + output_material->SetHasClearcoat(true); + const tinygltf::Value::Object &extension_object = + extension_it->second.Get(); + + // Decode clearcoat factor. + float value; + bool success; + DRACO_ASSIGN_OR_RETURN( + success, DecodeFloat("clearcoatFactor", extension_object, &value)); + if (success) { + output_material->SetClearcoatFactor(value); + } + + // Decode clearcoat roughness factor. + DRACO_ASSIGN_OR_RETURN(success, DecodeFloat("clearcoatRoughnessFactor", + extension_object, &value)); + if (success) { + output_material->SetClearcoatRoughnessFactor(value); + } + + // Decode clearcoat texture. + DRACO_RETURN_IF_ERROR(DecodeTexture("clearcoatTexture", TextureMap::CLEARCOAT, + extension_object, output_material)); + + // Decode clearcoat roughness texture. + DRACO_RETURN_IF_ERROR(DecodeTexture("clearcoatRoughnessTexture", + TextureMap::CLEARCOAT_ROUGHNESS, + extension_object, output_material)); + + // Decode clearcoat normal texture. + DRACO_RETURN_IF_ERROR(DecodeTexture("clearcoatNormalTexture", + TextureMap::CLEARCOAT_NORMAL, + extension_object, output_material)); + + return OkStatus(); +} + +Status GltfDecoder::DecodeMaterialVolumeExtension( + const tinygltf::Material &input_material, int input_material_index, + Material *output_material) { + // Do nothing if extension is absent. + const auto &extension_it = + input_material.extensions.find("KHR_materials_volume"); + if (extension_it == input_material.extensions.end()) { + return OkStatus(); + } + + output_material->SetHasVolume(true); + const tinygltf::Value::Object &extension_object = + extension_it->second.Get(); + + // Decode thickness factor. + float value; + bool success; + DRACO_ASSIGN_OR_RETURN( + success, DecodeFloat("thicknessFactor", extension_object, &value)); + if (success) { + // Volume thickness factor is given in the coordinate space of the model. + // When the model is loaded as draco::Mesh, the scene graph transformations + // are applied to position attribute. Since this effectively scales the + // model coordinate space, the volume thickness factor also must be scaled. + // No scaling is done when the model is loaded as draco::Scene. + float scale = 1.0f; + if (scene_ == nullptr) { + if (gltf_primitive_material_to_scales_.count(input_material_index) == 1) { + const std::vector &scales = + gltf_primitive_material_to_scales_[input_material_index]; + + // It is only possible to scale the volume thickness factor if all + // primitives using this material have the same transformation scale. + // An alternative would be to create a separate meterial for each scale. + scale = scales[0]; + for (int i = 1; i < scales.size(); i++) { + // Note that close-enough scales could also be permitted. + if (scales[i] != scale) { + return ErrorStatus("Cannot represent volume thickness in a mesh."); + } + } + } + } + output_material->SetThicknessFactor(scale * value); + } + + // Decode attenuation distance. + DRACO_ASSIGN_OR_RETURN( + success, DecodeFloat("attenuationDistance", extension_object, &value)); + if (success) { + output_material->SetAttenuationDistance(value); + } + + // Decode attenuation color. + Vector3f vector; + DRACO_ASSIGN_OR_RETURN( + success, DecodeVector3f("attenuationColor", extension_object, &vector)); + if (success) { + output_material->SetAttenuationColor(vector); + } + + // Decode thickness texture. + DRACO_RETURN_IF_ERROR(DecodeTexture("thicknessTexture", TextureMap::THICKNESS, + extension_object, output_material)); + + return OkStatus(); +} + +Status GltfDecoder::DecodeMaterialIorExtension( + const tinygltf::Material &input_material, Material *output_material) { + // Do nothing if extension is absent. + const auto &extension_it = + input_material.extensions.find("KHR_materials_ior"); + if (extension_it == input_material.extensions.end()) { + return OkStatus(); + } + + output_material->SetHasIor(true); + const tinygltf::Value::Object &extension_object = + extension_it->second.Get(); + + // Decode index of refraction. + float value; + DRACO_ASSIGN_OR_RETURN(const bool success, + DecodeFloat("ior", extension_object, &value)); + if (success) { + output_material->SetIor(value); + } + + return OkStatus(); +} + +Status GltfDecoder::DecodeMaterialSpecularExtension( + const tinygltf::Material &input_material, Material *output_material) { + // Do nothing if extension is absent. + const auto &extension_it = + input_material.extensions.find("KHR_materials_specular"); + if (extension_it == input_material.extensions.end()) { + return OkStatus(); + } + + output_material->SetHasSpecular(true); + const tinygltf::Value::Object &extension_object = + extension_it->second.Get(); + + // Decode specular factor. + float value; + bool success; + DRACO_ASSIGN_OR_RETURN( + success, DecodeFloat("specularFactor", extension_object, &value)); + if (success) { + output_material->SetSpecularFactor(value); + } + + // Decode specular color factor. + Vector3f vector; + DRACO_ASSIGN_OR_RETURN(success, DecodeVector3f("specularColorFactor", + extension_object, &vector)); + if (success) { + output_material->SetSpecularColorFactor(vector); + } + + // Decode speclar texture. + DRACO_RETURN_IF_ERROR(DecodeTexture("specularTexture", TextureMap::SPECULAR, + extension_object, output_material)); + + // Decode specular color texture. + DRACO_RETURN_IF_ERROR(DecodeTexture("specularColorTexture", + TextureMap::SPECULAR_COLOR, + extension_object, output_material)); + + return OkStatus(); +} + +StatusOr GltfDecoder::DecodeFloat(const std::string &name, + const tinygltf::Value::Object &object, + float *value) { + const auto &it = object.find(name); + if (it == object.end()) { + return false; + } + const tinygltf::Value &number = it->second; + if (!number.IsNumber()) { + return Status(Status::DRACO_ERROR, "Invalid " + name + "."); + } + *value = number.Get(); + return true; +} + +StatusOr GltfDecoder::DecodeInt(const std::string &name, + const tinygltf::Value::Object &object, + int *value) { + const auto &it = object.find(name); + if (it == object.end()) { + return false; + } + const tinygltf::Value &number = it->second; + if (!number.IsNumber()) { + return ErrorStatus("Invalid " + name + "."); + } + *value = number.Get(); + return true; +} + +StatusOr GltfDecoder::DecodeString(const std::string &name, + const tinygltf::Value::Object &object, + std::string *value) { + const auto &it = object.find(name); + if (it == object.end()) { + return false; + } + const tinygltf::Value &string = it->second; + if (!string.IsString()) { + return ErrorStatus("Invalid " + name + "."); + } + *value = string.Get(); + return true; +} + +StatusOr GltfDecoder::DecodePropertyTableData( + const std::string &name, const tinygltf::Value::Object &object, + PropertyTable::Property::Data *data) { + int buffer_view_index; + DRACO_ASSIGN_OR_RETURN(const bool success, + DecodeInt(name, object, &buffer_view_index)); + if (!success) { + return false; + } + DRACO_RETURN_IF_ERROR( + CopyDataFromBufferView(gltf_model_, buffer_view_index, &data->data)); + data->target = gltf_model_.bufferViews[buffer_view_index].target; + return true; +} + +StatusOr GltfDecoder::DecodeVector3f( + const std::string &name, const tinygltf::Value::Object &object, + Vector3f *value) { + const auto &it = object.find(name); + if (it == object.end()) { + return false; + } + const tinygltf::Value &array = it->second; + if (!array.IsArray() || array.Size() != 3) { + return Status(Status::DRACO_ERROR, "Invalid " + name + "."); + } + for (int i = 0; i < array.Size(); i++) { + const tinygltf::Value &array_entry = array.Get(i); + if (!array_entry.IsNumber()) { + return Status(Status::DRACO_ERROR, "Invalid " + name + "."); + } + (*value)[i] = array_entry.Get(); + } + return true; +} + +Status GltfDecoder::DecodeTexture(const std::string &name, + TextureMap::Type type, + const tinygltf::Value::Object &object, + Material *material) { + tinygltf::TextureInfo info; + DRACO_RETURN_IF_ERROR(ParseTextureInfo(name, object, &info)); + DRACO_RETURN_IF_ERROR(CheckAndAddTextureToDracoMaterial( + info.index, info.texCoord, info.extensions, material, type)); + return OkStatus(); +} + +Status GltfDecoder::ParseTextureInfo( + const std::string &texture_name, + const tinygltf::Value::Object &container_object, + tinygltf::TextureInfo *texture_info) { + // Note that tinygltf only parses material textures and not material extension + // textures. This method mimics the behavior of tinygltf's private function + // ParseTextureInfo() in order for Draco to decode extension textures. + + // Do nothing if texture with such name is absent. + const auto &texture_object_it = container_object.find(texture_name); + if (texture_object_it == container_object.end()) { + return OkStatus(); + } + + const tinygltf::Value::Object &texture_object = + texture_object_it->second.Get(); + + // Decode texture index. + const auto &index_it = texture_object.find("index"); + if (index_it != texture_object.end()) { + const tinygltf::Value &value = index_it->second; + if (!value.IsNumber()) { + return Status(Status::DRACO_ERROR, "Invalid texture index."); + } + texture_info->index = value.Get(); + } + + // Decode texture coordinate index. + const auto &tex_coord_it = texture_object.find("texCoord"); + if (tex_coord_it != texture_object.end()) { + const tinygltf::Value &value = tex_coord_it->second; + if (!value.IsInt()) { + return Status(Status::DRACO_ERROR, "Invalid texture texCoord."); + } + texture_info->texCoord = value.Get(); + } + + // Decode texture extensions. + const auto &extensions_it = texture_object.find("extensions"); + if (extensions_it != texture_object.end()) { + const tinygltf::Value &extensions = extensions_it->second; + if (!extensions.IsObject()) { + return Status(Status::DRACO_ERROR, "Invalid extension."); + } + for (const std::string &key : extensions.Keys()) { + texture_info->extensions[key] = extensions.Get(key); + } + } + + // Decode texture extras. + const auto &extras_it = texture_object.find("extras"); + if (extras_it != texture_object.end()) { + texture_info->extras = extras_it->second; + } + + return OkStatus(); +} + +Status GltfDecoder::AddMaterialsToScene() { + for (int input_material_index = 0; + input_material_index < gltf_model_.materials.size(); + ++input_material_index) { + Material *const output_material = + scene_->GetMaterialLibrary().MutableMaterial(input_material_index); + DRACO_RETURN_IF_ERROR( + AddGltfMaterial(input_material_index, output_material)); + } + + // Check if we need to add a default material for primitives without an + // assigned material. + const int default_material_index = + scene_->GetMaterialLibrary().NumMaterials(); + bool default_material_needed = false; + for (MeshGroupIndex mgi(0); mgi < scene_->NumMeshGroups(); ++mgi) { + MeshGroup *const mg = scene_->GetMeshGroup(mgi); + for (int mi = 0; mi < mg->NumMeshInstances(); ++mi) { + MeshGroup::MeshInstance &mesh_instance = mg->GetMeshInstance(mi); + if (mesh_instance.material_index == -1) { + mesh_instance.material_index = default_material_index; + default_material_needed = true; + } + } + } + if (default_material_needed) { + // Create an empty default material (our defaults correspond to glTF + // defaults). + scene_->GetMaterialLibrary().MutableMaterial(default_material_index); + } + + std::unordered_set meshes_that_need_tangents; + // Check if we need to generate tangent space for any of the loaded meshes. + for (MeshGroupIndex mgi(0); mgi < scene_->NumMeshGroups(); ++mgi) { + const MeshGroup *const mg = scene_->GetMeshGroup(mgi); + for (int mi = 0; mi < mg->NumMeshInstances(); ++mi) { + const MeshGroup::MeshInstance &mesh_instance = mg->GetMeshInstance(mi); + const auto tangent_map = + scene_->GetMaterialLibrary() + .GetMaterial(mesh_instance.material_index) + ->GetTextureMapByType(TextureMap::NORMAL_TANGENT_SPACE); + if (tangent_map != nullptr) { + Mesh &mesh = scene_->GetMesh(mesh_instance.mesh_index); + if (mesh.GetNamedAttribute(GeometryAttribute::TANGENT) == nullptr) { + meshes_that_need_tangents.insert(&mesh); + } + } + } + } + + return OkStatus(); +} + +Status GltfDecoder::AddSkinsToScene() { + for (int source_skin_index = 0; source_skin_index < gltf_model_.skins.size(); + ++source_skin_index) { + const tinygltf::Skin &skin = gltf_model_.skins[source_skin_index]; + const SkinIndex skin_index = scene_->AddSkin(); + Skin *const new_skin = scene_->GetSkin(skin_index); + + // The skin index was set previously while processing the nodes. + if (skin_index.value() != source_skin_index) { + return Status(Status::DRACO_ERROR, "Skin indices are mismatched."); + } + + if (skin.inverseBindMatrices >= 0) { + const tinygltf::Accessor &accessor = + gltf_model_.accessors[skin.inverseBindMatrices]; + DRACO_RETURN_IF_ERROR(TinyGltfUtils::AddAccessorToAnimationData( + gltf_model_, accessor, &new_skin->GetInverseBindMatrices())); + } + + if (skin.skeleton >= 0) { + const auto it = gltf_node_to_scenenode_index_.find(skin.skeleton); + if (it == gltf_node_to_scenenode_index_.end()) { + // TODO(b/200317162): If skeleton is not found set the default. + return Status(Status::DRACO_ERROR, + "Could not find skeleton in the skin."); + } + new_skin->SetJointRoot(it->second); + } + + for (int joint : skin.joints) { + const auto it = gltf_node_to_scenenode_index_.find(joint); + if (it == gltf_node_to_scenenode_index_.end()) { + // TODO(b/200317162): If skeleton is not found set the default. + return Status(Status::DRACO_ERROR, + "Could not find skeleton in the skin."); + } + new_skin->AddJoint(it->second); + } + } + return OkStatus(); +} + +void GltfDecoder::MoveNonMaterialTextures(Mesh *mesh) { + std::unordered_set non_material_textures; + for (MeshFeaturesIndex i(0); i < mesh->NumMeshFeatures(); i++) { + Texture *const texture = mesh->GetMeshFeatures(i).GetTextureMap().texture(); + if (texture != nullptr) { + non_material_textures.insert(texture); + } + } + MoveNonMaterialTextures(non_material_textures, + &mesh->GetMaterialLibrary().MutableTextureLibrary(), + &mesh->GetNonMaterialTextureLibrary()); +} + +void GltfDecoder::MoveNonMaterialTextures(Scene *scene) { + std::unordered_set non_material_textures; + for (MeshIndex i(0); i < scene->NumMeshes(); i++) { + for (MeshFeaturesIndex j(0); j < scene->GetMesh(i).NumMeshFeatures(); j++) { + Texture *const texture = + scene->GetMesh(i).GetMeshFeatures(j).GetTextureMap().texture(); + if (texture != nullptr) { + non_material_textures.insert(texture); + } + } + } + MoveNonMaterialTextures(non_material_textures, + &scene->GetMaterialLibrary().MutableTextureLibrary(), + &scene->GetNonMaterialTextureLibrary()); +} + +void GltfDecoder::MoveNonMaterialTextures( + const std::unordered_set &non_material_textures, + TextureLibrary *material_tl, TextureLibrary *non_material_tl) { + // TODO(vytyaz): Consider textures that are both material and non-material. + for (int i = 0; i < material_tl->NumTextures(); i++) { + // Move non-material texture from material to non-material texture library. + if (non_material_textures.count(material_tl->GetTexture(i)) == 1) { + non_material_tl->PushTexture(material_tl->RemoveTexture(i--)); + } + } +} + +bool GltfDecoder::PrimitiveSignature::operator==( + const PrimitiveSignature &signature) const { + return primitive.indices == signature.primitive.indices && + primitive.attributes == signature.primitive.attributes && + primitive.extras == signature.primitive.extras && + primitive.extensions == signature.primitive.extensions && + primitive.mode == signature.primitive.mode && + primitive.targets == signature.primitive.targets; +} + +size_t GltfDecoder::PrimitiveSignature::Hash::operator()( + const PrimitiveSignature &signature) const { + size_t hash = 79; // Magic number. + hash = HashCombine(signature.primitive.attributes.size(), hash); + for (auto it = signature.primitive.attributes.begin(); + it != signature.primitive.attributes.end(); ++it) { + hash = HashCombine(it->first, hash); + hash = HashCombine(it->second, hash); + } + hash = HashCombine(signature.primitive.indices, hash); + hash = HashCombine(signature.primitive.mode, hash); + return hash; +} + +StatusOr> GltfDecoder::BuildMeshFromBuilder( + bool use_mesh_builder, TriangleSoupMeshBuilder *mb, PointCloudBuilder *pb) { + std::unique_ptr mesh; + if (use_mesh_builder) { + mesh = mb->Finalize(); + } else { + std::unique_ptr pc = pb->Finalize(true); + if (pc) { + mesh.reset(new Mesh()); + PointCloud *mesh_pc = mesh.get(); + mesh_pc->Copy(*pc); + } + } + if (!mesh) { + return ErrorStatus("Failed to build Draco mesh from glTF data."); + } + return mesh; +} + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED diff --git a/contrib/draco/src/draco/io/gltf_decoder.h b/contrib/draco/src/draco/io/gltf_decoder.h new file mode 100644 index 000000000..2ae12106e --- /dev/null +++ b/contrib/draco/src/draco/io/gltf_decoder.h @@ -0,0 +1,524 @@ +// Copyright 2018 The Draco Authors. +// +// 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 DRACO_IO_GLTF_DECODER_H_ +#define DRACO_IO_GLTF_DECODER_H_ + +#include "draco/draco_features.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED +#include +#include +#include +#include +#include +#include + +#include "draco/core/decoder_buffer.h" +#include "draco/core/status.h" +#include "draco/core/status_or.h" +#include "draco/io/tiny_gltf_utils.h" +#include "draco/mesh/mesh.h" +#include "draco/mesh/triangle_soup_mesh_builder.h" +#include "draco/point_cloud/point_cloud_builder.h" +#include "draco/scene/scene.h" + +namespace draco { + +// Decodes a glTF file and returns a draco::Mesh. All of the |mesh|'s attributes +// will be merged into one draco::Mesh +class GltfDecoder { + public: + GltfDecoder(); + + // Decodes a glTF file stored in the input |file_name| or |buffer| to a Mesh. + // The second form returns a vector of files used as input to the mesh during + // the decoding process. Returns nullptr when decode fails. + StatusOr> DecodeFromFile(const std::string &file_name); + StatusOr> DecodeFromFile( + const std::string &file_name, std::vector *mesh_files); + StatusOr> DecodeFromBuffer(DecoderBuffer *buffer); + + // Decodes a glTF file stored in the input |file_name| or |buffer| to a Scene. + // The second form returns a vector of files used as input to the scene during + // the decoding process. Returns nullptr if the decode fails. + StatusOr> DecodeFromFileToScene( + const std::string &file_name); + StatusOr> DecodeFromFileToScene( + const std::string &file_name, std::vector *scene_files); + StatusOr> DecodeFromBufferToScene( + DecoderBuffer *buffer); + + // Scene graph can be loaded either as a tree or a general directed acyclic + // graph (DAG) that allows multiple parent nodes. By default. we decode the + // scene graph as a tree. If the tree mode is selected and the input contains + // nodes with multiple parents, these nodes are duplicated to form a tree. + // TODO(ostava): Add support for DAG mode to other parts of the Draco + // library. + enum class GltfSceneGraphMode { TREE, DAG }; + void SetSceneGraphMode(GltfSceneGraphMode mode) { + gltf_scene_graph_mode_ = mode; + } + + private: + // Loads |file_name| into |gltf_model_|. Fills |input_files| with paths to all + // input files when non-null. + Status LoadFile(const std::string &file_name, + std::vector *input_files); + + // Loads |gltf_model_| from |buffer| in GLB format. + Status LoadBuffer(const DecoderBuffer &buffer); + + // Builds mesh from |gltf_model_|. + StatusOr> BuildMesh(); + + // Checks |gltf_model_| for unsupported features. If |gltf_model_| contains + // unsupported features then the function will return with a status code of + // UNSUPPORTED_FEATURE. + Status CheckUnsupportedFeatures(); + + // Decodes a glTF Node as well as any child Nodes. If |node| contains a mesh + // it will process all of the mesh's primitives. + Status DecodeNode(int node_index, const Eigen::Matrix4d &parent_matrix); + + // Decodes the number of entries in the first attribute of a given glTF + // |primitive|. Note that all attributes have the same entry count according + // to glTF 2.0 spec. + StatusOr DecodePrimitiveAttributeCount( + const tinygltf::Primitive &primitive) const; + + // Decodes the number of indices in a given glTF |primitive|. If primitive's + // indices property is not defined, the index count is implied from the entry + // count of a primitive attribute. + StatusOr DecodePrimitiveIndicesCount( + const tinygltf::Primitive &primitive) const; + + // Decodes indices property of a given glTF |primitive|. If primitive's + // indices property is not defined, the indices are generated based on entry + // count of a primitive attribute. + StatusOr> DecodePrimitiveIndices( + const tinygltf::Primitive &primitive) const; + + // Decodes a glTF Primitive. All of the |primitive|'s attributes will be + // merged into the draco::Mesh output if they are of the same type that + // already has been decoded. + Status DecodePrimitive(const tinygltf::Primitive &primitive, + const Eigen::Matrix4d &transform_matrix); + + // Sums the number of elements per attribute for |node|'s mesh and any of + // |node|'s children. Fills out the material index map. + Status NodeGatherAttributeAndMaterialStats(const tinygltf::Node &node); + + // Sums the number of elements per attribute for all of the meshes and + // primitives. + Status GatherAttributeAndMaterialStats(); + + // Sums the attribute counts into total_attribute_counts_. + void SumAttributeStats(const std::string &attribute_name, int count); + + // Checks that all the same glTF attribute types in different meshes and + // primitives contain the same characteristics. + Status CheckTypes(const std::string &attribute_name, int component_type, + int type, bool normalized); + + // Accumulates the number of elements per attribute for |primitive|. + Status AccumulatePrimitiveStats(const tinygltf::Primitive &primitive); + + // Adds all of the attributes from the glTF file to a Draco mesh. + // GatherAttributeAndMaterialStats() must be called before this function. The + // GeometryAttribute::MATERIAL attribute will be created only if the glTF file + // contains more than one material. + template + Status AddAttributesToDracoMesh(BuilderT *builder); + + // Copies attribute data from |accessor| and adds it to a Draco mesh using the + // geometry builder |builder|. + template + Status AddAttributeValuesToBuilder(const std::string &attribute_name, + const tinygltf::Accessor &accessor, + const std::vector &indices_data, + int att_id, int number_of_elements, + const Eigen::Matrix4d &transform_matrix, + BuilderT *builder); + + // Copies the tangent attribute data from |accessor| and adds it to a Draco + // mesh. This function will transform all of the data by |transform_matrix| + // and then normalize before adding the data to the Draco mesh. + // |indices_data| is the indices data from the glTF file. |att_id| is the + // attribute id of the tangent attribute in the Draco mesh. + // |number_of_elements| is the number of faces or points this function will + // process. |reverse_winding| if set will change the orientation of the data. + template + Status AddTangentToBuilder(const tinygltf::Accessor &accessor, + const std::vector &indices_data, + int att_id, int number_of_elements, + const Eigen::Matrix4d &transform_matrix, + bool reverse_winding, BuilderT *builder); + + // Copies the texture coordinate attribute data from |accessor| and adds it to + // a Draco mesh. This function will flip the data on the horizontal axis as + // Draco meshes store the texture coordinates differently than glTF. + // |indices_data| is the indices data from the glTF file. |att_id| is the + // attribute id of the texture coordinate attribute in the Draco mesh. + // |number_of_elements| is the number of faces or points this function will + // process. |reverse_winding| if set will change the orientation of the data. + template + Status AddTexCoordToBuilder(const tinygltf::Accessor &accessor, + const std::vector &indices_data, + int att_id, int number_of_elements, + bool reverse_winding, BuilderT *builder); + + // Copies the mesh feature ID attribute data from |accessor| and adds it to a + // Draco mesh. |indices_data| is the indices data from the glTF file. |att_id| + // is the attribute ID of the mesh feature ID attribute in the Draco mesh. + // |number_of_elements| is the number of faces or points this function will + // process. |reverse_winding| if set will change the orientation of the data. + template + Status AddFeatureIdToBuilder(const tinygltf::Accessor &accessor, + const std::vector &indices_data, + int att_id, int number_of_elements, + bool reverse_winding, + const std::string &attribute_name, + BuilderT *builder); + + // Copies the attribute data from |accessor| and adds it to a Draco mesh. + // This function will transform all of the data by |transform_matrix| before + // adding the data to the Draco mesh. |indices_data| is the indices data + // from the glTF file. |att_id| is the attribute id of the attribute in the + // Draco mesh. |number_of_elements| is the number of faces or points this + // function will process. |normalize| if set will normalize all of the vector + // data after transformation. |reverse_winding| if set will change the + // orientation of the data. + template + Status AddTransformedDataToBuilder(const tinygltf::Accessor &accessor, + const std::vector &indices_data, + int att_id, int number_of_elements, + const Eigen::Matrix4d &transform_matrix, + bool normalize, bool reverse_winding, + BuilderT *builder); + + // Sets values in |data| into the builder |builder| for |att_id|. + template + void SetValuesForBuilder(const std::vector &indices_data, + int att_id, int number_of_elements, + const std::vector &data, bool reverse_winding, + TriangleSoupMeshBuilder *builder); + template + void SetValuesForBuilder(const std::vector &indices_data, + int att_id, int number_of_elements, + const std::vector &data, bool reverse_winding, + PointCloudBuilder *builder); + + // Sets values in |data| into the mesh builder |mb| for |att_id|. + // |reverse_winding| if set will change the orientation of the data. + template + void SetValuesPerFace(const std::vector &indices_data, int att_id, + int number_of_faces, const std::vector &data, + bool reverse_winding, TriangleSoupMeshBuilder *mb); + + // Returns an address pointing to the content stored in |data|. This is used + // when passing values to mesh / point cloud builder when the input type can + // be either a VectorD or an arithmetic type. + template + const void *GetDataContentAddress(const T &data) const; + + // Adds the attribute data in |accessor| to |mb| for unique attribute + // |att_id|. |indices_data| is the mesh's indices data. |reverse_winding| if + // set will change the orientation of the data. + template + Status AddAttributeDataByTypes(const tinygltf::Accessor &accessor, + const std::vector &indices_data, + int att_id, int number_of_elements, + bool reverse_winding, BuilderT *builder); + + // Adds the textures to |owner|. + template + Status CopyTextures(T *owner); + + // Sets extra attribute properties on a constructed draco mesh. + void SetAttributePropertiesOnDracoMesh(Mesh *mesh); + + // Adds the materials to |mesh|. + Status AddMaterialsToDracoMesh(Mesh *mesh); + + // Adds the material data for the GeometryAttribute::MATERIAL attribute to the + // Draco mesh. + template + Status AddMaterialDataToBuilder(int material_value, int number_of_elements, + BuilderT *builder); + template + Status AddMaterialDataToBuilderInternal(T material_value, int number_of_faces, + TriangleSoupMeshBuilder *builder); + template + Status AddMaterialDataToBuilderInternal(T material_value, + int number_of_points, + PointCloudBuilder *builder); + + // Checks if the glTF file contains a texture. If there is a texture, this + // function will read the texture data and add it to the Draco |material|. If + // there is no texture, this function will return OkStatus(). |texture_info| + // is the data structure containing information about the texture in the glTF + // file. |type| is the type of texture defined by Draco. This is not the same + // as the texture coordinate attribute id. + Status CheckAndAddTextureToDracoMaterial( + int texture_index, int tex_coord_attribute_index, + const tinygltf::ExtensionMap &tex_info_ext, Material *material, + TextureMap::Type type); + + // Decode glTF file to scene. + Status DecodeGltfToScene(); + + // Decode glTF lights into a scene. + Status AddLightsToScene(); + + // Decodes glTF materials variants names into a scene. + Status AddMaterialsVariantsNamesToScene(); + + // Decode glTF animations into a scene. All of the glTF nodes must be decoded + // to the scene before this function is called. + Status AddAnimationsToScene(); + + // Decode glTF node into a Draco scene. |parent_index| is the index of the + // parent node. If |node| is a root node set |parent_index| to + // |kInvalidSceneNodeIndex|. All glTF lights must be decoded to the scene + // before this function is called. + Status DecodeNodeForScene(int node_index, SceneNodeIndex parent_index); + + // Decode glTF primitive into a Draco scene. + Status DecodePrimitiveForScene(const tinygltf::Primitive &primitive, + MeshGroup *mesh_group); + + // Decodes glTF materials variants from |extension| and adds it into materials + // variants |mappings|. Before calling this function, all materials variants + // names must be decoded by calling AddMaterialsVariantsNamesToScene(). + Status DecodeMaterialsVariantsMappings( + const tinygltf::Value::Object &extension, + std::vector *mappings); + + // Decodes glTF mesh feature ID sets from all glTF primitives and adds them to + // |mesh|. + Status AddMeshFeaturesToDracoMesh(Mesh *mesh); + + // Decodes glTF mesh feature ID sets from glTF primitive in glTF node at + // |node_index| and adds them to |mesh|. + Status AddMeshFeaturesToDracoMesh(int node_index, Mesh *mesh); + + // Decodes glTF structural metadata from glTF model and adds it to |geometry|. + template + Status AddStructuralMetadataToGeometry(GeometryT *geometry); + + // Decodes glTF mesh feature ID sets from |primitive| and adds them to |mesh|. + Status DecodeMeshFeatures(const tinygltf::Primitive &primitive, + TextureLibrary *texture_library, Mesh *mesh); + + // Decodes glTF mesh feature ID sets from |extension| and adds them to the + // |mesh_features| vector. + Status DecodeMeshFeatures( + const tinygltf::Value::Object &extension, TextureLibrary *texture_library, + std::vector> *mesh_features); + + // Adds an attribute of type |attribute_name| to |builder|. Returns the + // attribute id. + template + StatusOr AddAttribute(const std::string &attribute_name, + int component_type, int type, BuilderT *builder); + + // Adds an attribute of |attribute_type| to |builder|. Returns the attribute + // id. + template + StatusOr AddAttribute(GeometryAttribute::Type attribute_type, + int component_type, int type, BuilderT *builder); + + // Returns true if the KHR_texture_transform extension is set in |extension|. + // If the KHR_texture_transform extension is set then the values are returned + // in |transform|. + StatusOr CheckKhrTextureTransform( + const tinygltf::ExtensionMap &extension, TextureTransform *transform); + + // Adds glTF material |input_material_index| to |output_material|. + Status AddGltfMaterial(int input_material_index, Material *output_material); + + // Adds unlit property from glTF |input_material| to |output_material|. + void DecodeMaterialUnlitExtension(const tinygltf::Material &input_material, + Material *output_material); + + // Adds sheen properties from glTF |input_material| to |output_material|. + Status DecodeMaterialSheenExtension(const tinygltf::Material &input_material, + Material *output_material); + + // Adds transmission from glTF |input_material| to |output_material|. + Status DecodeMaterialTransmissionExtension( + const tinygltf::Material &input_material, Material *output_material); + + // Adds clearcoat properties from glTF |input_material| to |output_material|. + Status DecodeMaterialClearcoatExtension( + const tinygltf::Material &input_material, Material *output_material); + + // Adds volume properties from glTF |input_material| to |output_material|. + Status DecodeMaterialVolumeExtension(const tinygltf::Material &input_material, + int input_material_index, + Material *output_material); + + // Adds ior properties from glTF |input_material| to |output_material|. + Status DecodeMaterialIorExtension(const tinygltf::Material &input_material, + Material *output_material); + + // Adds specular properties from glTF |input_material| to |output_material|. + Status DecodeMaterialSpecularExtension( + const tinygltf::Material &input_material, Material *output_material); + + // Decodes a float value with |name| from |object| to |value| and returns true + // if a well-formed value with such |name| is present. + static StatusOr DecodeFloat(const std::string &name, + const tinygltf::Value::Object &object, + float *value); + + // Decodes an integer value with |name| from |object| to |value| and returns + // true if a well-formed value with such |name| is present. + static StatusOr DecodeInt(const std::string &name, + const tinygltf::Value::Object &object, + int *value); + + // Decodes a string value with |name| from |object| to |value| and returns + // true if a well-formed value with such |name| is present. + static StatusOr DecodeString(const std::string &name, + const tinygltf::Value::Object &object, + std::string *value); + + // Decodes data and data target from buffer view index with |name| in |object| + // to |data| and returns true if a well-formed data is present. + StatusOr DecodePropertyTableData(const std::string &name, + const tinygltf::Value::Object &object, + PropertyTable::Property::Data *data); + + // Decodes a 3D vector with |name| from |object| to |value| and returns true + // if a well-formed vector with such |name| is present. + static StatusOr DecodeVector3f(const std::string &name, + const tinygltf::Value::Object &object, + Vector3f *value); + + // Decodes a texture with |name| from |object| and adds it to |material| as a + // texture map of |type|. + Status DecodeTexture(const std::string &name, TextureMap::Type type, + const tinygltf::Value::Object &object, + Material *material); + + // Reads texture with |texture_name| from |container_object| into + // |texture_info|. + static Status ParseTextureInfo( + const std::string &texture_name, + const tinygltf::Value::Object &container_object, + tinygltf::TextureInfo *texture_info); + + // Adds the materials to the scene. + Status AddMaterialsToScene(); + + // Adds the skins to the scene. + Status AddSkinsToScene(); + + // All material and non-material textures (e.g., from EXT_mesh_features) are + // initially loaded into a texture library inside the the material library. + // These methods move |non_material_textures| from material texture library + // |material_tl| to non-material texture library |non_material_tl|. + static void MoveNonMaterialTextures(Mesh *mesh); + static void MoveNonMaterialTextures(Scene *scene); + static void MoveNonMaterialTextures( + const std::unordered_set &non_material_textures, + TextureLibrary *material_tl, TextureLibrary *non_material_tl); + + // Builds and returns a mesh constructed from either mesh builder |mb| or + // point cloud builder |pb|. Mesh builder is used if |use_mesh_builder| is set + // to true. + static StatusOr> BuildMeshFromBuilder( + bool use_mesh_builder, TriangleSoupMeshBuilder *mb, + PointCloudBuilder *pb); + + // Map of glTF Mesh to Draco scene mesh group. + std::map gltf_mesh_to_scene_mesh_group_; + + // Data structure that stores the glTF data. + tinygltf::Model gltf_model_; + + // Path to the glTF file. + std::string input_file_name_; + + // Class used to build the Draco mesh. + TriangleSoupMeshBuilder mb_; + PointCloudBuilder pb_; + + // Next face index used when adding attribute data to the Draco mesh. + int next_face_id_; + + // Next point index used when adding attribute data to the point cloud. + int next_point_id_; + + // Total number of indices from all the meshes and primitives. + int total_face_indices_count_; + int total_point_indices_count_; + + // This is the id of the GeometryAttribute::MATERIAL attribute added to the + // Draco mesh. + int material_att_id_; + + // Data used when decoding the entire glTF asset into a single draco::Mesh. + // The struct tracks the total number of elements across all matching + // attributes and it ensures all matching attributes are compatible. + struct MeshAttributeData { + int component_type = 0; + int attribute_type = 0; + bool normalized = false; + int total_attribute_counts = 0; + }; + + // Map of glTF attribute name to attribute component type. + std::map mesh_attribute_data_; + + // Map of glTF attribute name to Draco mesh attribute id. + std::map attribute_name_to_draco_mesh_attribute_id_; + + // Map of glTF material to Draco material index. + std::map gltf_primitive_material_to_draco_material_; + + // Map of glTF material index to transformation scales of primitives. + std::map> gltf_primitive_material_to_scales_; + + // Map of glTF image to Draco textures. + std::map gltf_image_to_draco_texture_; + + std::unique_ptr scene_; + + // Map of glTF Node to local store order. + std::map gltf_node_to_scenenode_index_; + + // Selected mode of the decoded scene graph. + GltfSceneGraphMode gltf_scene_graph_mode_ = GltfSceneGraphMode::TREE; + + // Functionality for deduping primitives on decode. + struct PrimitiveSignature { + const tinygltf::Primitive &primitive; + explicit PrimitiveSignature(const tinygltf::Primitive &primitive) + : primitive(primitive) {} + bool operator==(const PrimitiveSignature &signature) const; + struct Hash { + size_t operator()(const PrimitiveSignature &signature) const; + }; + }; + std::unordered_map + gltf_primitive_to_draco_mesh_index_; +}; + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED +#endif // DRACO_IO_GLTF_DECODER_H_ diff --git a/contrib/draco/src/draco/io/gltf_decoder_test.cc b/contrib/draco/src/draco/io/gltf_decoder_test.cc new file mode 100644 index 000000000..fade3ee26 --- /dev/null +++ b/contrib/draco/src/draco/io/gltf_decoder_test.cc @@ -0,0 +1,1402 @@ +// Copyright 2018 The Draco Authors. +// +// 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 "draco/io/gltf_decoder.h" + +#include +#include +#include +#include +#include +#include + +#include "draco/material/material_library.h" +#include "draco/scene/mesh_group.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED +#include "draco/core/constants.h" +#include "draco/core/draco_test_base.h" +#include "draco/core/draco_test_utils.h" +#include "draco/core/draco_types.h" +#include "draco/io/gltf_test_helper.h" +#include "draco/io/texture_io.h" +#include "draco/mesh/mesh_are_equivalent.h" +#include "draco/mesh/mesh_utils.h" +#include "draco/scene/scene_indices.h" +#include "draco/scene/scene_utils.h" +#include "draco/texture/texture_utils.h" + +namespace draco { + +namespace { +std::unique_ptr DecodeGltfFile(const std::string &file_name) { + const std::string path = GetTestFileFullPath(file_name); + GltfDecoder decoder; + + auto maybe_geometry = decoder.DecodeFromFile(path); + if (!maybe_geometry.ok()) { + return nullptr; + } + std::unique_ptr geometry = std::move(maybe_geometry).value(); + return geometry; +} + +std::unique_ptr DecodeGltfFileToScene(const std::string &file_name) { + const std::string path = GetTestFileFullPath(file_name); + GltfDecoder decoder; + + auto maybe_scene = decoder.DecodeFromFileToScene(path); + if (!maybe_scene.ok()) { + return nullptr; + } + std::unique_ptr scene = std::move(maybe_scene).value(); + return scene; +} + +void CompareVectorArray(const std::array &a, + const std::array &b) { + for (int v = 0; v < 3; ++v) { + for (int c = 0; c < 3; ++c) { + EXPECT_FLOAT_EQ(a[v][c], b[v][c]) << "v:" << v << " c:" << c; + } + } +} +} // namespace + +// Tests multiple textures. +TEST(GltfDecoderTest, SphereGltf) { + const std::string file_name = "sphere.gltf"; + const std::unique_ptr mesh(DecodeGltfFile(file_name)); + EXPECT_EQ(mesh->num_attributes(), 4) << "Unexpected number of attributes."; + EXPECT_EQ(mesh->num_points(), 231) << "Unexpected number of vertices."; + EXPECT_EQ(mesh->num_faces(), 224) << "Unexpected number of faces."; + ASSERT_EQ(mesh->GetMaterialLibrary().NumMaterials(), 1); + ASSERT_EQ(mesh->GetMaterialLibrary().GetMaterial(0)->NumTextureMaps(), 2); +} + +TEST(GltfDecoderTest, TriangleGltf) { + const std::string file_name = "one_face_123.gltf"; + const std::unique_ptr mesh(DecodeGltfFile(file_name)); + EXPECT_EQ(mesh->num_attributes(), 1) << "Unexpected number of attributes."; + EXPECT_EQ(mesh->num_points(), 3) << "Unexpected number of vertices."; + EXPECT_EQ(mesh->num_faces(), 1) << "Unexpected number of faces."; + ASSERT_EQ(mesh->GetMaterialLibrary().NumMaterials(), 1); + ASSERT_EQ(mesh->GetMaterialLibrary().GetMaterial(0)->NumTextureMaps(), 0); + + const auto *const pos_attribute = + mesh->GetNamedAttribute(GeometryAttribute::POSITION); + EXPECT_NE(pos_attribute, nullptr); + const auto &face = mesh->face(FaceIndex(0)); + std::array pos; + for (int c = 0; c < 3; ++c) { + pos_attribute->GetMappedValue(face[c], &pos[c][0]); + } + + // Test position values match. + std::array pos_test; + pos_test[0] = Vector3f(1, 0.0999713, 0); + pos_test[1] = Vector3f(2.00006104, 0.01, 0); + pos_test[2] = Vector3f(3, 0.10998169, 0); + CompareVectorArray(pos, pos_test); +} + +TEST(GltfDecoderTest, MirroredTriangleGltf) { + const std::string file_name = "one_face_123_mirror.gltf"; + const std::unique_ptr mesh(DecodeGltfFile(file_name)); + EXPECT_EQ(mesh->num_attributes(), 1) << "Unexpected number of attributes."; + EXPECT_EQ(mesh->num_points(), 3) << "Unexpected number of vertices."; + EXPECT_EQ(mesh->num_faces(), 1) << "Unexpected number of faces."; + ASSERT_EQ(mesh->GetMaterialLibrary().NumMaterials(), 1); + ASSERT_EQ(mesh->GetMaterialLibrary().GetMaterial(0)->NumTextureMaps(), 0); + + const auto *const pos_attribute = + mesh->GetNamedAttribute(GeometryAttribute::POSITION); + EXPECT_NE(pos_attribute, nullptr); + const auto &face = mesh->face(FaceIndex(0)); + std::array pos; + for (int c = 0; c < 3; ++c) { + pos_attribute->GetMappedValue(face[c], &pos[c][0]); + } + + // Test position values match. + std::array pos_test; + pos_test[0] = Vector3f(-1, -0.0999713, 0); + pos_test[1] = Vector3f(-3, -0.10998169, 0); + pos_test[2] = Vector3f(-2.00006104, -0.01, 0); + CompareVectorArray(pos, pos_test); +} + +TEST(GltfDecoderTest, TranslateTriangleGltf) { + const std::string file_name = "one_face_123_translated.gltf"; + const std::unique_ptr mesh(DecodeGltfFile(file_name)); + EXPECT_EQ(mesh->num_attributes(), 1) << "Unexpected number of attributes."; + EXPECT_EQ(mesh->num_points(), 3) << "Unexpected number of vertices."; + EXPECT_EQ(mesh->num_faces(), 1) << "Unexpected number of faces."; + ASSERT_EQ(mesh->GetMaterialLibrary().NumMaterials(), 1); + ASSERT_EQ(mesh->GetMaterialLibrary().GetMaterial(0)->NumTextureMaps(), 0); + + const auto *const pos_attribute = + mesh->GetNamedAttribute(GeometryAttribute::POSITION); + EXPECT_NE(pos_attribute, nullptr); + const auto &face = mesh->face(FaceIndex(0)); + std::array pos; + for (int c = 0; c < 3; ++c) { + pos_attribute->GetMappedValue(face[c], &pos[c][0]); + } + + // Test position values match. The glTF file contains a matrix in the main + // node. The matrix defines a translation of (-1.5, 5.0, 2.3). + std::array pos_test; + pos_test[0] = Vector3f(1, 0.0999713, 0); + pos_test[1] = Vector3f(2.00006104, 0.01, 0); + pos_test[2] = Vector3f(3, 0.10998169, 0); + const Vector3f translate(-1.5, 5.0, 2.3); + for (int v = 0; v < 3; ++v) { + pos_test[v] = pos_test[v] + translate; + } + CompareVectorArray(pos, pos_test); +} + +// Tests multiple materials. +TEST(GltfDecoderTest, MilkTruckGltf) { + const std::string file_name = "CesiumMilkTruck/glTF/CesiumMilkTruck.gltf"; + const std::unique_ptr mesh(DecodeGltfFile(file_name)); + EXPECT_EQ(mesh->num_attributes(), 4) << "Unexpected number of attributes."; + EXPECT_EQ(mesh->num_points(), 3564) << "Unexpected number of vertices."; + EXPECT_EQ(mesh->num_faces(), 3624) << "Unexpected number of faces."; + ASSERT_EQ(mesh->GetMaterialLibrary().NumMaterials(), 4); + ASSERT_EQ(mesh->GetMaterialLibrary().GetMaterial(0)->NumTextureMaps(), 1); + ASSERT_EQ(mesh->GetMaterialLibrary().GetMaterial(1)->NumTextureMaps(), 0); + ASSERT_EQ(mesh->GetMaterialLibrary().GetMaterial(2)->NumTextureMaps(), 0); + ASSERT_EQ(mesh->GetMaterialLibrary().GetMaterial(3)->NumTextureMaps(), 1); + ASSERT_EQ(mesh->GetMaterialLibrary().GetMaterial(0)->GetName(), "truck"); + ASSERT_EQ(mesh->GetMaterialLibrary().GetMaterial(1)->GetName(), "glass"); + ASSERT_EQ(mesh->GetMaterialLibrary().GetMaterial(2)->GetName(), + "window_trim"); + ASSERT_EQ(mesh->GetMaterialLibrary().GetMaterial(3)->GetName(), "wheels"); +} + +TEST(GltfDecoderTest, SceneMilkTruckGltf) { + const std::string file_name = "CesiumMilkTruck/glTF/CesiumMilkTruck.gltf"; + const std::unique_ptr scene(DecodeGltfFileToScene(file_name)); + + ASSERT_EQ(scene->NumMeshes(), 4); + ASSERT_EQ(scene->NumMeshGroups(), 2); + ASSERT_EQ(scene->NumNodes(), 5); + ASSERT_EQ(scene->NumRootNodes(), 1); + ASSERT_EQ(scene->NumLights(), 0); + ASSERT_EQ(scene->GetMaterialLibrary().NumMaterials(), 4); + ASSERT_EQ(scene->GetMaterialLibrary().GetMaterial(0)->NumTextureMaps(), 1); + ASSERT_EQ(scene->GetMaterialLibrary().GetMaterial(1)->NumTextureMaps(), 0); + ASSERT_EQ(scene->GetMaterialLibrary().GetMaterial(2)->NumTextureMaps(), 0); + ASSERT_EQ(scene->GetMaterialLibrary().GetMaterial(3)->NumTextureMaps(), 1); + ASSERT_EQ(scene->GetMaterialLibrary().GetMaterial(0)->GetName(), "truck"); + ASSERT_EQ(scene->GetMaterialLibrary().GetMaterial(1)->GetName(), "glass"); + ASSERT_EQ(scene->GetMaterialLibrary().GetMaterial(2)->GetName(), + "window_trim"); + ASSERT_EQ(scene->GetMaterialLibrary().GetMaterial(3)->GetName(), "wheels"); + ASSERT_EQ(scene->NumAnimations(), 1); + ASSERT_EQ(scene->NumSkins(), 0); + for (AnimationIndex i(0); i < scene->NumAnimations(); ++i) { + const Animation *const animation = scene->GetAnimation(i); + ASSERT_NE(animation, nullptr); + ASSERT_EQ(animation->NumSamplers(), 2); + ASSERT_EQ(animation->NumChannels(), 2); + } + + ASSERT_EQ(scene->GetMeshGroup(MeshGroupIndex(0))->GetName(), + "Cesium_Milk_Truck"); + ASSERT_EQ(scene->GetMeshGroup(MeshGroupIndex(1))->GetName(), "Wheels"); + + // Check all of the meshes do not have any materials. + for (MeshIndex i(0); i < scene->NumMeshes(); ++i) { + const Mesh &mesh = scene->GetMesh(i); + ASSERT_EQ(mesh.GetMaterialLibrary().NumMaterials(), 0); + } +} + +TEST(GltfDecoderTest, AnimatedBonesGltf) { + const std::string file_name = "CesiumMan/glTF/CesiumMan.gltf"; + const std::unique_ptr scene(DecodeGltfFileToScene(file_name)); + + ASSERT_EQ(scene->NumMeshes(), 1); + ASSERT_EQ(scene->NumMeshGroups(), 1); + const MeshGroup &mesh_group = *scene->GetMeshGroup(MeshGroupIndex(0)); + ASSERT_EQ(mesh_group.NumMeshInstances(), 1); + ASSERT_EQ(mesh_group.GetMeshInstance(0).material_index, 0); + ASSERT_EQ(scene->NumNodes(), 22); + ASSERT_EQ(scene->NumRootNodes(), 1); + ASSERT_EQ(scene->GetMaterialLibrary().NumMaterials(), 1); + ASSERT_EQ(scene->GetMaterialLibrary().GetMaterial(0)->NumTextureMaps(), 1); + ASSERT_EQ(scene->NumAnimations(), 1); + ASSERT_EQ(scene->NumSkins(), 1); + for (AnimationIndex i(0); i < scene->NumAnimations(); ++i) { + const Animation *const animation = scene->GetAnimation(i); + ASSERT_NE(animation, nullptr); + ASSERT_EQ(animation->NumSamplers(), 57); + ASSERT_EQ(animation->NumChannels(), 57); + } + + // Check all of the meshes do not have any materials. + for (MeshIndex i(0); i < scene->NumMeshes(); ++i) { + const Mesh &mesh = scene->GetMesh(i); + ASSERT_EQ(mesh.GetMaterialLibrary().NumMaterials(), 0); + } +} + +TEST(GltfDecoderTest, AnimatedBonesGlb) { + const std::string file_name = "CesiumMan/glTF_Binary/CesiumMan.glb"; + const std::unique_ptr scene(DecodeGltfFileToScene(file_name)); + ASSERT_EQ(scene->NumMeshes(), 1); + ASSERT_EQ(scene->NumMeshGroups(), 1); + const MeshGroup &mesh_group = *scene->GetMeshGroup(MeshGroupIndex(0)); + ASSERT_EQ(mesh_group.NumMeshInstances(), 1); + ASSERT_EQ(mesh_group.GetMeshInstance(0).material_index, 0); + ASSERT_EQ(scene->NumNodes(), 22); + ASSERT_EQ(scene->NumRootNodes(), 1); + ASSERT_EQ(scene->GetMaterialLibrary().NumMaterials(), 1); + ASSERT_EQ(scene->GetMaterialLibrary().GetMaterial(0)->NumTextureMaps(), 1); + ASSERT_EQ(scene->NumAnimations(), 1); + ASSERT_EQ(scene->NumSkins(), 1); + for (AnimationIndex i(0); i < scene->NumAnimations(); ++i) { + const Animation *const animation = scene->GetAnimation(i); + ASSERT_NE(animation, nullptr); + ASSERT_EQ(animation->NumSamplers(), 57); + ASSERT_EQ(animation->NumChannels(), 57); + } + + // Check all of the meshes do not have any materials. + for (MeshIndex i(0); i < scene->NumMeshes(); ++i) { + const Mesh &mesh = scene->GetMesh(i); + ASSERT_EQ(mesh.GetMaterialLibrary().NumMaterials(), 0); + } +} + +// Tests multiple primitives with the same material index. +TEST(GltfDecoderTest, LanternGltf) { + const std::string file_name = "Lantern/glTF/Lantern.gltf"; + const std::unique_ptr mesh(DecodeGltfFile(file_name)); + + EXPECT_EQ(mesh->num_attributes(), 4) << "Unexpected number of attributes."; + EXPECT_EQ(mesh->num_points(), 4145) << "Unexpected number of vertices."; + EXPECT_EQ(mesh->num_faces(), 5394) << "Unexpected number of faces."; + ASSERT_EQ(mesh->GetMaterialLibrary().NumMaterials(), 1); + ASSERT_EQ(mesh->GetMaterialLibrary().GetMaterial(0)->NumTextureMaps(), 4); +} + +// Tests COLOR_0 input attribute. +TEST(GltfDecoderTest, ColorAttributeGltf) { + const std::string file_name = "test_pos_color.gltf"; + const std::unique_ptr mesh(DecodeGltfFile(file_name)); + EXPECT_EQ(mesh->num_attributes(), 2) << "Unexpected number of attributes."; + EXPECT_EQ(mesh->num_points(), 114) << "Unexpected number of vertices."; + EXPECT_EQ(mesh->num_faces(), 224) << "Unexpected number of faces."; + ASSERT_EQ(mesh->GetMaterialLibrary().NumMaterials(), 1); + ASSERT_EQ(mesh->GetMaterialLibrary().GetMaterial(0)->NumTextureMaps(), 0); + ASSERT_NE(mesh->GetNamedAttribute(GeometryAttribute::COLOR), nullptr); + ASSERT_EQ(mesh->GetNamedAttribute(GeometryAttribute::COLOR)->data_type(), + draco::DT_UINT8); + // Ensure the normalized property for the color attribute is set properly. + ASSERT_TRUE(mesh->GetNamedAttribute(GeometryAttribute::COLOR)->normalized()); +} + +// Tests COLOR_0 input attribute when the asset is loaded into a scene. +TEST(GltfDecoderTest, ColorAttributeGltfScene) { + const std::string file_name = "test_pos_color.gltf"; + const std::unique_ptr scene(DecodeGltfFileToScene(file_name)); + ASSERT_EQ(scene->NumMeshes(), 1); + const Mesh &mesh = scene->GetMesh(MeshIndex(0)); + ASSERT_NE(mesh.GetNamedAttribute(GeometryAttribute::COLOR), nullptr); + ASSERT_EQ(mesh.GetNamedAttribute(GeometryAttribute::COLOR)->data_type(), + draco::DT_UINT8); + // Ensure the normalized property for the color attribute is set properly. + ASSERT_TRUE(mesh.GetNamedAttribute(GeometryAttribute::COLOR)->normalized()); +} + +// Tests a mesh with two sets of texture coordinates. +TEST(GltfDecoderTest, TwoTexCoordAttributesGltf) { + const std::string file_name = "sphere_two_tex_coords.gltf"; + const std::unique_ptr mesh(DecodeGltfFile(file_name)); + ASSERT_NE(mesh, nullptr); + ASSERT_EQ(mesh->NumNamedAttributes(GeometryAttribute::TEX_COORD), 2); +} + +// Tests an input with a valid tangent attribute does not auto generate the +// tangent attribute. +TEST(GltfDecoderTest, TestSceneWithTangents) { + const std::string file_name = "Lantern/glTF/Lantern.gltf"; + const std::unique_ptr scene(DecodeGltfFileToScene(file_name)); + ASSERT_NE(scene, nullptr); + + // Ensure no mesh has auto-generated tangents (and that some meshes have the + // tangent attribute). + int num_tangent_attributes = 0; + for (MeshIndex mi(0); mi < scene->NumMeshes(); ++mi) { + if (scene->GetMesh(mi).GetNamedAttribute(GeometryAttribute::TANGENT) != + nullptr) { + num_tangent_attributes++; + ASSERT_FALSE(MeshUtils::HasAutoGeneratedTangents(scene->GetMesh(mi))); + } + } + ASSERT_GT(num_tangent_attributes, 0); +} + +// Tests an input file where multiple textures share the same image asset. +TEST(GltfDecoderTest, SharedImages) { + const std::string file_name = "SphereAllSame/sphere_texture_all.gltf"; + const std::unique_ptr mesh(DecodeGltfFile(file_name)); + ASSERT_NE(mesh, nullptr); + ASSERT_EQ(mesh->GetMaterialLibrary().NumMaterials(), 1); + ASSERT_EQ(mesh->GetMaterialLibrary().GetMaterial(0)->NumTextureMaps(), 5); + ASSERT_EQ(mesh->GetMaterialLibrary().GetTextureLibrary().NumTextures(), 4); +} + +TEST(GltfDecoderTest, TextureNamesAreNotEmpty) { + const std::string file_name = "SphereAllSame/sphere_texture_all.gltf"; + const std::unique_ptr mesh(DecodeGltfFile(file_name)); + ASSERT_NE(mesh, nullptr); + ASSERT_EQ(mesh->GetMaterialLibrary().NumMaterials(), 1); + ASSERT_EQ(mesh->GetMaterialLibrary().GetMaterial(0)->NumTextureMaps(), 5); + ASSERT_EQ(mesh->GetMaterialLibrary().GetTextureLibrary().NumTextures(), 4); + const std::vector textures = { + mesh->GetMaterialLibrary().GetTextureLibrary().GetTexture(0), + mesh->GetMaterialLibrary().GetTextureLibrary().GetTexture(1), + mesh->GetMaterialLibrary().GetTextureLibrary().GetTexture(2), + mesh->GetMaterialLibrary().GetTextureLibrary().GetTexture(3)}; + EXPECT_EQ(TextureUtils::GetTargetStem(*textures[0]), "256x256_all_orange"); + EXPECT_EQ(TextureUtils::GetTargetStem(*textures[1]), "256x256_all_blue"); + EXPECT_EQ(TextureUtils::GetTargetStem(*textures[2]), "256x256_all_red"); + EXPECT_EQ(TextureUtils::GetTargetStem(*textures[3]), "256x256_all_green"); + EXPECT_EQ(TextureUtils::GetTargetFormat(*textures[0]), ImageFormat::PNG); + EXPECT_EQ(TextureUtils::GetTargetFormat(*textures[1]), ImageFormat::PNG); + EXPECT_EQ(TextureUtils::GetTargetFormat(*textures[2]), ImageFormat::PNG); + EXPECT_EQ(TextureUtils::GetTargetFormat(*textures[3]), ImageFormat::PNG); +} + +TEST(GltfDecoderTest, TestTexCoord1) { + const std::string file_name = "MultiUVTest/glTF/MultiUVTest.gltf"; + const std::unique_ptr mesh(DecodeGltfFile(file_name)); + ASSERT_NE(mesh, nullptr); + ASSERT_EQ(mesh->GetMaterialLibrary().NumMaterials(), 1); + ASSERT_EQ(mesh->GetMaterialLibrary().GetMaterial(0)->NumTextureMaps(), 2); + ASSERT_EQ(mesh->GetMaterialLibrary().GetTextureLibrary().NumTextures(), 2); + const std::vector textures = { + mesh->GetMaterialLibrary().GetTextureLibrary().GetTexture(0), + mesh->GetMaterialLibrary().GetTextureLibrary().GetTexture(1)}; + EXPECT_EQ(TextureUtils::GetTargetStem(*textures[0]), "uv0"); + EXPECT_EQ(TextureUtils::GetTargetStem(*textures[1]), "uv1"); + EXPECT_EQ(TextureUtils::GetTargetFormat(*textures[0]), ImageFormat::PNG); + EXPECT_EQ(TextureUtils::GetTargetFormat(*textures[1]), ImageFormat::PNG); + ASSERT_EQ(mesh->NumNamedAttributes(GeometryAttribute::TEX_COORD), 2); + ASSERT_EQ(mesh->NumNamedAttributes(GeometryAttribute::POSITION), 1); + ASSERT_EQ(mesh->NumNamedAttributes(GeometryAttribute::NORMAL), 1); + ASSERT_EQ(mesh->NumNamedAttributes(GeometryAttribute::TANGENT), 1); +} + +TEST(GltfDecoderTest, SimpleScene) { + const std::string file_name = "Box/glTF/Box.gltf"; + const std::unique_ptr scene(DecodeGltfFileToScene(file_name)); + + ASSERT_EQ(scene->NumMeshes(), 1); + ASSERT_EQ(scene->NumMeshGroups(), 1); + const MeshGroup &mesh_group = *scene->GetMeshGroup(MeshGroupIndex(0)); + ASSERT_EQ(mesh_group.NumMeshInstances(), 1); + ASSERT_EQ(mesh_group.GetMeshInstance(0).material_index, 0); + ASSERT_EQ(scene->NumNodes(), 2); + ASSERT_EQ(scene->NumRootNodes(), 1); + ASSERT_EQ(scene->GetMaterialLibrary().NumMaterials(), 1); + ASSERT_EQ(scene->GetMaterialLibrary().GetMaterial(0)->NumTextureMaps(), 0); + ASSERT_EQ(scene->NumSkins(), 0); + ASSERT_EQ(scene->NumAnimations(), 0); + + // Check all of the meshes do not have any materials. + for (MeshIndex i(0); i < scene->NumMeshes(); ++i) { + const Mesh &mesh = scene->GetMesh(i); + ASSERT_EQ(mesh.GetMaterialLibrary().NumMaterials(), 0); + } + + // Check names of nodes are empty. + EXPECT_TRUE(scene->GetNode(SceneNodeIndex(0))->GetName().empty()); + EXPECT_TRUE(scene->GetNode(SceneNodeIndex(1))->GetName().empty()); +} + +TEST(GltfDecoderTest, LanternScene) { + const std::string file_name = "Lantern/glTF/Lantern.gltf"; + const std::unique_ptr scene(DecodeGltfFileToScene(file_name)); + + EXPECT_EQ(scene->NumMeshes(), 3); + EXPECT_EQ(scene->NumMeshGroups(), 3); + EXPECT_EQ(scene->NumNodes(), 4); + EXPECT_EQ(scene->NumRootNodes(), 1); + EXPECT_EQ(scene->GetMaterialLibrary().NumMaterials(), 1); + EXPECT_EQ(scene->GetMaterialLibrary().GetMaterial(0)->NumTextureMaps(), 4); + EXPECT_EQ(scene->GetMaterialLibrary().GetMaterial(0)->GetDoubleSided(), + false); + EXPECT_EQ(scene->NumSkins(), 0); + EXPECT_EQ(scene->NumAnimations(), 0); + + // Check names of nodes have been populated. + EXPECT_EQ(scene->GetNode(SceneNodeIndex(0))->GetName(), "Lantern"); + EXPECT_EQ(scene->GetNode(SceneNodeIndex(1))->GetName(), "LanternPole_Body"); + EXPECT_EQ(scene->GetNode(SceneNodeIndex(2))->GetName(), "LanternPole_Chain"); + EXPECT_EQ(scene->GetNode(SceneNodeIndex(3))->GetName(), + "LanternPole_Lantern"); +} + +TEST(GltfDecoderTest, SimpleTriangleMesh) { + const std::string file_name = "Triangle/glTF/Triangle.gltf"; + const std::unique_ptr mesh(DecodeGltfFile(file_name)); + + EXPECT_EQ(mesh->num_attributes(), 1) << "Unexpected number of attributes."; + EXPECT_EQ(mesh->num_points(), 3) << "Unexpected number of vertices."; + EXPECT_EQ(mesh->num_faces(), 1) << "Unexpected number of faces."; + EXPECT_EQ(mesh->GetMaterialLibrary().NumMaterials(), 0); +} + +TEST(GltfDecoderTest, SimpleTriangleScene) { + const std::string file_name = "Triangle/glTF/Triangle.gltf"; + const std::unique_ptr scene(DecodeGltfFileToScene(file_name)); + + EXPECT_EQ(scene->NumMeshes(), 1); + EXPECT_EQ(scene->NumMeshGroups(), 1); + const MeshGroup &mesh_group = *scene->GetMeshGroup(MeshGroupIndex(0)); + ASSERT_EQ(mesh_group.NumMeshInstances(), 1); + ASSERT_EQ(mesh_group.GetMeshInstance(0).material_index, 0); + EXPECT_EQ(scene->NumNodes(), 1); + EXPECT_EQ(scene->NumRootNodes(), 1); + EXPECT_EQ(scene->GetMaterialLibrary().NumMaterials(), 1); + EXPECT_EQ(scene->NumSkins(), 0); + EXPECT_EQ(scene->NumAnimations(), 0); +} + +TEST(GltfDecoderTest, ThreeMeshesOneNoMaterialScene) { + const std::string file_name = + "three_meshes_two_materials_one_no_material/" + "three_meshes_two_materials_one_no_material.gltf"; + const std::unique_ptr scene(DecodeGltfFileToScene(file_name)); + + EXPECT_EQ(scene->NumMeshes(), 3); + EXPECT_EQ(scene->NumMeshGroups(), 3); + EXPECT_EQ(scene->NumNodes(), 4); + EXPECT_EQ(scene->NumRootNodes(), 1); + EXPECT_EQ(scene->GetMaterialLibrary().NumMaterials(), 3); + EXPECT_EQ(scene->NumSkins(), 0); + EXPECT_EQ(scene->NumAnimations(), 0); +} + +TEST(GltfDecoderTest, ThreeMeshesOneNoMaterialMesh) { + const std::string file_name = + "three_meshes_two_materials_one_no_material/" + "three_meshes_two_materials_one_no_material.gltf"; + const std::unique_ptr mesh(DecodeGltfFile(file_name)); + + EXPECT_EQ(mesh->num_attributes(), 4) << "Unexpected number of attributes."; + EXPECT_EQ(mesh->num_points(), 72) << "Unexpected number of vertices."; + EXPECT_EQ(mesh->num_faces(), 36) << "Unexpected number of faces."; + EXPECT_EQ(mesh->GetMaterialLibrary().NumMaterials(), 3); +} + +TEST(GltfDecoderTest, DoubleSidedMaterial) { + const std::string file_name = "TwoSidedPlane/glTF/TwoSidedPlane.gltf"; + const std::unique_ptr mesh(DecodeGltfFile(file_name)); + EXPECT_EQ(mesh->GetMaterialLibrary().NumMaterials(), 1); + EXPECT_EQ(mesh->GetMaterialLibrary().GetMaterial(0)->GetDoubleSided(), true); + + const std::unique_ptr scene(DecodeGltfFileToScene(file_name)); + EXPECT_EQ(scene->GetMaterialLibrary().NumMaterials(), 1); + EXPECT_EQ(scene->GetMaterialLibrary().GetMaterial(0)->GetDoubleSided(), true); +} + +TEST(GltfDecoderTest, VertexColorTest) { + const std::string file_name = "VertexColorTest/glTF/VertexColorTest.gltf"; + const std::unique_ptr mesh(DecodeGltfFile(file_name)); + EXPECT_EQ(mesh->GetMaterialLibrary().NumMaterials(), 2); + EXPECT_EQ(mesh->NumNamedAttributes(GeometryAttribute::COLOR), 1); + + const std::unique_ptr scene(DecodeGltfFileToScene(file_name)); + EXPECT_EQ(scene->GetMaterialLibrary().NumMaterials(), 2); + EXPECT_EQ(scene->NumMeshes(), 2); + const Mesh &second_mesh = scene->GetMesh(MeshIndex(1)); + EXPECT_EQ(second_mesh.NumNamedAttributes(GeometryAttribute::COLOR), 1); +} + +TEST(GltfDecoderTest, MorphTargets) { + const std::string filename = + "KhronosSampleModels/AnimatedMorphCube/glTF/AnimatedMorphCube.gltf"; + const std::string path = GetTestFileFullPath(filename); + GltfDecoder decoder; + const auto maybe_scene = decoder.DecodeFromFileToScene(path); + EXPECT_FALSE(maybe_scene.ok()); + EXPECT_EQ(maybe_scene.status().code(), Status::Code::UNSUPPORTED_FEATURE); +} + +TEST(GltfDecoderTest, SparseAccessors) { + const std::string filename = + "KhronosSampleModels/SimpleSparseAccessor/glTF/SimpleSparseAccessor.gltf"; + const std::string path = GetTestFileFullPath(filename); + GltfDecoder decoder; + const auto maybe_scene = decoder.DecodeFromFileToScene(path); + EXPECT_FALSE(maybe_scene.ok()); + EXPECT_EQ(maybe_scene.status().code(), Status::Code::UNSUPPORTED_FEATURE); +} + +TEST(GltfDecoderTest, PbrSpecularGlossinessExtension) { + const std::string filename = + "KhronosSampleModels/SpecGlossVsMetalRough/glTF/" + "SpecGlossVsMetalRough.gltf"; + const std::string path = GetTestFileFullPath(filename); + GltfDecoder decoder; + const auto maybe_scene = decoder.DecodeFromFileToScene(path); + EXPECT_FALSE(maybe_scene.ok()); + EXPECT_EQ(maybe_scene.status().code(), Status::Code::UNSUPPORTED_FEATURE); +} + +TEST(GltfDecoderTest, DifferentWrappingModes) { + const std::string filename = + "KhronosSampleModels/TextureSettingsTest/glTF/TextureSettingsTest.gltf"; + const std::string path = GetTestFileFullPath(filename); + GltfDecoder decoder; + const auto maybe_scene = decoder.DecodeFromFileToScene(path); + EXPECT_TRUE(maybe_scene.ok()); + const draco::Scene &scene = *maybe_scene.value(); + ASSERT_EQ(scene.GetMaterialLibrary().GetTextureLibrary().NumTextures(), 3); + ASSERT_EQ(scene.GetMaterialLibrary().NumMaterials(), 10); + const draco::Material &material = *scene.GetMaterialLibrary().GetMaterial(0); + ASSERT_EQ(material.NumTextureMaps(), 1); + ASSERT_EQ(material.GetTextureMapByIndex(0)->wrapping_mode().s, + draco::TextureMap::REPEAT); + ASSERT_EQ(material.GetTextureMapByIndex(0)->wrapping_mode().t, + draco::TextureMap::MIRRORED_REPEAT); +} + +TEST(GltfDecoderTest, KhrMaterialsUnlitExtension) { + const std::string no_unlit_filename = "Box/glTF/Box.gltf"; + const std::unique_ptr scene_no_unlit( + DecodeGltfFileToScene(no_unlit_filename)); + EXPECT_EQ(scene_no_unlit->GetMaterialLibrary().NumMaterials(), 1); + EXPECT_EQ(scene_no_unlit->GetMaterialLibrary().GetMaterial(0)->GetUnlit(), + false); + + const std::string filename = + "KhronosSampleModels/UnlitTest/glTF/UnlitTest.gltf"; + const std::unique_ptr mesh(DecodeGltfFile(filename)); + EXPECT_EQ(mesh->GetMaterialLibrary().NumMaterials(), 2); + EXPECT_EQ(mesh->GetMaterialLibrary().GetMaterial(0)->GetUnlit(), true); + EXPECT_EQ(mesh->GetMaterialLibrary().GetMaterial(1)->GetUnlit(), true); + + const std::unique_ptr scene(DecodeGltfFileToScene(filename)); + EXPECT_EQ(scene->GetMaterialLibrary().NumMaterials(), 2); + EXPECT_EQ(scene->GetMaterialLibrary().GetMaterial(0)->GetUnlit(), true); + EXPECT_EQ(scene->GetMaterialLibrary().GetMaterial(1)->GetUnlit(), true); +} + +TEST(GltfDecoderTest, KhrMaterialsSheenExtension) { + // Check that a model with no sheen is loaded with no sheen. + { + const std::unique_ptr scene( + DecodeGltfFileToScene("Box/glTF/Box.gltf")); + EXPECT_EQ(scene->GetMaterialLibrary().NumMaterials(), 1); + + // Check that material has no sheen. + const Material &material = *scene->GetMaterialLibrary().GetMaterial(0); + EXPECT_FALSE(material.HasSheen()); + + // Check that sheen color and roughness factors have default values. + EXPECT_EQ(material.GetSheenColorFactor(), Vector3f(0.f, 0.f, 0.f)); + EXPECT_EQ(material.GetSheenRoughnessFactor(), 0.f); + + // Check that sheen textures are absent. + EXPECT_EQ(material.GetTextureMapByType(TextureMap::SHEEN_COLOR), nullptr); + EXPECT_EQ(material.GetTextureMapByType(TextureMap::SHEEN_ROUGHNESS), + nullptr); + } + + // Check that a model with sheen is loaded as a mesh with sheen. + { + // Load model as a mesh. + const std::unique_ptr mesh( + DecodeGltfFile("KhronosSampleModels/SheenCloth/glTF/SheenCloth.gltf")); + EXPECT_NE(mesh, nullptr); + const Material &material = *mesh->GetMaterialLibrary().GetMaterial(0); + + // Check that material has sheen. + EXPECT_TRUE(material.HasSheen()); + + // Check that sheen color and roughness factors are present. + EXPECT_EQ(material.GetSheenColorFactor(), Vector3f(1.f, 1.f, 1.f)); + EXPECT_EQ(material.GetSheenRoughnessFactor(), 1.f); + + // Check that sheen color and roughness textures are present. + EXPECT_NE(material.GetTextureMapByType(TextureMap::SHEEN_COLOR), nullptr); + EXPECT_NE(material.GetTextureMapByType(TextureMap::SHEEN_ROUGHNESS), + nullptr); + + // Check that sheen color and roughness textures are shared. + EXPECT_EQ( + material.GetTextureMapByType(TextureMap::SHEEN_COLOR)->texture(), + material.GetTextureMapByType(TextureMap::SHEEN_ROUGHNESS)->texture()); + } + + // Check that a model with sheen is loaded as a scene with sheen. + { + // Load model as a scene. + const std::unique_ptr scene(DecodeGltfFileToScene( + "KhronosSampleModels/SheenCloth/glTF/SheenCloth.gltf")); + EXPECT_EQ(scene->GetMaterialLibrary().NumMaterials(), 1); + const Material &material = *scene->GetMaterialLibrary().GetMaterial(0); + + // Check that material has sheen. + EXPECT_TRUE(material.HasSheen()); + + // Check that sheen color and roughness factors are present. + EXPECT_EQ(material.GetSheenColorFactor(), Vector3f(1.f, 1.f, 1.f)); + EXPECT_EQ(material.GetSheenRoughnessFactor(), 1.f); + + // Check that sheen color and roughness textures are present. + EXPECT_NE(material.GetTextureMapByType(TextureMap::SHEEN_COLOR), nullptr); + EXPECT_NE(material.GetTextureMapByType(TextureMap::SHEEN_ROUGHNESS), + nullptr); + + // Check that sheen color and roughness textures are shared. + EXPECT_EQ( + material.GetTextureMapByType(TextureMap::SHEEN_COLOR)->texture(), + material.GetTextureMapByType(TextureMap::SHEEN_ROUGHNESS)->texture()); + } +} + +TEST(GltfDecoderTest, PbrNextExtensions) { + // Check that a model with no material extensions is loaded correctly. + { + const std::unique_ptr scene( + DecodeGltfFileToScene("Box/glTF/Box.gltf")); + EXPECT_EQ(scene->GetMaterialLibrary().NumMaterials(), 1); + const Material &m = *scene->GetMaterialLibrary().GetMaterial(0); + + // Check that material has no extensions. + EXPECT_FALSE(m.HasSheen()); + EXPECT_FALSE(m.HasTransmission()); + EXPECT_FALSE(m.HasClearcoat()); + EXPECT_FALSE(m.HasVolume()); + EXPECT_FALSE(m.HasIor()); + EXPECT_FALSE(m.HasSpecular()); + } + + // Check that a model with material extensions is loaded correctly. + { + const std::unique_ptr mesh( + DecodeGltfFile("pbr_next/sphere/glTF/sphere.gltf")); + EXPECT_NE(mesh, nullptr); + const Material &m = *mesh->GetMaterialLibrary().GetMaterial(0); + + // Check that material has extensions. + EXPECT_TRUE(m.HasSheen()); + EXPECT_TRUE(m.HasTransmission()); + EXPECT_TRUE(m.HasClearcoat()); + EXPECT_TRUE(m.HasVolume()); + EXPECT_TRUE(m.HasIor()); + EXPECT_TRUE(m.HasSpecular()); + + // Check that material has correct extension properties. + EXPECT_EQ(m.GetSheenColorFactor(), Vector3f(1.0f, 0.329f, 0.1f)); + EXPECT_EQ(m.GetSheenRoughnessFactor(), 0.8f); + EXPECT_EQ(m.GetTransmissionFactor(), 0.75f); + EXPECT_EQ(m.GetClearcoatFactor(), 0.95f); + EXPECT_EQ(m.GetClearcoatRoughnessFactor(), 0.03f); + EXPECT_EQ(m.GetAttenuationColor(), Vector3f(0.921f, 0.640f, 0.064f)); + EXPECT_EQ(m.GetAttenuationDistance(), 0.155f); + EXPECT_EQ(m.GetThicknessFactor(), 2.27f); + EXPECT_EQ(m.GetIor(), 1.55f); + EXPECT_EQ(m.GetSpecularFactor(), 0.3f); + EXPECT_EQ(m.GetSpecularColorFactor(), Vector3f(0.212f, 0.521f, 0.051f)); + + // Check that material has all extension textures. + EXPECT_NE(m.GetTextureMapByType(TextureMap::SHEEN_COLOR), nullptr); + EXPECT_NE(m.GetTextureMapByType(TextureMap::SHEEN_ROUGHNESS), nullptr); + EXPECT_NE(m.GetTextureMapByType(TextureMap::TRANSMISSION), nullptr); + EXPECT_NE(m.GetTextureMapByType(TextureMap::CLEARCOAT), nullptr); + EXPECT_NE(m.GetTextureMapByType(TextureMap::CLEARCOAT_ROUGHNESS), nullptr); + EXPECT_NE(m.GetTextureMapByType(TextureMap::CLEARCOAT_NORMAL), nullptr); + EXPECT_NE(m.GetTextureMapByType(TextureMap::THICKNESS), nullptr); + EXPECT_NE(m.GetTextureMapByType(TextureMap::SPECULAR), nullptr); + EXPECT_NE(m.GetTextureMapByType(TextureMap::SPECULAR_COLOR), nullptr); + } +} + +TEST(GltfDecoderTest, TextureTransformTest) { + const std::string filename = + "KhronosSampleModels/TextureTransformTest/glTF/TextureTransformTest.gltf"; + const std::unique_ptr mesh(DecodeGltfFile(filename)); + EXPECT_EQ(mesh->GetMaterialLibrary().NumMaterials(), 9); + for (int i = 0; i < 6; ++i) { + EXPECT_FALSE(TextureTransform::IsDefault(mesh->GetMaterialLibrary() + .GetMaterial(i) + ->GetTextureMapByIndex(0) + ->texture_transform())); + } + for (int i = 6; i < 9; ++i) { + EXPECT_TRUE(TextureTransform::IsDefault(mesh->GetMaterialLibrary() + .GetMaterial(i) + ->GetTextureMapByIndex(0) + ->texture_transform())); + } + + const std::unique_ptr scene(DecodeGltfFileToScene(filename)); + EXPECT_EQ(scene->GetMaterialLibrary().NumMaterials(), 9); + for (int i = 0; i < 6; ++i) { + EXPECT_FALSE(TextureTransform::IsDefault(scene->GetMaterialLibrary() + .GetMaterial(i) + ->GetTextureMapByIndex(0) + ->texture_transform())); + } + for (int i = 6; i < 9; ++i) { + EXPECT_TRUE(TextureTransform::IsDefault(scene->GetMaterialLibrary() + .GetMaterial(i) + ->GetTextureMapByIndex(0) + ->texture_transform())); + } +} + +TEST(GltfDecoderTest, GlbTextureSource) { + const std::string file_name = "KhronosSampleModels/Duck/glTF_Binary/Duck.glb"; + const std::unique_ptr scene(DecodeGltfFileToScene(file_name)); + EXPECT_EQ(scene->NumMeshes(), 1); + EXPECT_EQ(scene->NumMeshGroups(), 1); + const MeshGroup &mesh_group = *scene->GetMeshGroup(MeshGroupIndex(0)); + ASSERT_EQ(mesh_group.NumMeshInstances(), 1); + ASSERT_EQ(mesh_group.GetMeshInstance(0).material_index, 0); + EXPECT_EQ(scene->NumNodes(), 3); + EXPECT_EQ(scene->NumRootNodes(), 1); + EXPECT_EQ(scene->GetMaterialLibrary().NumMaterials(), 1); + EXPECT_EQ(scene->GetMaterialLibrary().GetMaterial(0)->NumTextureMaps(), 1); + EXPECT_EQ(scene->NumAnimations(), 0); + EXPECT_EQ(scene->NumSkins(), 0); + EXPECT_EQ(scene->GetMaterialLibrary().GetTextureLibrary().NumTextures(), 1); + const Texture *const texture = + scene->GetMaterialLibrary().GetTextureLibrary().GetTexture(0); + ASSERT_NE(texture, nullptr); + const SourceImage &source_image = texture->source_image(); + EXPECT_EQ(source_image.encoded_data().size(), 16302); + EXPECT_EQ(source_image.filename(), ""); + EXPECT_EQ(source_image.mime_type(), "image/png"); +} + +TEST(GltfDecoderTest, GltfTextureSource) { + const std::string file_name = "KhronosSampleModels/Duck/glTF/Duck.gltf"; + const std::unique_ptr scene(DecodeGltfFileToScene(file_name)); + EXPECT_EQ(scene->NumMeshes(), 1); + EXPECT_EQ(scene->NumMeshGroups(), 1); + const MeshGroup &mesh_group = *scene->GetMeshGroup(MeshGroupIndex(0)); + ASSERT_EQ(mesh_group.NumMeshInstances(), 1); + ASSERT_EQ(mesh_group.GetMeshInstance(0).material_index, 0); + EXPECT_EQ(scene->NumNodes(), 3); + EXPECT_EQ(scene->NumRootNodes(), 1); + EXPECT_EQ(scene->GetMaterialLibrary().NumMaterials(), 1); + EXPECT_EQ(scene->GetMaterialLibrary().GetMaterial(0)->NumTextureMaps(), 1); + EXPECT_EQ(scene->NumAnimations(), 0); + EXPECT_EQ(scene->NumSkins(), 0); + EXPECT_EQ(scene->GetMaterialLibrary().GetTextureLibrary().NumTextures(), 1); + const Texture *const texture = + scene->GetMaterialLibrary().GetTextureLibrary().GetTexture(0); + ASSERT_NE(texture, nullptr); + const SourceImage &source_image = texture->source_image(); + EXPECT_EQ(source_image.encoded_data().size(), 0); + EXPECT_FALSE(source_image.filename().empty()); + EXPECT_EQ(source_image.mime_type(), ""); +} + +TEST(GltfDecoderTest, GltfDecodeWithDraco) { + // Tests that we can decode a glTF containing Draco compressed geometry. + const std::string file_name = "Box/glTF_Binary/Box.glb"; + const std::string file_name_with_draco = "Box/glTF_Binary/Box_Draco.glb"; + const std::unique_ptr scene(DecodeGltfFileToScene(file_name)); + const std::unique_ptr scene_draco( + DecodeGltfFileToScene(file_name_with_draco)); + ASSERT_NE(scene, nullptr); + ASSERT_NE(scene_draco, nullptr); + EXPECT_EQ(scene->NumMeshes(), scene_draco->NumMeshes()); + EXPECT_EQ(scene->NumMeshGroups(), scene_draco->NumMeshGroups()); + EXPECT_EQ(scene->NumNodes(), scene_draco->NumNodes()); + EXPECT_EQ(scene->NumRootNodes(), scene_draco->NumRootNodes()); + EXPECT_EQ(scene->GetMaterialLibrary().NumMaterials(), + scene_draco->GetMaterialLibrary().NumMaterials()); + EXPECT_EQ(scene->NumAnimations(), scene_draco->NumAnimations()); + EXPECT_EQ(scene->NumSkins(), scene_draco->NumSkins()); + + EXPECT_EQ(scene->NumMeshes(), 1); + EXPECT_EQ(scene->GetMesh(draco::MeshIndex(0)).num_faces(), + scene_draco->GetMesh(draco::MeshIndex(0)).num_faces()); +} + +TEST(GltfDecoderTest, TestAnimationNames) { + const std::string file_name = "InterpolationTest/glTF/InterpolationTest.gltf"; + const std::unique_ptr scene(DecodeGltfFileToScene(file_name)); + ASSERT_NE(scene, nullptr); + + EXPECT_EQ(scene->NumAnimations(), 9); + + const std::vector animation_names{ + "Step Scale", "Linear Scale", + "CubicSpline Scale", "Step Rotation", + "CubicSpline Rotation", "Linear Rotation", + "Step Translation", "CubicSpline Translation", + "Linear Translation"}; + for (int i = 0; i < scene->NumAnimations(); ++i) { + const Animation *const anim = scene->GetAnimation(AnimationIndex(i)); + ASSERT_NE(anim, nullptr); + ASSERT_EQ(anim->GetName(), animation_names[i]); + } +} + +TEST(GltfDecoderTest, DuplicatePrimitives) { + const std::string file_name = "DuplicateMeshes/duplicate_meshes.gltf"; + const std::unique_ptr scene(DecodeGltfFileToScene(file_name)); + ASSERT_NE(scene, nullptr); + + // There should be only one unique base mesh in the scene and four mesh + // groups (instances). + ASSERT_EQ(scene->NumMeshes(), 1); + ASSERT_EQ(scene->NumMeshGroups(), 4); + + // There should be two materials used by the instances. + ASSERT_EQ(scene->GetMaterialLibrary().NumMaterials(), 2); +} + +TEST(GltfDecoderTest, SimpleSkin) { + // This is a simple skin example from glTF tutorial. + const std::string file_name = "simple_skin.gltf"; + const std::unique_ptr scene(DecodeGltfFileToScene(file_name)); + ASSERT_NE(scene, nullptr); + + // Check scene size. + ASSERT_EQ(scene->NumMeshes(), 1); + ASSERT_EQ(scene->NumMeshGroups(), 1); + ASSERT_EQ(scene->GetMeshGroup(draco::MeshGroupIndex(0))->NumMeshInstances(), + 1); + ASSERT_EQ(scene->NumNodes(), 3); + ASSERT_EQ(scene->NumRootNodes(), 1); + ASSERT_EQ(scene->GetMaterialLibrary().NumMaterials(), 1); + ASSERT_EQ(scene->NumAnimations(), 1); + ASSERT_EQ(scene->NumSkins(), 1); + + // Check animation size. + const Animation *const animation = scene->GetAnimation(AnimationIndex(0)); + ASSERT_NE(animation, nullptr); + ASSERT_EQ(animation->NumSamplers(), 1); + ASSERT_EQ(animation->NumChannels(), 1); + ASSERT_EQ(animation->NumNodeAnimationData(), 2); + + // Check animation sampler. + const AnimationSampler *const sampler = animation->GetSampler(0); + ASSERT_NE(sampler, nullptr); + ASSERT_EQ(sampler->input_index, 0); + ASSERT_EQ(sampler->interpolation_type, + AnimationSampler::SamplerInterpolation::LINEAR); + ASSERT_EQ(sampler->output_index, 1); + + // Check animation channel. + const AnimationChannel *const channel = animation->GetChannel(0); + ASSERT_NE(channel, nullptr); + ASSERT_EQ(channel->sampler_index, 0); + ASSERT_EQ(channel->target_index, 2); + ASSERT_EQ(channel->transformation_type, + AnimationChannel::ChannelTransformation::ROTATION); + + // Check the first node animation data. + { + const NodeAnimationData *const node_animation = + animation->GetNodeAnimationData(0); + ASSERT_EQ(node_animation->ComponentSize(), 4); + ASSERT_EQ(node_animation->NumComponents(), 1); + ASSERT_EQ(node_animation->count(), 12); + ASSERT_EQ(node_animation->type(), NodeAnimationData::Type::SCALAR); + ASSERT_FALSE(node_animation->normalized()); + const std::vector &node_animation_data = *node_animation->GetData(); + const std::vector expected_node_animation_data{ + 0.0f, 0.5f, 1.0f, 1.5f, 2.0f, 2.5f, 3.0f, 3.5f, 4.0f, 4.5f, 5.0f, 5.5f}; + ASSERT_EQ(node_animation_data, expected_node_animation_data); + } + + // Check the second node animation data. + { + const NodeAnimationData *const node_animation = + animation->GetNodeAnimationData(1); + ASSERT_EQ(node_animation->ComponentSize(), 4); + ASSERT_EQ(node_animation->NumComponents(), 4); + ASSERT_EQ(node_animation->count(), 12); + ASSERT_EQ(node_animation->type(), NodeAnimationData::Type::VEC4); + ASSERT_FALSE(node_animation->normalized()); + const std::vector &node_animation_data = *node_animation->GetData(); + std::cout << std::endl; + // clang-format off + const std::vector expected_node_animation_data{ + 0.000f, 0.000f, 0.000f, 1.000f, + 0.000f, 0.000f, 0.383f, 0.924f, + 0.000f, 0.000f, 0.707f, 0.707f, + 0.000f, 0.000f, 0.707f, 0.707f, + 0.000f, 0.000f, 0.383f, 0.924f, + 0.000f, 0.000f, 0.000f, 1.000f, + 0.000f, 0.000f, 0.000f, 1.000f, + 0.000f, 0.000f, -0.383f, 0.924f, + 0.000f, 0.000f, -0.707f, 0.707f, + 0.000f, 0.000f, -0.707f, 0.707f, + 0.000f, 0.000f, -0.383f, 0.924f, + 0.000f, 0.000f, 0.000f, 1.000f}; + // clang-format on + ASSERT_EQ(node_animation_data, expected_node_animation_data); + } + + // Check skin. + const Skin *const skin = scene->GetSkin(SkinIndex(0)); + ASSERT_NE(skin, nullptr); + ASSERT_EQ(skin->NumJoints(), 2); + ASSERT_EQ(skin->GetJointRoot(), kInvalidSceneNodeIndex); + ASSERT_EQ(skin->GetJoint(0), SceneNodeIndex(1)); + ASSERT_EQ(skin->GetJoint(1), SceneNodeIndex(2)); + + // Check inverse bind matrices. + const NodeAnimationData &bind_matrices = skin->GetInverseBindMatrices(); + ASSERT_EQ(bind_matrices.type(), NodeAnimationData::Type::MAT4); + ASSERT_EQ(bind_matrices.count(), 2); + ASSERT_EQ(bind_matrices.normalized(), false); + ASSERT_NE(bind_matrices.GetData(), nullptr); + const std::vector &bind_matrices_data = *bind_matrices.GetData(); + // clang-format off + const std::vector expected_bind_matrices_data{ + // First matrix. + 1.0f, 0.0f, 0.0f, 0.0f, + 0.0f, 1.0f, 0.0f, 0.0f, + 0.0f, 0.0f, 1.0f, 0.0f, + -0.5f, -1.0f, 0.0f, 1.0f, + // Second matrix. + 1.0f, 0.0f, 0.0f, 0.0f, + 0.0f, 1.0f, 0.0f, 0.0f, + 0.0f, 0.0f, 1.0f, 0.0f, + -0.5f, -1.0f, 0.0f, 1.0f}; + // clang-format on + ASSERT_EQ(bind_matrices_data, expected_bind_matrices_data); + + // Check mesh size. + const Mesh &mesh = scene->GetMesh(MeshIndex(0)); + ASSERT_EQ(mesh.num_faces(), 8); + ASSERT_EQ(mesh.num_points(), 10); + ASSERT_EQ(mesh.num_attributes(), 3); + + // Check vertex joint indices. + const PointAttribute *const joints_att = + mesh.GetNamedAttribute(GeometryAttribute::JOINTS); + ASSERT_NE(joints_att, nullptr); + ASSERT_EQ(joints_att->data_type(), DT_UINT16); + ASSERT_EQ(joints_att->num_components(), 4); + ASSERT_EQ(joints_att->size(), 1); + // clang-format off + const std::array expected_joints = { + // Each vertex is associated with four joints. + 0, 1, 0, 0, + 0, 1, 0, 0, + 0, 1, 0, 0, + 0, 1, 0, 0, + 0, 1, 0, 0, + 0, 1, 0, 0, + 0, 1, 0, 0, + 0, 1, 0, 0, + 0, 1, 0, 0, + 0, 1, 0, 0 }; + // clang-format on + std::array joints; + for (draco::PointIndex pi(0); pi < mesh.num_points(); ++pi) { + joints_att->GetMappedValue(pi, &joints[4 * pi.value()]); + } + ASSERT_EQ(joints, expected_joints); + + // Check vertex joint weights. + const PointAttribute *const weights_att = + mesh.GetNamedAttribute(GeometryAttribute::WEIGHTS); + ASSERT_NE(weights_att, nullptr); + ASSERT_EQ(weights_att->data_type(), DT_FLOAT32); + ASSERT_EQ(weights_att->num_components(), 4); + ASSERT_EQ(weights_att->size(), 5); + // clang-format off + const std::array expected_weights = { + // Each vertex has four joint weights. + 1.00f, 0.00f, 0.00f, 0.00f, + 1.00f, 0.00f, 0.00f, 0.00f, + 0.75f, 0.25f, 0.00f, 0.00f, + 0.75f, 0.25f, 0.00f, 0.00f, + 0.50f, 0.50f, 0.00f, 0.00f, + 0.50f, 0.50f, 0.00f, 0.00f, + 0.25f, 0.75f, 0.00f, 0.00f, + 0.25f, 0.75f, 0.00f, 0.00f, + 0.00f, 1.00f, 0.00f, 0.00f, + 0.00f, 1.00f, 0.00f, 0.00f }; + // clang-format on + std::array weights; + for (draco::PointIndex pi(0); pi < mesh.num_points(); ++pi) { + weights_att->GetMappedValue(pi, &weights[4 * pi.value()]); + } + ASSERT_EQ(weights, expected_weights); +} + +TEST(GltfDecoderTest, DecodeMeshWithImplicitPrimitiveIndices) { + // Check that glTF primitives with implicit indices can be loaded as a mesh. + const std::string file_name = "Fox/glTF/Fox.gltf"; + const std::unique_ptr mesh(DecodeGltfFile(file_name)); + ASSERT_NE(mesh, nullptr); + ASSERT_EQ(mesh->num_faces(), 576); +} + +TEST(GltfDecoderTest, DecodeSceneWithImplicitPrimitiveIndices) { + // Check that glTF primitives with implicit indices can be loaded as a scene. + const std::string file_name = "Fox/glTF/Fox.gltf"; + const std::unique_ptr scene(DecodeGltfFileToScene(file_name)); + ASSERT_NE(scene, nullptr); + ASSERT_EQ(scene->NumMeshes(), 1); + ASSERT_EQ(scene->GetMesh(MeshIndex(0)).num_faces(), 576); +} + +TEST(GltfDecoderTest, DecodeFromBufferToMesh) { + // Checks that a mesh can be decoded from buffer in GLB format. + // Read GLB file contents into a buffer. + const std::string file_name = "KhronosSampleModels/Duck/glTF_Binary/Duck.glb"; + const std::string file_path = GetTestFileFullPath(file_name); + std::vector file_data; + ASSERT_TRUE(ReadFileToBuffer(file_path, &file_data)); + DecoderBuffer buffer; + buffer.Init(file_data.data(), file_data.size()); + + // Decode mesh from buffer. + GltfDecoder decoder; + DRACO_ASSIGN_OR_ASSERT(auto mesh, decoder.DecodeFromBuffer(&buffer)); + ASSERT_NE(mesh, nullptr); + + // Decode mesh from GLB file. + const std::unique_ptr expected_mesh(DecodeGltfFile(file_name)); + ASSERT_NE(expected_mesh, nullptr); + + // Check that meshes decoded from the buffer and from GLB file are equivalent. + MeshAreEquivalent eq; + ASSERT_TRUE(eq(*mesh, *expected_mesh)); +} + +TEST(GltfDecoderTest, DecodeGraph) { + // Checks that we can decode a scene with a general graph structure where a + // node has multiple parents. + // The input model has one root node, 4 children nodes that all point to a + // single node that contains the cube mesh. + const std::string file_name = "CubeScaledInstances/glTF/cube_att.gltf"; + const std::string file_path = GetTestFileFullPath(file_name); + + // First decode the scene into a tree-graph. + draco::GltfDecoder dec_tree; + DRACO_ASSIGN_OR_ASSERT(auto scene_tree, + dec_tree.DecodeFromFileToScene(file_path)); + // We expect to have 9 nodes with 4 mesh instances. The leaf node with the + // cube is duplicated 4 times, once for each instance. + ASSERT_EQ(scene_tree->NumNodes(), 9); + auto instances_tree = draco::SceneUtils::ComputeAllInstances(*scene_tree); + ASSERT_EQ(instances_tree.size(), 4); + + // Decode the scene into a scene-graph. + draco::GltfDecoder dec_graph; + dec_graph.SetSceneGraphMode(draco::GltfDecoder::GltfSceneGraphMode::DAG); + DRACO_ASSIGN_OR_ASSERT(auto scene_graph, + dec_graph.DecodeFromFileToScene(file_path)); + + // We expect to have 6 nodes with 4 mesh instances. The leaf node is shared + // for all mesh instances. + ASSERT_EQ(scene_graph->NumNodes(), 6); + auto instances_graph = draco::SceneUtils::ComputeAllInstances(*scene_graph); + ASSERT_EQ(instances_graph.size(), 4); + + // Check that all instances share the same scene node. + for (draco::MeshInstanceIndex mii(1); mii < 4; ++mii) { + ASSERT_EQ(instances_graph[mii - 1].scene_node_index, + instances_graph[mii].scene_node_index); + } +} + +TEST(GltfDecoderTest, CorrectVolumeThicknessFactor) { + // Checks that when a model is decoded as draco::Mesh the PBR material volume + // thickness factor is corrected according to geometry transformation scale in + // the scene graph. + constexpr float kDragonScale = 0.25f; + constexpr float kDragonVolumeThickness = 2.27f; + + // Read model as draco::Scene and check dragon mesh transformation scale and + // its PBR material volume thickness factor. + const std::unique_ptr scene = draco::ReadSceneFromTestFile( + "KhronosSampleModels/DragonAttenuation/glTF/DragonAttenuation.gltf"); + ASSERT_NE(scene, nullptr); + auto instances = draco::SceneUtils::ComputeAllInstances(*scene); + ASSERT_EQ(instances.size(), 2); + ASSERT_EQ(instances[MeshInstanceIndex(0)].transform.col(0).norm(), + kDragonScale); + ASSERT_EQ(scene->GetMaterialLibrary().GetMaterial(1)->GetThicknessFactor(), + kDragonVolumeThickness); + + // Read model as draco::Mesh and check corrected volume thickness factor. + const std::unique_ptr mesh = draco::ReadMeshFromTestFile( + "KhronosSampleModels/DragonAttenuation/glTF/DragonAttenuation.gltf"); + ASSERT_NE(mesh, nullptr); + ASSERT_EQ(mesh->GetMaterialLibrary().GetMaterial(1)->GetThicknessFactor(), + kDragonScale * kDragonVolumeThickness); +} + +TEST(GltfDecoderTest, DecodeLightsIntoMesh) { + // Checks that a model with lights can be decoded into draco::Mesh with the + // lights discarded. + const std::string file_name = "sphere_lights.gltf"; + const std::unique_ptr mesh(DecodeGltfFile(file_name)); + ASSERT_NE(mesh, nullptr); + ASSERT_EQ(mesh->num_faces(), 224); +} + +TEST(GltfDecoderTest, DecodeLightsIntoScene) { + // Checks that a model with lights can be decoded into draco::Scene. + const std::string file_name = "sphere_lights.gltf"; + const std::unique_ptr scene(DecodeGltfFileToScene(file_name)); + ASSERT_NE(scene, nullptr); + ASSERT_EQ(scene->NumLights(), 4); + + // Check spot light with all properties specified. + Light &light = *scene->GetLight(LightIndex(0)); + ASSERT_EQ(light.GetName(), "Blue Lightsaber"); + ASSERT_EQ(light.GetColor(), draco::Vector3f(0.72f, 0.71f, 1.00f)); + ASSERT_EQ(light.GetIntensity(), 3.0); + ASSERT_EQ(light.GetType(), draco::Light::SPOT); + ASSERT_EQ(light.GetRange(), 100); + ASSERT_EQ(light.GetInnerConeAngle(), 0.2); + ASSERT_EQ(light.GetOuterConeAngle(), 0.8); + + // Check point light with all properties specified. + light = *scene->GetLight(LightIndex(1)); + ASSERT_EQ(light.GetName(), "The Star of Earendil"); + ASSERT_EQ(light.GetColor(), draco::Vector3f(0.90f, 0.97f, 1.0f)); + ASSERT_EQ(light.GetIntensity(), 5.0); + ASSERT_EQ(light.GetType(), draco::Light::POINT); + ASSERT_EQ(light.GetRange(), 1000); + ASSERT_EQ(light.GetInnerConeAngle(), 0.0); + ASSERT_NEAR(light.GetOuterConeAngle(), DRACO_PI / 4.0f, 1e-8); + + // Check directional light with some properties specified. + light = *scene->GetLight(LightIndex(2)); + ASSERT_EQ(light.GetName(), "Arc Reactor"); + ASSERT_EQ(light.GetColor(), draco::Vector3f(0.9f, 0.9, 0.9f)); + ASSERT_EQ(light.GetIntensity(), 1.0); + ASSERT_EQ(light.GetType(), draco::Light::DIRECTIONAL); + ASSERT_EQ(light.GetRange(), 200.0); + + // Check spot light with no properties specified. + light = *scene->GetLight(LightIndex(3)); + ASSERT_EQ(light.GetName(), ""); + ASSERT_EQ(light.GetColor(), draco::Vector3f(1.0f, 1.0f, 1.0f)); + ASSERT_EQ(light.GetIntensity(), 1.0); + ASSERT_EQ(light.GetType(), draco::Light::SPOT); + ASSERT_EQ(light.GetRange(), std::numeric_limits::max()); + ASSERT_EQ(light.GetInnerConeAngle(), 0.0); + ASSERT_NEAR(light.GetOuterConeAngle(), DRACO_PI / 4.0f, 1e-8); + + // Check that lights are referenced by the scene nodes. + ASSERT_EQ(scene->GetNode(SceneNodeIndex(0))->GetLightIndex(), + kInvalidLightIndex); + ASSERT_EQ(scene->GetNode(SceneNodeIndex(1))->GetLightIndex(), LightIndex(0)); + ASSERT_EQ(scene->GetNode(SceneNodeIndex(2))->GetLightIndex(), LightIndex(2)); + ASSERT_EQ(scene->GetNode(SceneNodeIndex(3))->GetLightIndex(), LightIndex(3)); + ASSERT_EQ(scene->GetNode(SceneNodeIndex(4))->GetLightIndex(), LightIndex(1)); +} + +TEST(GltfDecoderTest, MaterialsVariants) { + // Checks that a model with KHR_materials_variants extension can be decoded. + draco::GltfDecoder decoder; + DRACO_ASSIGN_OR_ASSERT(auto scene, + decoder.DecodeFromFileToScene(GetTestFileFullPath( + "KhronosSampleModels/DragonAttenuation/glTF/" + "DragonAttenuation.gltf"))); + ASSERT_NE(scene, nullptr); + const draco::MaterialLibrary &library = scene->GetMaterialLibrary(); + ASSERT_EQ(library.NumMaterialsVariants(), 2); + ASSERT_EQ(library.GetMaterialsVariantName(0), "Attenuation"); + ASSERT_EQ(library.GetMaterialsVariantName(1), "Surface Color"); + + // Check that the cloth mesh has no material variants. + const draco::MeshGroup &cloth_group = + *scene->GetMeshGroup(draco::MeshGroupIndex(0)); + ASSERT_EQ(cloth_group.GetName(), "Cloth Backdrop"); + ASSERT_EQ(cloth_group.NumMeshInstances(), 1); + const auto &cloth_mappings = + cloth_group.GetMeshInstance(0).materials_variants_mappings; + ASSERT_EQ(cloth_mappings.size(), 0); + + // Check that the dragon has correct materials variants. + const draco::MeshGroup &dragon_group = + *scene->GetMeshGroup(draco::MeshGroupIndex(1)); + ASSERT_EQ(dragon_group.GetName(), "Dragon"); + ASSERT_EQ(dragon_group.NumMeshInstances(), 1); + const auto &dragon_mappings = + dragon_group.GetMeshInstance(0).materials_variants_mappings; + ASSERT_EQ(dragon_mappings.size(), 2); + ASSERT_EQ(dragon_mappings[0].material, 1); + ASSERT_EQ(dragon_mappings[1].material, 2); + ASSERT_EQ(dragon_mappings[0].variants.size(), 1); + ASSERT_EQ(dragon_mappings[1].variants.size(), 1); + ASSERT_EQ(dragon_mappings[0].variants[0], 0); + ASSERT_EQ(dragon_mappings[1].variants[0], 1); +} + +TEST(GltfDecoderTest, DecodeMeshWithMeshFeaturesWithStructuralMetadata) { + // Checks decoding of a simple glTF with mesh features and structural metadata + // property table as draco::Mesh. + constexpr bool kDracoCompressionEnabled = false; + const auto path = GetTestFileFullPath("BoxMeta/glTF/BoxMeta.gltf"); + draco::GltfDecoder decoder; + DRACO_ASSIGN_OR_ASSERT(auto mesh, decoder.DecodeFromFile(path)); + ASSERT_NE(mesh, nullptr); + GltfTestHelper::CheckBoxMetaMeshFeatures(*mesh, kDracoCompressionEnabled); + GltfTestHelper::CheckBoxMetaStructuralMetadata(*mesh); +} + +TEST(GltfDecoderTest, DecodeMeshWithMeshFeaturesWithDracoCompression) { + // Checks decoding of a simple glTF with mesh features compressed with Draco + // as draco::Mesh. + constexpr bool kDracoCompressionEnabled = true; + const auto path = GetTestFileFullPath("BoxMetaDraco/glTF/BoxMetaDraco.gltf"); + draco::GltfDecoder decoder; + DRACO_ASSIGN_OR_ASSERT(auto mesh, decoder.DecodeFromFile(path)); + ASSERT_NE(mesh, nullptr); + GltfTestHelper::CheckBoxMetaMeshFeatures(*mesh, kDracoCompressionEnabled); +} + +TEST(GltfDecoderTest, DecodeSceneWithMeshFeaturesWithStructuralMetadata) { + // Checks decoding of a simple glTF with mesh features and structural metadata + // property table as draco::Scene. + constexpr bool kHasDracoCompression = false; + const auto path = GetTestFileFullPath("BoxMeta/glTF/BoxMeta.gltf"); + draco::GltfDecoder decoder; + DRACO_ASSIGN_OR_ASSERT(auto scene, decoder.DecodeFromFileToScene(path)); + ASSERT_NE(scene, nullptr); + GltfTestHelper::CheckBoxMetaMeshFeatures(*scene, kHasDracoCompression); + GltfTestHelper::CheckBoxMetaStructuralMetadata(*scene); +} + +TEST(GltfDecoderTest, DecodeSceneWithMeshFeaturesWithDracoCompression) { + // Checks decoding of a simple glTF with mesh features compressed with Draco + // as draco::Scene. + constexpr bool kHasDracoCompression = true; + const auto path = GetTestFileFullPath("BoxMetaDraco/glTF/BoxMetaDraco.gltf"); + draco::GltfDecoder decoder; + DRACO_ASSIGN_OR_ASSERT(auto scene, decoder.DecodeFromFileToScene(path)); + ASSERT_NE(scene, nullptr); + GltfTestHelper::CheckBoxMetaMeshFeatures(*scene, kHasDracoCompression); +} + +TEST(GltfDecoderTest, DecodePointCloudToMesh) { + // Checks decoding of a simple glTF with point primitives (no meshes). + const auto path = GetTestFileFullPath( + "SphereTwoMaterials/sphere_two_materials_point_cloud.gltf"); + draco::GltfDecoder decoder; + DRACO_ASSIGN_OR_ASSERT(auto mesh, decoder.DecodeFromFile(path)); + ASSERT_NE(mesh, nullptr); + + // Check the point cloud has expected number of points and attributes. + ASSERT_EQ(mesh->num_faces(), 0); + ASSERT_EQ(mesh->num_points(), 462); + + ASSERT_EQ(mesh->NumNamedAttributes(draco::GeometryAttribute::NORMAL), 1); + ASSERT_EQ(mesh->NumNamedAttributes(draco::GeometryAttribute::TEX_COORD), 1); + ASSERT_EQ(mesh->NumNamedAttributes(draco::GeometryAttribute::TANGENT), 1); + ASSERT_EQ(mesh->NumNamedAttributes(draco::GeometryAttribute::MATERIAL), 1); + + // Check the point cloud has two materials. + ASSERT_EQ(mesh->GetNamedAttribute(draco::GeometryAttribute::MATERIAL)->size(), + 2); +} + +TEST(GltfDecoderTest, DecodeMeshAndPointCloudToMesh) { + // Checks decoding of a simple glTF with a mesh and point primitives into + // draco::Mesh. This should fail (draco::Mesh can't support mixed primitives). + const auto path = GetTestFileFullPath( + "SphereTwoMaterials/sphere_two_materials_mesh_and_point_cloud.gltf"); + draco::GltfDecoder decoder; + ASSERT_FALSE(decoder.DecodeFromFile(path).ok()); +} + +TEST(GltfDecoderTest, DecodePointCloudToScene) { + // Checks decoding of a simple glTF with point primitives (no meshes) into + // draco::Scene. + const auto path = GetTestFileFullPath( + "SphereTwoMaterials/sphere_two_materials_point_cloud.gltf"); + draco::GltfDecoder decoder; + DRACO_ASSIGN_OR_ASSERT(auto scene, decoder.DecodeFromFileToScene(path)); + ASSERT_NE(scene, nullptr); + + ASSERT_EQ(scene->NumMeshes(), 2); + + // Check that each point cloud has expected number of points and attributes. + for (draco::MeshIndex mi(0); mi < scene->NumMeshes(); ++mi) { + const auto &mesh = scene->GetMesh(mi); + ASSERT_EQ(mesh.num_faces(), 0); + ASSERT_EQ(mesh.num_points(), 231); + + ASSERT_EQ(mesh.NumNamedAttributes(draco::GeometryAttribute::NORMAL), 1); + ASSERT_EQ(mesh.NumNamedAttributes(draco::GeometryAttribute::TEX_COORD), 1); + ASSERT_EQ(mesh.NumNamedAttributes(draco::GeometryAttribute::TANGENT), 1); + ASSERT_EQ(mesh.NumNamedAttributes(draco::GeometryAttribute::MATERIAL), 0); + } + + // Check the materials are properly assigned to each point cloud. + const auto instances = draco::SceneUtils::ComputeAllInstances(*scene); + ASSERT_EQ(instances.size(), 2); + ASSERT_EQ(draco::SceneUtils::GetMeshInstanceMaterialIndex( + *scene, instances[draco::MeshInstanceIndex(0)]), + 0); + ASSERT_EQ(draco::SceneUtils::GetMeshInstanceMaterialIndex( + *scene, instances[draco::MeshInstanceIndex(1)]), + 1); +} + +TEST(GltfDecoderTest, DecodeMeshAndPointCloudToScene) { + // Checks decoding of a simple glTF with a mesh and point primitives into + // draco::Scene. + const auto path = GetTestFileFullPath( + "SphereTwoMaterials/sphere_two_materials_mesh_and_point_cloud.gltf"); + draco::GltfDecoder decoder; + DRACO_ASSIGN_OR_ASSERT(auto scene, decoder.DecodeFromFileToScene(path)); + ASSERT_NE(scene, nullptr); + + ASSERT_EQ(scene->NumMeshes(), 2); + + // First mesh should be a real mesh while the other one should be a point + // cloud (no faces). Otherwise, they should have the same properties. + for (draco::MeshIndex mi(0); mi < scene->NumMeshes(); ++mi) { + const auto &mesh = scene->GetMesh(mi); + ASSERT_EQ(mesh.num_faces(), mi.value() == 0 ? 224 : 0); + ASSERT_EQ(mesh.num_points(), 231); + + ASSERT_EQ(mesh.NumNamedAttributes(draco::GeometryAttribute::NORMAL), 1); + ASSERT_EQ(mesh.NumNamedAttributes(draco::GeometryAttribute::TEX_COORD), 1); + ASSERT_EQ(mesh.NumNamedAttributes(draco::GeometryAttribute::TANGENT), 1); + } +} + +TEST(GltfDecoderTest, TestLoadUnsupportedTexCoordAttributes) { + // Checks that unsupported attributes (TEXCOORD_2 ... TEXCOORD_7) are ignored + // without causing the decoder to fail. + auto scene = draco::ReadSceneFromTestFile("UnusedTexCoords/TexCoord2.gltf"); + ASSERT_NE(scene, nullptr); + ASSERT_EQ(scene->GetMesh(draco::MeshIndex(0)) + .NumNamedAttributes(draco::GeometryAttribute::TEX_COORD), + 2); +} + +} // namespace draco +#endif // DRACO_TRANSCODER_SUPPORTED diff --git a/contrib/draco/src/draco/io/gltf_encoder.cc b/contrib/draco/src/draco/io/gltf_encoder.cc new file mode 100644 index 000000000..0509b588f --- /dev/null +++ b/contrib/draco/src/draco/io/gltf_encoder.cc @@ -0,0 +1,3662 @@ +// Copyright 2018 The Draco Authors. +// +// 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 "draco/io/gltf_encoder.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "draco/attributes/geometry_attribute.h" +#include "draco/attributes/point_attribute.h" +#include "draco/compression/draco_compression_options.h" +#include "draco/compression/expert_encode.h" +#include "draco/core/draco_types.h" +#include "draco/core/vector_d.h" +#include "draco/io/file_utils.h" +#include "draco/io/file_writer_utils.h" +#include "draco/io/gltf_utils.h" +#include "draco/io/texture_io.h" +#include "draco/mesh/mesh_features.h" +#include "draco/mesh/mesh_splitter.h" +#include "draco/mesh/mesh_utils.h" +#include "draco/scene/instance_array.h" +#include "draco/scene/scene_indices.h" +#include "draco/scene/scene_utils.h" +#include "draco/texture/texture_utils.h" + +namespace draco { + +// Values are specfified from glTF 2.0 sampler spec. See here for more +// information: +// https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#sampler +int TextureFilterTypeToGltfValue(TextureMap::FilterType filter_type) { + switch (filter_type) { + case TextureMap::NEAREST: + return 9728; + case TextureMap::LINEAR: + return 9729; + case TextureMap::NEAREST_MIPMAP_NEAREST: + return 9984; + case TextureMap::LINEAR_MIPMAP_NEAREST: + return 9985; + case TextureMap::NEAREST_MIPMAP_LINEAR: + return 9986; + case TextureMap::LINEAR_MIPMAP_LINEAR: + return 9987; + default: + return -1; + } +} + +// Values are specfified from glTF 2.0 sampler spec. See here for more +// information: +// https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#sampler +int TextureAxisWrappingModeToGltfValue(TextureMap::AxisWrappingMode mode) { + switch (mode) { + case TextureMap::CLAMP_TO_EDGE: + return 33071; + case TextureMap::MIRRORED_REPEAT: + return 33648; + case TextureMap::REPEAT: + return 10497; + default: + return -1; + } +} + +// Checks |att| metadata entry in |mesh| with key "attribute_name" and returns +// entry value if it begins with "_FEATURE_ID_", or an empty string otherwise. +std::string GetFeatureIdAttributeName(const PointAttribute &att, + const Mesh &mesh) { + const auto *const metadata = + mesh.GetAttributeMetadataByAttributeId(att.unique_id()); + if (metadata) { + std::string attribute_name; + if (metadata->GetEntryString("attribute_name", &attribute_name)) { + constexpr char kPrefix[] = "_FEATURE_ID_"; + if (attribute_name.rfind(kPrefix) == 0) { + return attribute_name; + } + } + } + return std::string(); +} + +// Struct to hold glTF Scene data. +struct GltfScene { + std::vector node_indices; +}; + +// Struct to hold glTF Node data. +struct GltfNode { + GltfNode() + : mesh_index(-1), + skin_index(-1), + light_index(-1), + instance_array_index(-1), + root_node(false) {} + + std::string name; + std::vector childern_indices; + int mesh_index; + int skin_index; + int light_index; + int instance_array_index; + bool root_node; + TrsMatrix trs_matrix; +}; + +// Struct to hold image data. +struct GltfImage { + std::string image_name; + const Texture *texture; + std::unique_ptr owned_texture; + int num_components = 0; + int buffer_view = -1; + std::string mime_type; +}; + +// Struct to hold texture filtering options. The members are based on glTF 2.0 +// samplers. For more information see: +// https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#samplers +struct TextureSampler { + TextureSampler(TextureMap::FilterType min, TextureMap::FilterType mag, + TextureMap::WrappingMode mode) + : min_filter(min), mag_filter(mag), wrapping_mode(mode) {} + + bool operator==(const TextureSampler &other) const { + if (min_filter != other.min_filter) { + return false; + } + if (mag_filter != other.mag_filter) { + return false; + } + return wrapping_mode.s == other.wrapping_mode.s && + wrapping_mode.t == other.wrapping_mode.t; + } + + TextureMap::FilterType min_filter = TextureMap::UNSPECIFIED; + TextureMap::FilterType mag_filter = TextureMap::UNSPECIFIED; + TextureMap::WrappingMode wrapping_mode = {TextureMap::CLAMP_TO_EDGE, + TextureMap::CLAMP_TO_EDGE}; +}; + +// Struct to hold texture data. Multiple textures can reference the same image. +struct GltfTexture { + GltfTexture(int image, int sampler) + : image_index(image), sampler_index(sampler) {} + bool operator==(const GltfTexture &other) const { + return image_index == other.image_index && + sampler_index == other.sampler_index; + } + int image_index; + int sampler_index; +}; + +// Struct to hold glTF Accessor data. +struct GltfAccessor { + GltfAccessor() + : buffer_view_index(-1), + byte_stride(0), + component_type(-1), + normalized(false) {} + + int buffer_view_index; + int byte_stride; + int component_type; + int64_t count; + std::vector max; + std::vector min; + std::string type; + bool normalized; +}; + +// Struct to hold glTF BufferView data. Currently there is only one Buffer, so +// there is no need to store a buffer index. +struct GltfBufferView { + int64_t buffer_byte_offset = -1; + int64_t byte_length = 0; + int target = 0; +}; + +// Struct to hold information about a Draco compressed mesh. +struct GltfDracoCompressedMesh { + int buffer_view_index = -1; + std::map attributes; +}; + +// Struct to hold glTF Primitive data. +struct GltfPrimitive { + GltfPrimitive() : indices(-1), mode(4), material(0) {} + + int indices; + int mode; + int material; + std::vector material_variants_mappings; + std::vector mesh_features; + std::map attributes; + GltfDracoCompressedMesh compressed_mesh_info; +}; + +struct GltfMesh { + std::string name; + std::vector primitives; +}; + +// Class to hold and output glTF data. +class GltfAsset { + public: + // glTF value types and values. + enum ComponentType { + BYTE = 5120, + UNSIGNED_BYTE = 5121, + SHORT = 5122, + UNSIGNED_SHORT = 5123, + UNSIGNED_INT = 5125, + FLOAT = 5126 + }; + // Return the size of the component based on |max_value|. + static int UnsignedIntComponentSize(unsigned int max_value); + + // Return component type based on |max_value|. + static ComponentType UnsignedIntComponentType(unsigned int max_value); + + GltfAsset(); + + std::string generator() const { return generator_; } + std::string version() const { return version_; } + std::string buffer_name() const { return buffer_name_; } + void buffer_name(const std::string &name) { buffer_name_ = name; } + const EncoderBuffer *Buffer() const { return &buffer_; } + + // Convert a Draco Mesh to glTF data. + bool AddDracoMesh(const Mesh &mesh); + + // Convert a Draco Scene to glTF data. + Status AddScene(const Scene &scene); + + // Copy the glTF data to |buf_out|. + Status Output(EncoderBuffer *buf_out); + + // Return the output image referenced by |index|. + const GltfImage *GetImage(int index) const; + + // Return the number of images added to the GltfAsset. + int NumImages() const { return images_.size(); } + + const std::string &image_name(int i) const { return images_[i].image_name; } + + void set_add_images_to_buffer(bool flag) { add_images_to_buffer_ = flag; } + bool add_images_to_buffer() const { return add_images_to_buffer_; } + void set_output_type(GltfEncoder::OutputType type) { output_type_ = type; } + GltfEncoder::OutputType output_type() const { return output_type_; } + void set_json_output_mode(JsonWriter::Mode mode) { gltf_json_.SetMode(mode); } + + private: + // Pad |buffer_| to 4 byte boundary. + bool PadBuffer(); + + // Returns the index of the scene that was added. -1 on error. + int AddScene(); + + // Add a glTF attribute index to |draco_extension|. + void AddAttributeToDracoExtension( + const Mesh &mesh, GeometryAttribute::Type type, int index, + const std::string &name, GltfDracoCompressedMesh *compressed_mesh_info); + + // Compresses |mesh| using Draco. On success returns the buffer_view in + // |primitive| and number of encoded points and faces. + Status CompressMeshWithDraco(const Mesh &mesh, + const Eigen::Matrix4d &transform, + GltfPrimitive *primitive, + int64_t *num_encoded_points, + int64_t *num_encoded_faces); + + // Adds a Draco mesh associated with a material id and material variants. + bool AddDracoMesh(const Mesh &mesh, int material_id, + const std::vector + &material_variants_mappings, + const Eigen::Matrix4d &transform); + + // Add the Draco mesh indices to the glTF data. |num_encoded_faces| is the + // number of faces encoded in |mesh|, which can be different than + // mesh.numfaces(). Returns the index of the accessor that was added. -1 on + // error. + int AddDracoIndices(const Mesh &mesh, int64_t num_encoded_faces); + + // Add the Draco mesh positions attribute to the glTF data. + // |num_encoded_points| is the number of points encoded in |mesh|, which can + // be different than mesh.num_points(). Returns the index of the accessor that + // was added. -1 on error. + int AddDracoPositions(const Mesh &mesh, int num_encoded_points); + + // Add the Draco mesh normals attribute to the glTF data. |num_encoded_points| + // is the number of points encoded in |mesh|, which can be different than + // mesh.num_points(). Returns the index of + // the accessor that was added. -1 on error. + int AddDracoNormals(const Mesh &mesh, int num_encoded_points); + + // Add the Draco mesh vertex color attribute to the glTF data. + // |num_encoded_points| is the number of points encoded in |mesh|, which can + // be different than mesh.num_points(). Returns the index of the accessor that + // was added. -1 on error. + int AddDracoColors(const Mesh &mesh, int num_encoded_points); + + // Add the Draco mesh texture attribute to the glTF data. |tex_coord_index| is + // the index into the texture coordinates added to |mesh|. + // |num_encoded_points| is the number of points encoded in |mesh|, which can + // be different than mesh.num_points(). Returns the index of the accessor that + // was added. -1 on error. + int AddDracoTexture(const Mesh &mesh, int tex_coord_index, + int num_encoded_points); + + // Add the Draco mesh tangent attribute to the glTF data. The Draco mesh + // tangents only contains the x, y, and z components and glTF needs the + // x, y, z, and w components for glTF mesh tangents. Note this is not true + // for tangents of glTF morph targets. This function will add the w component + // to the glTF tangents. |num_encoded_points| is the + // number of points encoded in |mesh|, which can be different than + // mesh.num_points(). Returns the index of the accessor that was added. + // -1 on error. + // Note: Tangents are not added if the attribute contains "auto_generated" + // metadata. See go/tangents_and_draco_simplifier for more details. + int AddDracoTangents(const Mesh &mesh, int num_encoded_points); + + int AddDracoJoints(const Mesh &mesh, int num_encoded_points); + int AddDracoWeights(const Mesh &mesh, int num_encoded_points); + std::vector> AddDracoGenerics( + const Mesh &mesh, int num_encoded_points); + + // Iterate through the materials that are associated with |mesh| and add them + // to the asset. Returns true if |mesh| does not contain any materials or all + // the materials are supported. Returns false if |mesh| contains materials + // that are not supported. + bool AddMaterials(const Mesh &mesh); + + // Checks whether a given Draco |attribute| has data of expected |data_type| + // and whether the data has one of expected |num_components|. Returns true + // when the |attribute| meets expectations, false otherwise. + static bool CheckDracoAttribute(const PointAttribute *attribute, + const std::set &data_types, + const std::set &num_components); + + // Returns the name of |texture|. If |texture|'s name is empty then it will + // generate a name using |texture_index| and |suffix|. If it cannot generate a + // name then it will return an empty string. + std::string GetTextureName(const Texture &texture, int texture_index, + const std::string &suffix) const; + + // Adds a new glTF image to the asset and returns its index. |owned_texture| + // is an optional argument that can be used when the added image is not + // contained in the encoded MaterialLibrary (e.g. for images that are locally + // modified before they are encoded to disk). The image file name is generated + // by combining |image_stem| and image mime type contained in the |texture|. + StatusOr AddImage(const std::string &image_stem, const Texture *texture, + int num_components); + StatusOr AddImage(const std::string &image_stem, const Texture *texture, + std::unique_ptr owned_texture, + int num_components); + + // Saves an image with a given |image_index| into a buffer. + Status SaveImageToBuffer(int image_index); + + // Adds |sampler| to vector of samplers and returns the index. If |sampler| is + // equal to default values then |sampler| is not added to the vector and + // returns -1. + StatusOr AddTextureSampler(const TextureSampler &sampler); + + // Adds a Draco SceneNode, referenced by |scene_node_index|, to the glTF data. + Status AddSceneNode(const Scene &scene, SceneNodeIndex scene_node_index); + + // Iterate through the materials that are associated with |scene| and add them + // to the asset. Returns true if |scene| does not contain any materials or all + // the materials are supported. Returns false if |scene| contains materials + // that are not supported. + bool AddMaterials(const Scene &scene); + + // Iterate through the animations that are associated with |scene| and add + // them to the asset. Returns OkStatus() if |scene| does not contain any + // animations. + Status AddAnimations(const Scene &scene); + + // Converts the data associated with |node_animation_data| and adds that to + // the encoder as an accessor. + StatusOr AddNodeAnimationData( + const NodeAnimationData &node_animation_data); + + // Iterate through the skins that are associated with |scene| and add + // them to the asset. Returns OkStatus() if |scene| does not contain any + // skins. + Status AddSkins(const Scene &scene); + + // Iterate through the lights that are associated with |scene| and add them to + // the asset. Returns OkStatus() if |scene| does not contain any lights. + Status AddLights(const Scene &scene); + + // Iterate through materials variants names that are associated with |scene| + // and add them to the asset. Returns OkStatus() if |scene| does not contain + // any materials variants. + Status AddMaterialsVariantsNames(const Scene &scene); + + // Iterate through the mesh group instance arrays that are associated with + // |scene| and add them to the asset. Returns OkStatus() if |scene| does not + // contain any mesh group instance arrays. + Status AddInstanceArrays(const Scene &scene); + + // Adds structural metadata from |geometry| to the asset, if any. + template + void AddStructuralMetadata(const GeometryT &geometry); + + // Adds float |data| representing |num_components|-length vectors to the + // encoder as accessor and return the new accessor index. + StatusOr AddData(const std::vector &data, int num_components); + + // Adds property table |data| as buffer view and returns buffer view index. + StatusOr AddBufferView(const PropertyTable::Property::Data &data); + + bool EncodeAssetProperty(EncoderBuffer *buf_out); + bool EncodeScenesProperty(EncoderBuffer *buf_out); + bool EncodeInitialSceneProperty(EncoderBuffer *buf_out); + bool EncodeNodesProperty(EncoderBuffer *buf_out); + Status EncodeMeshesProperty(EncoderBuffer *buf_out); + Status EncodePrimitiveExtensionsProperty(const GltfPrimitive &primitive, + EncoderBuffer *buf_out); + Status EncodeMaterials(EncoderBuffer *buf_out); + + // Encodes a color material. |red|, |green|, |blue|, |alpha|, and + // |metallic_factor| are values in the range of 0.0 - 1.0. + void EncodeColorMaterial(float red, float green, float blue, float alpha, + float metallic_factor); + Status EncodeDefaultMaterial(EncoderBuffer *buf_out); + + // Encodes a texture map. |object_name| is the name of the texture map. + // |image_index| is the index into the texture image array. |tex_coord_index| + // is the index into the texture coordinates. |texture_map| is a reference to + // the texture map that is going to be encoded. + Status EncodeTextureMap(const std::string &object_name, int image_index, + int tex_coord_index, const Material &material, + const TextureMap &texture_map); + + // Encodes a texture map similar to the method above. When the |object_name| + // is "texture" and |channels| is not empty, then the |channels| is encoded + // into the "channels" property as required by the "texture" object of the + // EXT_mesh_features extension. + Status EncodeTextureMap(const std::string &object_name, int image_index, + int tex_coord_index, const Material &material, + const TextureMap &texture_map, + const std::vector &channels); + Status EncodeMaterialsProperty(EncoderBuffer *buf_out); + + void EncodeMaterialUnlitExtension(const Material &material); + Status EncodeMaterialSheenExtension(const Material &material, + const Material &defaults, + int material_index); + Status EncodeMaterialTransmissionExtension(const Material &material, + const Material &defaults, + int material_index); + Status EncodeMaterialClearcoatExtension(const Material &material, + const Material &defaults, + int material_index); + Status EncodeMaterialVolumeExtension(const Material &material, + const Material &defaults, + int material_index); + Status EncodeMaterialIorExtension(const Material &material, + const Material &defaults); + Status EncodeMaterialSpecularExtension(const Material &material, + const Material &defaults, + int material_index); + Status EncodeTexture(const std::string &name, const std::string &stem_suffix, + TextureMap::Type type, int num_components, + const Material &material, int material_index); + Status EncodeAnimationsProperty(EncoderBuffer *buf_out); + Status EncodeSkinsProperty(EncoderBuffer *buf_out); + Status EncodeTopLevelExtensionsProperty(EncoderBuffer *buf_out); + Status EncodeLightsProperty(EncoderBuffer *buf_out); + Status EncodeMaterialsVariantsNamesProperty(EncoderBuffer *buf_out); + Status EncodeStructuralMetadataProperty(EncoderBuffer *buf_out); + bool EncodeAccessorsProperty(EncoderBuffer *buf_out); + bool EncodeBufferViewsProperty(EncoderBuffer *buf_out); + bool EncodeBuffersProperty(EncoderBuffer *buf_out); + Status EncodeExtensionsProperties(EncoderBuffer *buf_out); + + // Encodes a draco::VectorNX as a glTF array. + template + void EncodeVectorArray(const std::string &array_name, T vec) { + gltf_json_.BeginArray(array_name); + for (int i = 0; i < T::dimension; ++i) { + gltf_json_.OutputValue(vec[i]); + } + gltf_json_.EndArray(); + } + + // Add a mesh Draco attribute |att| that is comprised of floats to the glTF + // data. Returns the index accessor added to the glTF data. Returns -1 on + // error. + template + int AddAttribute(const PointAttribute &att, int num_points, + int num_encoded_points, bool compress) { + const int num_components = att.num_components(); + switch (num_components) { + case 1: + return AddAttribute<1, att_data_t>(att, num_points, num_encoded_points, + "SCALAR", compress); + break; + case 2: + return AddAttribute<2, att_data_t>(att, num_points, num_encoded_points, + "VEC2", compress); + break; + case 3: + return AddAttribute<3, att_data_t>(att, num_points, num_encoded_points, + "VEC3", compress); + break; + case 4: + return AddAttribute<4, att_data_t>(att, num_points, num_encoded_points, + "VEC4", compress); + break; + default: + break; + } + return -1; + } + + // Template method only has specialized implementations for known glTF types. + template + ComponentType GetComponentType() const = delete; + + template + int AddAttribute(const PointAttribute &att, int num_points, + int num_encoded_points, const std::string &type, + bool compress); + + std::string generator_; + std::string version_; + std::vector scenes_; + + // Initial scene to load. + int scene_index_; + + std::vector nodes_; + std::vector accessors_; + std::vector buffer_views_; + std::vector meshes_; + + // Data structure to copy the input meshes materials. + MaterialLibrary material_library_; + + std::vector images_; + std::vector textures_; + + std::unordered_map texture_to_image_index_map_; + + std::string buffer_name_; + EncoderBuffer buffer_; + JsonWriter gltf_json_; + + // Keeps track if the glTF mesh has been added. + std::map mesh_group_index_to_gltf_mesh_; + std::map> mesh_index_to_gltf_mesh_primitive_; + IndexTypeVector base_mesh_transforms_; + + struct EncoderAnimation { + std::string name; + std::vector> samplers; + std::vector> channels; + }; + std::vector> animations_; + + struct EncoderSkin { + EncoderSkin() : inverse_bind_matrices_index(-1), skeleton_index(-1) {} + int inverse_bind_matrices_index; + std::vector joints; + int skeleton_index; + }; + + // Instance array is represented by its attribute accessors. + struct EncoderInstanceArray { + EncoderInstanceArray() : translation(-1), rotation(-1), scale(-1) {} + int translation; + int rotation; + int scale; + }; + + std::vector> skins_; + std::vector> lights_; + std::vector materials_variants_names_; + std::vector instance_arrays_; + PropertyTable::Schema property_table_schema_; + std::vector property_tables_; + + // Indicates whether Draco compression is used for any of the asset meshes. + bool draco_compression_used_; + + // Indicates whether mesh features are used. + bool mesh_features_used_; + + // Counter for naming mesh feature textures. + int mesh_features_texture_index_; + + // If set GltfAsset will add the images to |buffer_| instead of writing the + // images to separate files. + bool add_images_to_buffer_; + + // Used to hold the extensions used and required by the glTF asset. + std::set extensions_used_; + std::set extensions_required_; + + std::vector texture_samplers_; + + GltfEncoder::OutputType output_type_; + + // Temporary storage for meshes created during the runtime of the GltfEncoder. + // We need to store them here to ensure their content doesn't get deleted + // before it is used by the encoder. + std::vector> local_meshes_; +}; + +int GltfAsset::UnsignedIntComponentSize(unsigned int max_value) { + // According to GLTF 2.0 spec, 0xff (and 0xffff respectively) are reserved for + // the primitive restart symbol. + if (max_value < 0xff) { + return 1; + } else if (max_value < 0xffff) { + return 2; + } + return 4; +} + +GltfAsset::ComponentType GltfAsset::UnsignedIntComponentType( + unsigned int max_value) { + // According to GLTF 2.0 spec, 0xff (and 0xffff respectively) are reserved for + // the primitive restart symbol. + if (max_value < 0xff) { + return UNSIGNED_BYTE; + } else if (max_value < 0xffff) { + return UNSIGNED_SHORT; + } + return UNSIGNED_INT; +} + +GltfAsset::GltfAsset() + : generator_("draco_decoder"), + version_("2.0"), + scene_index_(-1), + buffer_name_("buffer0.bin"), + draco_compression_used_(false), + mesh_features_used_(false), + mesh_features_texture_index_(0), + add_images_to_buffer_(false), + output_type_(GltfEncoder::COMPACT) {} + +bool GltfAsset::AddDracoMesh(const Mesh &mesh) { + const int scene_index = AddScene(); + if (scene_index < 0) { + return false; + } + if (!AddMaterials(mesh)) { + return false; + } + + GltfMesh gltf_mesh; + meshes_.push_back(gltf_mesh); + + AddStructuralMetadata(mesh); + + const int32_t material_att_id = + mesh.GetNamedAttributeId(GeometryAttribute::MATERIAL); + if (material_att_id == -1) { + if (!AddDracoMesh(mesh, 0, {}, Eigen::Matrix4d::Identity())) { + return false; + } + } else { + const auto mat_att = mesh.GetNamedAttribute(GeometryAttribute::MATERIAL); + + // Split mesh using the material attribute. + MeshSplitter splitter; + auto split_maybe = splitter.SplitMesh(mesh, material_att_id); + if (!split_maybe.ok()) { + return false; + } + auto split_meshes = std::move(split_maybe).value(); + for (int i = 0; i < split_meshes.size(); ++i) { + if (split_meshes[i] == nullptr) { + continue; // Empty mesh. Ignore. + } + uint32_t mat_index = 0; + mat_att->GetValue(AttributeValueIndex(i), &mat_index); + + // Copy over mesh features for a given material index. + Mesh::CopyMeshFeaturesForMaterial(mesh, split_meshes[i].get(), mat_index); + + // Move the split mesh to a temporary storage of the GltfAsset. This will + // ensure the mesh will stay alive as long the asset needs it. We have to + // do this because the split mesh may contain mesh features data that are + // used later in the encoding process. + local_meshes_.push_back(std::move(split_meshes[i])); + + // The material index in the glTF file corresponds to the index of the + // split mesh. + if (!AddDracoMesh(*(local_meshes_.back().get()), mat_index, {}, + Eigen::Matrix4d::Identity())) { + return false; + } + } + } + + // Currently output only one mesh. + GltfNode mesh_node; + mesh_node.mesh_index = 0; + nodes_.push_back(mesh_node); + nodes_.back().root_node = true; + return true; +} + +int GltfAsset::AddScene() { + GltfScene scene; + scenes_.push_back(scene); + const int scene_index = static_cast(scenes_.size()) - 1; + + if (scene_index_ == -1) { + scene_index_ = scene_index; + } + return scene_index; +} + +Status GltfAsset::Output(EncoderBuffer *buf_out) { + gltf_json_.BeginObject(); + if (!EncodeAssetProperty(buf_out)) { + return Status(Status::DRACO_ERROR, "Failed encoding asset."); + } + if (!EncodeScenesProperty(buf_out)) { + return Status(Status::DRACO_ERROR, "Failed encoding scenes."); + } + if (!EncodeInitialSceneProperty(buf_out)) { + return Status(Status::DRACO_ERROR, "Failed encoding initial scene."); + } + if (!EncodeNodesProperty(buf_out)) { + return Status(Status::DRACO_ERROR, "Failed encoding nodes."); + } + DRACO_RETURN_IF_ERROR(EncodeMeshesProperty(buf_out)); + DRACO_RETURN_IF_ERROR(EncodeMaterials(buf_out)); + if (!EncodeAccessorsProperty(buf_out)) { + return Status(Status::DRACO_ERROR, "Failed encoding accessors."); + } + DRACO_RETURN_IF_ERROR(EncodeAnimationsProperty(buf_out)); + DRACO_RETURN_IF_ERROR(EncodeSkinsProperty(buf_out)); + DRACO_RETURN_IF_ERROR(EncodeTopLevelExtensionsProperty(buf_out)); + if (!EncodeBufferViewsProperty(buf_out)) { + return Status(Status::DRACO_ERROR, "Failed encoding buffer views."); + } + if (!EncodeBuffersProperty(buf_out)) { + return Status(Status::DRACO_ERROR, "Failed encoding buffers."); + } + DRACO_RETURN_IF_ERROR(EncodeExtensionsProperties(buf_out)); + gltf_json_.EndObject(); + + const std::string asset_str = gltf_json_.MoveData(); + if (!buf_out->Encode(asset_str.data(), asset_str.length())) { + return Status(Status::DRACO_ERROR, "Failed encoding json data."); + } + if (!buf_out->Encode("\n", 1)) { + return Status(Status::DRACO_ERROR, "Failed encoding json data."); + } + return OkStatus(); +} + +const GltfImage *GltfAsset::GetImage(int index) const { + if (index < 0 || index >= images_.size()) { + return nullptr; + } + return &images_[index]; +} + +bool GltfAsset::PadBuffer() { + if (buffer_.size() % 4 != 0) { + const int pad_bytes = 4 - buffer_.size() % 4; + const int pad_data = 0; + if (!buffer_.Encode(&pad_data, pad_bytes)) { + return false; + } + } + return true; +} + +void GltfAsset::AddAttributeToDracoExtension( + const Mesh &mesh, GeometryAttribute::Type type, int index, + const std::string &name, GltfDracoCompressedMesh *compressed_mesh_info) { + if (mesh.IsCompressionEnabled()) { + const PointAttribute *const att = mesh.GetNamedAttribute(type, index); + if (att) { + compressed_mesh_info->attributes.insert( + std::pair(name, att->unique_id())); + } + } +} + +Status GltfAsset::CompressMeshWithDraco(const Mesh &mesh, + const Eigen::Matrix4d &transform, + GltfPrimitive *primitive, + int64_t *num_encoded_points, + int64_t *num_encoded_faces) { + // Check that geometry comression options are valid. + DracoCompressionOptions compression_options = mesh.GetCompressionOptions(); + DRACO_RETURN_IF_ERROR(compression_options.Check()); + + // Make a copy of the mesh. It will be modified and compressed. + std::unique_ptr mesh_copy(new Mesh()); + mesh_copy->Copy(mesh); + + // Delete auto-generated tangents. + if (MeshUtils::HasAutoGeneratedTangents(*mesh_copy)) { + for (int i = 0; i < mesh_copy->num_attributes(); ++i) { + PointAttribute *const att = mesh_copy->attribute(i); + if (att->attribute_type() == GeometryAttribute::TANGENT) { + while (mesh_copy->GetNamedAttribute(GeometryAttribute::TANGENT)) { + mesh_copy->DeleteAttribute( + mesh_copy->GetNamedAttributeId(GeometryAttribute::TANGENT)); + } + break; + } + } + } + + // Create Draco encoder. + EncoderBuffer buffer; + ExpertEncoder encoder(*mesh_copy); + encoder.SetTrackEncodedProperties(true); + + // Convert compression level to speed (that 0 = slowest, 10 = fastest). + const int speed = 10 - compression_options.compression_level; + encoder.SetSpeedOptions(speed, speed); + + // Configure attribute quantization. + for (int i = 0; i < mesh_copy->num_attributes(); ++i) { + const PointAttribute *const att = mesh_copy->attribute(i); + if (att->attribute_type() == GeometryAttribute::POSITION && + !compression_options.quantization_position + .AreQuantizationBitsDefined()) { + // Desired spacing in the "global" coordinate system. + const float global_spacing = + compression_options.quantization_position.spacing(); + + // Note: Ideally we would transform the whole mesh before encoding and + // apply the original global spacing on the transformed mesh. But neither + // KHR_draco_mesh_compression, nor Draco bitstream support post-decoding + // transformations so we have to modify the grid settings here. + + // Transform this spacing to the local coordinate system of the base mesh. + // We will get the largest scale factor from the transformation matrix and + // use it to adjust the grid spacing. + const Vector3f scale_vec(transform.col(0).norm(), transform.col(1).norm(), + transform.col(2).norm()); + + const float max_scale = scale_vec.MaxCoeff(); + + // Spacing is inverse to the scale. The larger the scale, the smaller the + // spacing must be. + const float local_spacing = global_spacing / max_scale; + + // Update the compression options of the processed mesh. + compression_options.quantization_position.SetGrid(local_spacing); + } else { + int num_quantization_bits = -1; + switch (att->attribute_type()) { + case GeometryAttribute::POSITION: + num_quantization_bits = + compression_options.quantization_position.quantization_bits(); + break; + case GeometryAttribute::NORMAL: + num_quantization_bits = compression_options.quantization_bits_normal; + break; + case GeometryAttribute::TEX_COORD: + num_quantization_bits = + compression_options.quantization_bits_tex_coord; + break; + case GeometryAttribute::TANGENT: + num_quantization_bits = compression_options.quantization_bits_tangent; + break; + case GeometryAttribute::WEIGHTS: + num_quantization_bits = compression_options.quantization_bits_weight; + break; + case GeometryAttribute::GENERIC: + if (GetFeatureIdAttributeName(*att, *mesh_copy).empty()) { + num_quantization_bits = + compression_options.quantization_bits_generic; + } else { + // Quantization is explicitly disabled for feature ID attributes. + encoder.SetAttributeQuantization(i, -1); + } + break; + default: + break; + } + if (num_quantization_bits > 0) { + encoder.SetAttributeQuantization(i, num_quantization_bits); + } + } + } + + // Flip UV values as required by glTF Draco and non-Draco files. + for (int i = 0; i < mesh_copy->num_attributes(); ++i) { + PointAttribute *const att = mesh_copy->attribute(i); + if (att->attribute_type() == GeometryAttribute::TEX_COORD) { + if (!MeshUtils::FlipTextureUvValues(false, true, att)) { + return Status(Status::DRACO_ERROR, "Could not flip texture UV values."); + } + } + } + + // Change tangents, joints, and weights attribute types to generic. The + // original mesh's attribute type is unchanged and the mapping of the glTF + // attribute type to Draco compressed attribute id is written to the output + // glTF file. + for (int i = 0; i < mesh_copy->num_attributes(); ++i) { + PointAttribute *const att = mesh_copy->attribute(i); + if (att->attribute_type() == GeometryAttribute::TANGENT || + att->attribute_type() == GeometryAttribute::JOINTS || + att->attribute_type() == GeometryAttribute::WEIGHTS) { + att->set_attribute_type(GeometryAttribute::GENERIC); + } + } + + // |compression_options| may have been modified and we need to update them + // before we start the encoding. + mesh_copy->SetCompressionOptions(compression_options); + DRACO_RETURN_IF_ERROR(encoder.EncodeToBuffer(&buffer)); + *num_encoded_points = encoder.num_encoded_points(); + *num_encoded_faces = encoder.num_encoded_faces(); + const size_t buffer_start_offset = buffer_.size(); + if (!buffer_.Encode(buffer.data(), buffer.size())) { + return Status(Status::DRACO_ERROR, "Could not copy Draco compressed data."); + } + if (!PadBuffer()) { + return Status(Status::DRACO_ERROR, "Could not pad glTF buffer."); + } + + GltfBufferView buffer_view; + buffer_view.buffer_byte_offset = buffer_start_offset; + buffer_view.byte_length = buffer_.size() - buffer_start_offset; + buffer_views_.push_back(buffer_view); + primitive->compressed_mesh_info.buffer_view_index = + static_cast(buffer_views_.size() - 1); + return OkStatus(); +} + +bool CheckAndGetTexCoordAttributeOrder(const Mesh &mesh, + std::vector *tex_coord_order) { + // We will only consider at most two texture coordinate attributes. + *tex_coord_order = {0, 1}; + const int num_attributes = + std::min(mesh.NumNamedAttributes(GeometryAttribute::TEX_COORD), 2); + + // Collect texture coordinate attribute names from metadata. + std::vector names(num_attributes, ""); + for (int i = 0; i < num_attributes; i++) { + const auto metadata = mesh.GetAttributeMetadataByAttributeId( + mesh.GetNamedAttributeId(GeometryAttribute::TEX_COORD, i)); + std::string attribute_name; + if (metadata != nullptr) { + metadata->GetEntryString("attribute_name", &attribute_name); + names[i] = attribute_name; + } + } + + // Attribute names may be absent. + if (num_attributes == 0 || + std::all_of(names.begin(), names.end(), + [](const std::string &name) { return name.empty(); })) { + return true; + } + + // Attribute names must be unique. + const std::unordered_set unique_names(names.begin(), + names.end()); + if (unique_names.size() != num_attributes) { + return false; + } + + // Attribute names must be valid. + if (std::any_of(names.begin(), names.end(), [](const std::string &name) { + return name != "TEXCOORD_0" && name != "TEXCOORD_1"; + })) { + return false; + } + + // Populate texture coordinate order index based on attribute names. + if (names[0] == "TEXCOORD_1") { + *tex_coord_order = {1, 0}; + } + return true; +} + +bool GltfAsset::AddDracoMesh( + const Mesh &mesh, int material_id, + const std::vector + &material_variants_mappings, + const Eigen::Matrix4d &transform) { + GltfPrimitive primitive; + int64_t num_encoded_points = mesh.num_points(); + int64_t num_encoded_faces = mesh.num_faces(); + if (num_encoded_faces > 0 && mesh.IsCompressionEnabled()) { + const Status status = CompressMeshWithDraco( + mesh, transform, &primitive, &num_encoded_points, &num_encoded_faces); + if (!status.ok()) { + return false; + } + draco_compression_used_ = true; + } + int indices_index = -1; + if (num_encoded_faces > 0) { + indices_index = AddDracoIndices(mesh, num_encoded_faces); + if (indices_index < 0) { + return false; + } + } + const int position_index = AddDracoPositions(mesh, num_encoded_points); + if (position_index < 0) { + return false; + } + // Check texture coordinate attributes and get the desired encoding order. + std::vector tex_coord_order; + if (!CheckAndGetTexCoordAttributeOrder(mesh, &tex_coord_order)) { + return false; + } + const int normals_accessor_index = AddDracoNormals(mesh, num_encoded_points); + const int colors_accessor_index = AddDracoColors(mesh, num_encoded_points); + const int texture0_accessor_index = + AddDracoTexture(mesh, tex_coord_order[0], num_encoded_points); + const int texture1_accessor_index = + AddDracoTexture(mesh, tex_coord_order[1], num_encoded_points); + const int tangent_accessor_index = AddDracoTangents(mesh, num_encoded_points); + const int joints_accessor_index = AddDracoJoints(mesh, num_encoded_points); + const int weights_accessor_index = AddDracoWeights(mesh, num_encoded_points); + const std::vector> generics_accessors = + AddDracoGenerics(mesh, num_encoded_points); + + if (num_encoded_faces == 0) { + primitive.mode = 0; // POINTS mode. + } + primitive.material = material_id; + primitive.material_variants_mappings = material_variants_mappings; + primitive.mesh_features.reserve(mesh.NumMeshFeatures()); + for (MeshFeaturesIndex i(0); i < mesh.NumMeshFeatures(); ++i) { + primitive.mesh_features.push_back(&mesh.GetMeshFeatures(i)); + } + primitive.indices = indices_index; + primitive.attributes.insert( + std::pair("POSITION", position_index)); + AddAttributeToDracoExtension(mesh, GeometryAttribute::POSITION, 0, "POSITION", + &primitive.compressed_mesh_info); + if (normals_accessor_index > 0) { + primitive.attributes.insert( + std::pair("NORMAL", normals_accessor_index)); + AddAttributeToDracoExtension(mesh, GeometryAttribute::NORMAL, 0, "NORMAL", + &primitive.compressed_mesh_info); + } + if (colors_accessor_index > 0) { + primitive.attributes.insert( + std::pair("COLOR_0", colors_accessor_index)); + AddAttributeToDracoExtension(mesh, GeometryAttribute::COLOR, 0, "COLOR_0", + &primitive.compressed_mesh_info); + } + if (texture0_accessor_index > 0) { + primitive.attributes.insert( + std::pair("TEXCOORD_0", texture0_accessor_index)); + AddAttributeToDracoExtension(mesh, GeometryAttribute::TEX_COORD, 0, + "TEXCOORD_0", &primitive.compressed_mesh_info); + } + if (texture1_accessor_index > 0) { + primitive.attributes.insert( + std::pair("TEXCOORD_1", texture1_accessor_index)); + AddAttributeToDracoExtension(mesh, GeometryAttribute::TEX_COORD, 1, + "TEXCOORD_1", &primitive.compressed_mesh_info); + } + if (tangent_accessor_index > 0) { + primitive.attributes.insert( + std::pair("TANGENT", tangent_accessor_index)); + AddAttributeToDracoExtension(mesh, GeometryAttribute::TANGENT, 0, "TANGENT", + &primitive.compressed_mesh_info); + } + if (joints_accessor_index > 0) { + primitive.attributes.insert( + std::pair("JOINTS_0", joints_accessor_index)); + AddAttributeToDracoExtension(mesh, GeometryAttribute::JOINTS, 0, "JOINTS_0", + &primitive.compressed_mesh_info); + } + if (weights_accessor_index > 0) { + primitive.attributes.insert( + std::pair("WEIGHTS_0", weights_accessor_index)); + AddAttributeToDracoExtension(mesh, GeometryAttribute::WEIGHTS, 0, + "WEIGHTS_0", &primitive.compressed_mesh_info); + } + for (int att_index = 0; att_index < generics_accessors.size(); ++att_index) { + const std::string &attribute_name = generics_accessors[att_index].first; + if (!attribute_name.empty()) { + primitive.attributes.insert(generics_accessors[att_index]); + AddAttributeToDracoExtension(mesh, GeometryAttribute::GENERIC, att_index, + attribute_name, + &primitive.compressed_mesh_info); + } + } + + meshes_.back().primitives.push_back(primitive); + return true; +} + +int GltfAsset::AddDracoIndices(const Mesh &mesh, int64_t num_encoded_faces) { + // Get the min and max value for the indices. + uint32_t min_index = 0xffffffff; + uint32_t max_index = 0; + for (FaceIndex i(0); i < mesh.num_faces(); ++i) { + const auto &f = mesh.face(i); + + for (int j = 0; j < 3; ++j) { + if (f[j] < min_index) { + min_index = f[j].value(); + } + if (f[j] > max_index) { + max_index = f[j].value(); + } + } + } + + const int component_size = GltfAsset::UnsignedIntComponentSize(max_index); + + GltfAccessor accessor; + if (!mesh.IsCompressionEnabled()) { + const size_t buffer_start_offset = buffer_.size(); + for (FaceIndex i(0); i < mesh.num_faces(); ++i) { + const auto &f = mesh.face(i); + for (int j = 0; j < 3; ++j) { + int index = f[j].value(); + if (!buffer_.Encode(&index, component_size)) { + return -1; + } + } + } + + if (!PadBuffer()) { + return -1; + } + + GltfBufferView buffer_view; + buffer_view.buffer_byte_offset = buffer_start_offset; + buffer_view.byte_length = buffer_.size() - buffer_start_offset; + buffer_views_.push_back(buffer_view); + accessor.buffer_view_index = static_cast(buffer_views_.size() - 1); + } + + accessor.component_type = UnsignedIntComponentType(max_index); + accessor.count = num_encoded_faces * 3; + if (output_type_ == GltfEncoder::VERBOSE) { + accessor.max.push_back(GltfValue(max_index)); + accessor.min.push_back(GltfValue(min_index)); + } + accessor.type = "SCALAR"; + accessors_.push_back(accessor); + return static_cast(accessors_.size() - 1); +} + +int GltfAsset::AddDracoPositions(const Mesh &mesh, int num_encoded_points) { + const PointAttribute *const att = + mesh.GetNamedAttribute(GeometryAttribute::POSITION); + if (!CheckDracoAttribute(att, {DT_FLOAT32}, {3})) { + return -1; + } + return AddAttribute(*att, mesh.num_points(), num_encoded_points, + mesh.IsCompressionEnabled()); +} + +int GltfAsset::AddDracoNormals(const Mesh &mesh, int num_encoded_points) { + const PointAttribute *const att = + mesh.GetNamedAttribute(GeometryAttribute::NORMAL); + if (!CheckDracoAttribute(att, {DT_FLOAT32}, {3})) { + return -1; + } + return AddAttribute(*att, mesh.num_points(), num_encoded_points, + mesh.IsCompressionEnabled()); +} + +int GltfAsset::AddDracoColors(const Mesh &mesh, int num_encoded_points) { + const PointAttribute *const att = + mesh.GetNamedAttribute(GeometryAttribute::COLOR); + // TODO(b/200302561): Add support for DT_UINT16 with COLOR. + if (!CheckDracoAttribute(att, {DT_UINT8, DT_FLOAT32}, {3, 4})) { + return -1; + } + if (att->data_type() == DT_FLOAT32) { + return AddAttribute(*att, mesh.num_points(), num_encoded_points, + mesh.IsCompressionEnabled()); + } + return AddAttribute(*att, mesh.num_points(), num_encoded_points, + mesh.IsCompressionEnabled()); +} + +int GltfAsset::AddDracoTexture(const Mesh &mesh, int tex_coord_index, + int num_encoded_points) { + const PointAttribute *const att = + mesh.GetNamedAttribute(GeometryAttribute::TEX_COORD, tex_coord_index); + // TODO(b/200303080): Add support for DT_UINT8 and DT_UINT16 with TEX_COORD. + if (!CheckDracoAttribute(att, {DT_FLOAT32}, {2})) { + return -1; + } + + // glTF stores texture coordinates flipped on the horizontal axis compared to + // how Draco stores texture coordinates. + GeometryAttribute ga; + ga.Init(GeometryAttribute::TEX_COORD, nullptr, 2, att->data_type(), false, + DataTypeLength(att->data_type()) * 2, 0); + PointAttribute ta(ga); + ta.SetIdentityMapping(); + ta.Reset(mesh.num_points()); + + std::array value; + for (PointIndex v(0); v < mesh.num_points(); ++v) { + if (!att->GetValue(att->mapped_index(v), &value)) { + return -1; + } + + // Draco texture v component needs to be flipped. + Vector2f texture_coord(value[0], 1.0 - value[1]); + ta.SetAttributeValue(AttributeValueIndex(v.value()), texture_coord.data()); + } + return AddAttribute(ta, mesh.num_points(), num_encoded_points, + mesh.IsCompressionEnabled()); +} + +int GltfAsset::AddDracoTangents(const Mesh &mesh, int num_encoded_points) { + const PointAttribute *const att = + mesh.GetNamedAttribute(GeometryAttribute::TANGENT); + if (!CheckDracoAttribute(att, {DT_FLOAT32}, {3, 4})) { + return -1; + } + if (MeshUtils::HasAutoGeneratedTangents(mesh)) { + // Ignore auto-generated tangents. See go/tangents_and_draco_simplifier. + return -1; + } + + if (att->num_components() == 4) { + return AddAttribute(*att, mesh.num_points(), num_encoded_points, + mesh.IsCompressionEnabled()); + } + + // glTF mesh needs the w component. + GeometryAttribute ga; + ga.Init(GeometryAttribute::TANGENT, nullptr, 4, DT_FLOAT32, false, + DataTypeLength(DT_FLOAT32) * 4, 0); + PointAttribute ta(ga); + ta.SetIdentityMapping(); + ta.Reset(mesh.num_points()); + + std::array value; + for (PointIndex v(0); v < mesh.num_points(); ++v) { + if (!att->GetValue(att->mapped_index(v), &value)) { + return -1; + } + + // Draco tangent w component is always 1.0. + Vector4f tangent(value[0], value[1], value[2], 1.0); + ta.SetAttributeValue(AttributeValueIndex(v.value()), tangent.data()); + } + return AddAttribute(ta, mesh.num_points(), num_encoded_points, + mesh.IsCompressionEnabled()); +} + +int GltfAsset::AddDracoJoints(const Mesh &mesh, int num_encoded_points) { + const PointAttribute *const att = + mesh.GetNamedAttribute(GeometryAttribute::JOINTS); + if (!CheckDracoAttribute(att, {DT_UINT8, DT_UINT16}, {4})) { + return -1; + } + if (att->data_type() == DT_UINT16) { + return AddAttribute(*att, mesh.num_points(), num_encoded_points, + mesh.IsCompressionEnabled()); + } + return AddAttribute(*att, mesh.num_points(), num_encoded_points, + mesh.IsCompressionEnabled()); +} + +int GltfAsset::AddDracoWeights(const Mesh &mesh, int num_encoded_points) { + const PointAttribute *const att = + mesh.GetNamedAttribute(GeometryAttribute::WEIGHTS); + // TODO(b/200303026): Add support for DT_UINT8 and DT_UINT16 with WEIGHTS. + if (!CheckDracoAttribute(att, {DT_FLOAT32}, {4})) { + return -1; + } + return AddAttribute(*att, mesh.num_points(), num_encoded_points, + mesh.IsCompressionEnabled()); +} + +// Adds generic attributes that have metadata describing the attribute name. +// This allows for export of application-specific attributes and feature ID +// attributes defined in glTF extension EXT_mesh_features. Returns a vector of +// attribute-name, accessor pairs for each valid attribute. The length of the +// vector is equal to the number of generic attributes. Vector entries +// corresponding to unsupported attributes (e.g., with no metadata) contain +// empty attribute names. +std::vector> GltfAsset::AddDracoGenerics( + const Mesh &mesh, int num_encoded_points) { + const int num_attributes = + mesh.NumNamedAttributes(GeometryAttribute::GENERIC); + std::vector> attrs(num_attributes); + for (int i = 0; i < num_attributes; ++i) { + const PointAttribute *const att = + mesh.GetNamedAttribute(GeometryAttribute::GENERIC, i); + auto const *metadata = + mesh.GetAttributeMetadataByAttributeId(att->unique_id()); + if (metadata) { + std::string attr_name; + if (metadata->GetEntryString(GltfEncoder::kDracoMetadataGltfAttributeName, + &attr_name)) { + if (att->data_type() == DT_FLOAT32) { + int accessor = + AddAttribute(*att, mesh.num_points(), num_encoded_points, + mesh.IsCompressionEnabled()); + attrs[i] = {attr_name, accessor}; + } + } else { + // Try to find feature ID attribute name like "_FEATURE_ID_5" then check + // that the attribute stores scalar values of complient data types as + // defined by the EXT_mesh_features glTF extension. + attr_name = GetFeatureIdAttributeName(*att, mesh); + if (!attr_name.empty() && att->num_components() == 1) { + int accessor = -1; + switch (att->data_type()) { + case DT_UINT8: + accessor = AddAttribute(*att, mesh.num_points(), + num_encoded_points, + mesh.IsCompressionEnabled()); + break; + case DT_UINT16: + accessor = AddAttribute(*att, mesh.num_points(), + num_encoded_points, + mesh.IsCompressionEnabled()); + break; + case DT_FLOAT32: + accessor = AddAttribute(*att, mesh.num_points(), + num_encoded_points, + mesh.IsCompressionEnabled()); + break; + default: + continue; + } + attrs[i] = {attr_name, accessor}; + } + } + } + } + return attrs; +} + +bool GltfAsset::AddMaterials(const Mesh &mesh) { + if (mesh.GetMaterialLibrary().NumMaterials() == 0) { + return true; + } + material_library_.Copy(mesh.GetMaterialLibrary()); + return true; +} + +bool GltfAsset::CheckDracoAttribute(const PointAttribute *attribute, + const std::set &data_types, + const std::set &num_components) { + // Attribute must be valid. + if (attribute == nullptr || attribute->size() == 0) { + return false; + } + + // Attribute must have an expected data type. + if (data_types.find(attribute->data_type()) == data_types.end()) { + return false; + } + + // Attribute must have an expected number of components. + if (num_components.find(attribute->num_components()) == + num_components.end()) { + return false; + } + + return true; +} + +StatusOr GltfAsset::AddImage(const std::string &image_stem, + const Texture *texture, int num_components) { + return AddImage(image_stem, texture, nullptr, num_components); +} + +StatusOr GltfAsset::AddImage(const std::string &image_stem, + const Texture *texture, + std::unique_ptr owned_texture, + int num_components) { + const auto it = texture_to_image_index_map_.find(texture); + if (it != texture_to_image_index_map_.end()) { + // We already have an image for the given |texture|. Update its number of + // components if needed. + GltfImage &image = images_[it->second]; + if (image.num_components < num_components) { + image.num_components = num_components; + } + return it->second; + } + std::string extension = TextureUtils::GetTargetExtension(*texture); + if (extension.empty()) { + // Try to get extension from the source file name. + extension = LowercaseFileExtension(texture->source_image().filename()); + } + GltfImage image; + image.image_name = image_stem + "." + extension; + image.texture = texture; + image.owned_texture = std::move(owned_texture); + image.num_components = num_components; + + // Always maintain the mime_type. Used elsewhere to determine image type. + if (extension == "jpg") { + image.mime_type = "image/jpeg"; + } else { + image.mime_type = "image/" + extension; + } + + // For KTX2 with Basis compression, state that its extension is required. + if (extension == "ktx2") { + extensions_used_.insert("KHR_texture_basisu"); + extensions_required_.insert("KHR_texture_basisu"); + } + + // If this is webp, state that its extension is required. + if (extension == "webp") { + extensions_used_.insert("EXT_texture_webp"); + extensions_required_.insert("EXT_texture_webp"); + } + + images_.push_back(std::move(image)); + texture_to_image_index_map_[texture] = images_.size() - 1; + return images_.size() - 1; +} + +Status GltfAsset::SaveImageToBuffer(int image_index) { + GltfImage &image = images_[image_index]; + const Texture *const texture = image.texture; + const int num_components = image.num_components; + std::vector buffer; + DRACO_RETURN_IF_ERROR(WriteTextureToBuffer(*texture, &buffer)); + + // Add the image data to the buffer. + const size_t buffer_start_offset = buffer_.size(); + buffer_.Encode(buffer.data(), buffer.size()); + if (!PadBuffer()) { + return Status(Status::DRACO_ERROR, + "Could not pad buffer in SaveImageToBuffer."); + } + + // Add a buffer view pointing to the image data in the buffer. + GltfBufferView buffer_view; + buffer_view.buffer_byte_offset = buffer_start_offset; + buffer_view.byte_length = buffer_.size() - buffer_start_offset; + buffer_views_.push_back(buffer_view); + + image.buffer_view = buffer_views_.size() - 1; + return OkStatus(); +} + +// TODO(vytyaz): The return type could be int. +StatusOr GltfAsset::AddTextureSampler(const TextureSampler &sampler) { + // If sampler is equal to defaults do not add to vector and return -1. + if (sampler.min_filter == TextureMap::UNSPECIFIED && + sampler.mag_filter == TextureMap::UNSPECIFIED && + sampler.wrapping_mode.s == TextureMap::REPEAT && + sampler.wrapping_mode.t == TextureMap::REPEAT) { + return -1; + } + + const auto &it = + std::find(texture_samplers_.begin(), texture_samplers_.end(), sampler); + if (it != texture_samplers_.end()) { + const int index = std::distance(texture_samplers_.begin(), it); + return index; + } + + texture_samplers_.push_back(sampler); + return texture_samplers_.size() - 1; +} + +Status GltfAsset::AddScene(const Scene &scene) { + const int scene_index = AddScene(); + if (scene_index < 0) { + return Status(Status::DRACO_ERROR, "Error creating a new scene."); + } + if (!AddMaterials(scene)) { + return Status(Status::DRACO_ERROR, "Error adding materials to the scene."); + } + // Initialize base mesh transforms that may be needed when the base meshes are + // compressed with Draco. + base_mesh_transforms_ = SceneUtils::FindLargestBaseMeshTransforms(scene); + for (SceneNodeIndex i(0); i < scene.NumNodes(); ++i) { + DRACO_RETURN_IF_ERROR(AddSceneNode(scene, i)); + } + // There is 1:1 mapping between draco::Scene node indices and |nodes_|. + for (int i = 0; i < scene.NumRootNodes(); ++i) { + nodes_[scene.GetRootNodeIndex(i).value()].root_node = true; + } + DRACO_RETURN_IF_ERROR(AddAnimations(scene)); + DRACO_RETURN_IF_ERROR(AddSkins(scene)); + DRACO_RETURN_IF_ERROR(AddLights(scene)); + DRACO_RETURN_IF_ERROR(AddMaterialsVariantsNames(scene)); + DRACO_RETURN_IF_ERROR(AddInstanceArrays(scene)); + AddStructuralMetadata(scene); + return OkStatus(); +} + +Status GltfAsset::AddSceneNode(const Scene &scene, + SceneNodeIndex scene_node_index) { + const SceneNode *const scene_node = scene.GetNode(scene_node_index); + if (scene_node == nullptr) { + return Status(Status::DRACO_ERROR, "Could not find node in scene."); + } + + GltfNode node; + node.name = scene_node->GetName(); + node.trs_matrix.Copy(scene_node->GetTrsMatrix()); + + for (int i = 0; i < scene_node->NumChildren(); ++i) { + node.childern_indices.push_back(scene_node->Child(i).value()); + } + + const MeshGroupIndex mesh_group_index = scene_node->GetMeshGroupIndex(); + if (mesh_group_index != kInvalidMeshGroupIndex) { + const auto it = mesh_group_index_to_gltf_mesh_.find(mesh_group_index); + if (it == mesh_group_index_to_gltf_mesh_.end()) { + GltfMesh gltf_mesh; + const MeshGroup *const mesh_group = scene.GetMeshGroup(mesh_group_index); + if (!mesh_group->GetName().empty()) { + gltf_mesh.name = mesh_group->GetName(); + } + meshes_.push_back(gltf_mesh); + + for (int i = 0; i < mesh_group->NumMeshInstances(); ++i) { + const MeshGroup::MeshInstance &instance = + mesh_group->GetMeshInstance(i); + const auto mi_it = + mesh_index_to_gltf_mesh_primitive_.find(instance.mesh_index); + if (mi_it == mesh_index_to_gltf_mesh_primitive_.end()) { + // We have not added the mesh to the scene yet. + const Mesh &mesh = scene.GetMesh(instance.mesh_index); + if (!AddDracoMesh(mesh, instance.material_index, + instance.materials_variants_mappings, + base_mesh_transforms_[instance.mesh_index])) { + return Status(Status::DRACO_ERROR, "Adding a Draco mesh failed."); + } + const int gltf_mesh_index = meshes_.size() - 1; + const int gltf_primitive_index = meshes_.back().primitives.size() - 1; + mesh_index_to_gltf_mesh_primitive_[instance.mesh_index] = + std::make_pair(gltf_mesh_index, gltf_primitive_index); + } else { + // The mesh was already added to the scene. This is a copy instance + // that may have a different material. + const int gltf_mesh_index = mi_it->second.first; + const int gltf_primitive_index = mi_it->second.second; + GltfPrimitive primitive = + meshes_[gltf_mesh_index].primitives[gltf_primitive_index]; + primitive.material = instance.material_index; + primitive.material_variants_mappings = + instance.materials_variants_mappings; + const Mesh &mesh = scene.GetMesh(instance.mesh_index); + primitive.mesh_features.clear(); + primitive.mesh_features.reserve(mesh.NumMeshFeatures()); + for (MeshFeaturesIndex j(0); j < mesh.NumMeshFeatures(); ++j) { + primitive.mesh_features.push_back(&mesh.GetMeshFeatures(j)); + } + meshes_.back().primitives.push_back(primitive); + } + } + mesh_group_index_to_gltf_mesh_[mesh_group_index] = meshes_.size() - 1; + } + node.mesh_index = mesh_group_index_to_gltf_mesh_[mesh_group_index]; + } + node.skin_index = scene_node->GetSkinIndex().value(); + node.light_index = scene_node->GetLightIndex().value(); + node.instance_array_index = scene_node->GetInstanceArrayIndex().value(); + + nodes_.push_back(node); + return OkStatus(); +} + +bool GltfAsset::AddMaterials(const Scene &scene) { + if (scene.GetMaterialLibrary().NumMaterials() == 0) { + return true; + } + material_library_.Copy(scene.GetMaterialLibrary()); + return true; +} + +Status GltfAsset::AddAnimations(const Scene &scene) { + if (scene.NumAnimations() == 0) { + return OkStatus(); + } + // Mapping of the node animation data to the output accessors. The first part + // of the key is the animation index and the second part of the key is the + // node animation data index. + std::map, int> node_animation_data_to_accessor; + + // Mapping of the node animation data to the output accessors. + std::unordered_map + data_to_index_map; + + // First add all the accessors and create a mapping from animation accessors + // to accessors owned by the encoder. + for (AnimationIndex i(0); i < scene.NumAnimations(); ++i) { + const Animation *const animation = scene.GetAnimation(i); + + for (int j = 0; j < animation->NumNodeAnimationData(); ++j) { + const NodeAnimationData *const node_animation_data = + animation->GetNodeAnimationData(j); + + int index = -1; + + NodeAnimationDataHash nadh(node_animation_data); + if (data_to_index_map.find(nadh) == data_to_index_map.end()) { + // The current data is new, add it to the encoder. + DRACO_ASSIGN_OR_RETURN( + index, AddNodeAnimationData(*nadh.GetNodeAnimationData())); + data_to_index_map[nadh] = index; + } else { + index = data_to_index_map[nadh]; + } + + const auto key = std::make_pair(i.value(), j); + node_animation_data_to_accessor[key] = index; + } + } + + // Add all the samplers and channels. + for (AnimationIndex i(0); i < scene.NumAnimations(); ++i) { + const Animation *const animation = scene.GetAnimation(i); + std::unique_ptr new_animation(new EncoderAnimation); + new_animation->name = animation->GetName(); + + for (int j = 0; j < animation->NumSamplers(); ++j) { + const AnimationSampler *const sampler = animation->GetSampler(j); + const auto input_key = std::make_pair(i.value(), sampler->input_index); + const auto input_it = node_animation_data_to_accessor.find(input_key); + if (input_it == node_animation_data_to_accessor.end()) { + return Status(Status::DRACO_ERROR, + "Could not find animation accessor input index."); + } + const auto output_key = std::make_pair(i.value(), sampler->output_index); + const auto output_it = node_animation_data_to_accessor.find(output_key); + if (output_it == node_animation_data_to_accessor.end()) { + return Status(Status::DRACO_ERROR, + "Could not find animation accessor output index."); + } + + std::unique_ptr new_sampler(new AnimationSampler()); + new_sampler->input_index = input_it->second; + new_sampler->output_index = output_it->second; + + if (output_type_ == GltfEncoder::COMPACT) { + // Remove min/max from output accessor. + accessors_[new_sampler->output_index].min.clear(); + accessors_[new_sampler->output_index].max.clear(); + } + + new_sampler->interpolation_type = sampler->interpolation_type; + + new_animation->samplers.push_back(std::move(new_sampler)); + } + + for (int j = 0; j < animation->NumChannels(); ++j) { + const AnimationChannel *const channel = animation->GetChannel(j); + std::unique_ptr new_channel(new AnimationChannel()); + new_channel->Copy(*channel); + new_animation->channels.push_back(std::move(new_channel)); + } + + animations_.push_back(std::move(new_animation)); + } + return OkStatus(); +} + +StatusOr GltfAsset::AddNodeAnimationData( + const NodeAnimationData &node_animation_data) { + const size_t buffer_start_offset = buffer_.size(); + + const int component_size = node_animation_data.ComponentSize(); + const int num_components = node_animation_data.NumComponents(); + const std::vector *data = node_animation_data.GetData(); + + std::vector min_values; + min_values.resize(num_components); + for (int j = 0; j < num_components; ++j) { + min_values[j] = (*data)[j]; + } + std::vector max_values = min_values; + + for (int i = 0; i < node_animation_data.count(); ++i) { + for (int j = 0; j < num_components; ++j) { + const float value = (*data)[(i * num_components) + j]; + if (value < min_values[j]) { + min_values[j] = value; + } + if (value > max_values[j]) { + max_values[j] = value; + } + + buffer_.Encode(&value, component_size); + } + } + + if (!PadBuffer()) { + return Status(Status::DRACO_ERROR, + "AddNodeAnimationData: PadBuffer returned DRACO_ERROR."); + } + + GltfBufferView buffer_view; + buffer_view.buffer_byte_offset = buffer_start_offset; + buffer_view.byte_length = buffer_.size() - buffer_start_offset; + buffer_views_.push_back(buffer_view); + + GltfAccessor accessor; + accessor.buffer_view_index = static_cast(buffer_views_.size() - 1); + accessor.component_type = ComponentType::FLOAT; + accessor.count = node_animation_data.count(); + for (int j = 0; j < num_components; ++j) { + accessor.max.push_back(GltfValue(max_values[j])); + accessor.min.push_back(GltfValue(min_values[j])); + } + accessor.type = node_animation_data.TypeAsString(); + accessor.normalized = node_animation_data.normalized(); + accessors_.push_back(accessor); + return static_cast(accessors_.size() - 1); +} + +Status GltfAsset::AddSkins(const Scene &scene) { + if (scene.NumSkins() == 0) { + return OkStatus(); + } + + for (SkinIndex i(0); i < scene.NumSkins(); ++i) { + const Skin *const skin = scene.GetSkin(i); + DRACO_ASSIGN_OR_RETURN( + const int output_accessor_index, + AddNodeAnimationData(skin->GetInverseBindMatrices())); + + std::unique_ptr encoder_skin(new EncoderSkin); + encoder_skin->inverse_bind_matrices_index = output_accessor_index; + encoder_skin->joints.reserve(skin->NumJoints()); + for (int j = 0; j < skin->NumJoints(); j++) { + encoder_skin->joints.push_back(skin->GetJoint(j).value()); + } + encoder_skin->skeleton_index = skin->GetJointRoot().value(); + skins_.push_back(std::move(encoder_skin)); + } + return OkStatus(); +} + +Status GltfAsset::AddLights(const Scene &scene) { + if (scene.NumLights() == 0) { + return OkStatus(); + } + + for (LightIndex i(0); i < scene.NumLights(); ++i) { + std::unique_ptr light = std::unique_ptr(new Light()); + light->Copy(*scene.GetLight(i)); + lights_.push_back(std::move(light)); + } + return OkStatus(); +} + +Status GltfAsset::AddMaterialsVariantsNames(const Scene &scene) { + const MaterialLibrary &library = scene.GetMaterialLibrary(); + for (int i = 0; i < library.NumMaterialsVariants(); ++i) { + materials_variants_names_.push_back(library.GetMaterialsVariantName(i)); + } + return OkStatus(); +} + +Status GltfAsset::AddInstanceArrays(const Scene &scene) { + if (scene.NumInstanceArrays() == 0) { + return OkStatus(); + } + + // Add each of the instance arrays. + std::vector t_data; + std::vector r_data; + std::vector s_data; + for (InstanceArrayIndex i(0); i < scene.NumInstanceArrays(); ++i) { + // Find which of the optional TRS components are set. + // TODO(vytyaz): Treat default TRS component vectors as absent. + const InstanceArray &array = *scene.GetInstanceArray(i); + bool is_t_set = false; + bool is_r_set = false; + bool is_s_set = false; + for (int i = 0; i < array.NumInstances(); i++) { + const InstanceArray::Instance &instance = array.GetInstance(i); + if (instance.trs.TranslationSet()) { + is_t_set = true; + } + if (instance.trs.RotationSet()) { + is_r_set = true; + } + if (instance.trs.ScaleSet()) { + is_s_set = true; + } + } + + // Create contiguous data vectors for individual TRS components. + t_data.clear(); + r_data.clear(); + s_data.clear(); + if (is_t_set) { + t_data.reserve(array.NumInstances() * 3); + } + if (is_r_set) { + r_data.reserve(array.NumInstances() * 4); + } + if (is_s_set) { + s_data.reserve(array.NumInstances() * 3); + } + + // Add TRS vectors of each instance to corresponding data vectors. + for (int i = 0; i < array.NumInstances(); i++) { + const InstanceArray::Instance &instance = array.GetInstance(i); + if (is_t_set) { + DRACO_ASSIGN_OR_RETURN(const auto &t_vector, + instance.trs.Translation()); + t_data.push_back(t_vector.x()); + t_data.push_back(t_vector.y()); + t_data.push_back(t_vector.z()); + } + if (is_r_set) { + DRACO_ASSIGN_OR_RETURN(const auto &r_vector, instance.trs.Rotation()); + r_data.push_back(r_vector.x()); + r_data.push_back(r_vector.y()); + r_data.push_back(r_vector.z()); + r_data.push_back(r_vector.w()); + } + if (is_s_set) { + DRACO_ASSIGN_OR_RETURN(const auto &s_vector, instance.trs.Scale()); + s_data.push_back(s_vector.x()); + s_data.push_back(s_vector.y()); + s_data.push_back(s_vector.z()); + } + } + + // Add TRS vectors to attribute buffers and collect their accessor indices. + EncoderInstanceArray accessors; + if (is_t_set) { + DRACO_ASSIGN_OR_RETURN(accessors.translation, AddData(t_data, 3)); + } + if (is_r_set) { + DRACO_ASSIGN_OR_RETURN(accessors.rotation, AddData(r_data, 4)); + } + if (is_s_set) { + DRACO_ASSIGN_OR_RETURN(accessors.scale, AddData(s_data, 3)); + } + + // Store accessors for later to encode as EXT_mesh_gpu_instancing extension. + instance_arrays_.push_back(accessors); + } + return OkStatus(); +} + +template +void GltfAsset::AddStructuralMetadata(const GeometryT &geometry) { + const StructuralMetadata &structural_metadata = + geometry.GetStructuralMetadata(); + if (!structural_metadata.GetPropertyTableSchema().Empty()) { + property_table_schema_ = structural_metadata.GetPropertyTableSchema(); + for (int i = 0; i < structural_metadata.NumPropertyTables(); ++i) { + property_tables_.push_back(&structural_metadata.GetPropertyTable(i)); + } + } +} + +StatusOr GltfAsset::AddData(const std::vector &data, + int num_components) { + std::string type; + switch (num_components) { + case 3: + type = "VEC3"; + break; + case 4: + type = "VEC4"; + break; + default: + return ErrorStatus("Unsupported number of components."); + } + + const size_t buffer_start_offset = buffer_.size(); + + std::vector min_values(num_components); + for (int j = 0; j < num_components; ++j) { + min_values[j] = data[j]; + } + std::vector max_values = min_values; + + const int count = data.size() / num_components; + for (int i = 0; i < count; ++i) { + for (int j = 0; j < num_components; ++j) { + const float value = data[(i * num_components) + j]; + if (value < min_values[j]) { + min_values[j] = value; + } + if (value > max_values[j]) { + max_values[j] = value; + } + buffer_.Encode(&value, sizeof(float)); + } + } + + if (!PadBuffer()) { + return ErrorStatus("AddArray: PadBuffer returned DRACO_ERROR."); + } + + GltfBufferView buffer_view; + buffer_view.buffer_byte_offset = buffer_start_offset; + buffer_view.byte_length = buffer_.size() - buffer_start_offset; + buffer_views_.push_back(buffer_view); + + GltfAccessor accessor; + accessor.buffer_view_index = static_cast(buffer_views_.size() - 1); + accessor.component_type = ComponentType::FLOAT; + accessor.count = count; + for (int j = 0; j < num_components; ++j) { + accessor.max.push_back(GltfValue(max_values[j])); + accessor.min.push_back(GltfValue(min_values[j])); + } + accessor.type = type; + accessor.normalized = false; + accessors_.push_back(accessor); + return static_cast(accessors_.size() - 1); +} + +StatusOr GltfAsset::AddBufferView( + const PropertyTable::Property::Data &data) { + const size_t buffer_start_offset = buffer_.size(); + buffer_.Encode(data.data.data(), data.data.size()); + if (!PadBuffer()) { + return ErrorStatus("AddBufferView: PadBuffer returned DRACO_ERROR."); + } + GltfBufferView buffer_view; + buffer_view.buffer_byte_offset = buffer_start_offset; + buffer_view.byte_length = buffer_.size() - buffer_start_offset; + buffer_view.target = data.target; + buffer_views_.push_back(buffer_view); + return static_cast(buffer_views_.size() - 1); +} + +bool GltfAsset::EncodeAssetProperty(EncoderBuffer *buf_out) { + gltf_json_.BeginObject("asset"); + gltf_json_.OutputValue("version", version_); + gltf_json_.OutputValue("generator", generator_); + gltf_json_.EndObject(); + + const std::string asset_str = gltf_json_.MoveData(); + return buf_out->Encode(asset_str.data(), asset_str.length()); +} + +bool GltfAsset::EncodeScenesProperty(EncoderBuffer *buf_out) { + // We currently only support one scene. + gltf_json_.BeginArray("scenes"); + gltf_json_.BeginObject(); + gltf_json_.BeginArray("nodes"); + + for (int i = 0; i < nodes_.size(); ++i) { + if (nodes_[i].root_node) { + gltf_json_.OutputValue(i); + } + } + gltf_json_.EndArray(); + gltf_json_.EndObject(); + gltf_json_.EndArray(); + + const std::string asset_str = gltf_json_.MoveData(); + return buf_out->Encode(asset_str.data(), asset_str.length()); +} + +bool GltfAsset::EncodeInitialSceneProperty(EncoderBuffer *buf_out) { + gltf_json_.OutputValue("scene", scene_index_); + const std::string asset_str = gltf_json_.MoveData(); + return buf_out->Encode(asset_str.data(), asset_str.length()); +} + +bool GltfAsset::EncodeNodesProperty(EncoderBuffer *buf_out) { + gltf_json_.BeginArray("nodes"); + + for (int i = 0; i < nodes_.size(); ++i) { + gltf_json_.BeginObject(); + if (!nodes_[i].name.empty()) { + gltf_json_.OutputValue("name", nodes_[i].name); + } + if (nodes_[i].mesh_index >= 0) { + gltf_json_.OutputValue("mesh", nodes_[i].mesh_index); + } + if (nodes_[i].skin_index >= 0) { + gltf_json_.OutputValue("skin", nodes_[i].skin_index); + } + if (nodes_[i].instance_array_index >= 0 || nodes_[i].light_index >= 0) { + gltf_json_.BeginObject("extensions"); + if (nodes_[i].instance_array_index >= 0) { + gltf_json_.BeginObject("EXT_mesh_gpu_instancing"); + gltf_json_.BeginObject("attributes"); + const int index = nodes_[i].instance_array_index; + const EncoderInstanceArray &accessors = instance_arrays_[index]; + if (accessors.translation != -1) { + gltf_json_.OutputValue("TRANSLATION", accessors.translation); + } + if (accessors.rotation != -1) { + gltf_json_.OutputValue("ROTATION", accessors.rotation); + } + if (accessors.scale != -1) { + gltf_json_.OutputValue("SCALE", accessors.scale); + } + gltf_json_.EndObject(); + gltf_json_.EndObject(); + } + if (nodes_[i].light_index >= 0) { + gltf_json_.BeginObject("KHR_lights_punctual"); + gltf_json_.OutputValue("light", nodes_[i].light_index); + gltf_json_.EndObject(); + } + gltf_json_.EndObject(); + } + + if (!nodes_[i].childern_indices.empty()) { + gltf_json_.BeginArray("children"); + for (int j = 0; j < nodes_[i].childern_indices.size(); ++j) { + gltf_json_.OutputValue(nodes_[i].childern_indices[j]); + } + gltf_json_.EndArray(); + } + + if (!nodes_[i].trs_matrix.IsMatrixIdentity()) { + const auto maybe_transformation = nodes_[i].trs_matrix.Matrix(); + const auto transformation = maybe_transformation.ValueOrDie(); + + if (nodes_[i].trs_matrix.IsMatrixTranslationOnly()) { + gltf_json_.BeginArray("translation"); + for (int j = 0; j < 3; ++j) { + gltf_json_.OutputValue(transformation(j, 3)); + } + gltf_json_.EndArray(); + } else { + gltf_json_.BeginArray("matrix"); + for (int j = 0; j < 4; ++j) { + for (int k = 0; k < 4; ++k) { + gltf_json_.OutputValue(transformation(k, j)); + } + } + gltf_json_.EndArray(); + } + } else { + if (nodes_[i].trs_matrix.TranslationSet()) { + const auto maybe_translation = nodes_[i].trs_matrix.Translation(); + const auto translation = maybe_translation.ValueOrDie(); + gltf_json_.BeginArray("translation"); + for (int j = 0; j < 3; ++j) { + gltf_json_.OutputValue(translation[j]); + } + gltf_json_.EndArray(); + } + if (nodes_[i].trs_matrix.RotationSet()) { + const auto maybe_rotation = nodes_[i].trs_matrix.Rotation(); + const auto rotation = maybe_rotation.ValueOrDie(); + gltf_json_.BeginArray("rotation"); + for (int j = 0; j < 4; ++j) { + // Note: coeffs() returns quaternion values as (x, y, z, w) which is + // the expected format of glTF. + gltf_json_.OutputValue(rotation.coeffs()[j]); + } + gltf_json_.EndArray(); + } + if (nodes_[i].trs_matrix.ScaleSet()) { + const auto maybe_scale = nodes_[i].trs_matrix.Scale(); + const auto scale = maybe_scale.ValueOrDie(); + gltf_json_.BeginArray("scale"); + for (int j = 0; j < 3; ++j) { + gltf_json_.OutputValue(scale[j]); + } + gltf_json_.EndArray(); + } + } + + gltf_json_.EndObject(); + } + + gltf_json_.EndArray(); + + const std::string asset_str = gltf_json_.MoveData(); + return buf_out->Encode(asset_str.data(), asset_str.length()); +} + +Status GltfAsset::EncodeMeshesProperty(EncoderBuffer *buf_out) { + mesh_features_texture_index_ = 0; + gltf_json_.BeginArray("meshes"); + + for (int i = 0; i < meshes_.size(); ++i) { + gltf_json_.BeginObject(); + + if (!meshes_[i].name.empty()) { + gltf_json_.OutputValue("name", meshes_[i].name); + } + + if (!meshes_[i].primitives.empty()) { + gltf_json_.BeginArray("primitives"); + + for (int j = 0; j < meshes_[i].primitives.size(); ++j) { + const GltfPrimitive &primitive = meshes_[i].primitives[j]; + gltf_json_.BeginObject(); + + gltf_json_.BeginObject("attributes"); + for (auto const &it : primitive.attributes) { + gltf_json_.OutputValue(it.first, it.second); + } + gltf_json_.EndObject(); + + if (primitive.indices >= 0) { + gltf_json_.OutputValue("indices", primitive.indices); + } + gltf_json_.OutputValue("mode", primitive.mode); + if (primitive.material >= 0) { + gltf_json_.OutputValue("material", primitive.material); + } + DRACO_RETURN_IF_ERROR( + EncodePrimitiveExtensionsProperty(primitive, buf_out)); + gltf_json_.EndObject(); + } + + gltf_json_.EndArray(); + } + + gltf_json_.EndObject(); + } + + gltf_json_.EndArray(); + + const std::string asset_str = gltf_json_.MoveData(); + if (!buf_out->Encode(asset_str.data(), asset_str.length())) { + return ErrorStatus("Failed encoding meshes."); + } + return OkStatus(); +} + +Status GltfAsset::EncodePrimitiveExtensionsProperty( + const GltfPrimitive &primitive, EncoderBuffer *buf_out) { + // Return if the primitive has no extensions to encode. + const bool has_draco_mesh_compression = + primitive.compressed_mesh_info.buffer_view_index >= 0; + const bool has_materials_variants = + !primitive.material_variants_mappings.empty(); + const bool has_mesh_features = !primitive.mesh_features.empty(); + if (!has_draco_mesh_compression && !has_materials_variants && + !has_mesh_features) { + return OkStatus(); + } + + // Encode primitive extensions. + gltf_json_.BeginObject("extensions"); + if (has_draco_mesh_compression) { + gltf_json_.BeginObject("KHR_draco_mesh_compression"); + gltf_json_.OutputValue("bufferView", + primitive.compressed_mesh_info.buffer_view_index); + gltf_json_.BeginObject("attributes"); + for (auto const &it : primitive.compressed_mesh_info.attributes) { + gltf_json_.OutputValue(it.first, it.second); + } + gltf_json_.EndObject(); // attributes entry. + gltf_json_.EndObject(); // KHR_draco_mesh_compression entry. + } + if (has_materials_variants) { + gltf_json_.BeginObject("KHR_materials_variants"); + gltf_json_.BeginArray("mappings"); + for (const auto &mapping : primitive.material_variants_mappings) { + gltf_json_.BeginObject(); + gltf_json_.OutputValue("material", mapping.material); + gltf_json_.BeginArray("variants"); + for (const int variant : mapping.variants) { + gltf_json_.OutputValue(variant); + } + gltf_json_.EndArray(); // variants array. + gltf_json_.EndObject(); + } + gltf_json_.EndArray(); // mappings array. + gltf_json_.EndObject(); // KHR_materials_variants entry. + } + if (has_mesh_features) { + gltf_json_.BeginObject("EXT_mesh_features"); + gltf_json_.BeginArray("featureIds"); + for (int i = 0; i < primitive.mesh_features.size(); i++) { + const auto &features = primitive.mesh_features[i]; + gltf_json_.BeginObject(); + if (!features->GetLabel().empty()) { + gltf_json_.OutputValue("label", features->GetLabel()); + } + gltf_json_.OutputValue("featureCount", features->GetFeatureCount()); + if (features->GetAttributeIndex() != -1) { + gltf_json_.OutputValue("attribute", features->GetAttributeIndex()); + } + if (features->GetPropertyTableIndex() != -1) { + gltf_json_.OutputValue("propertyTable", + features->GetPropertyTableIndex()); + } + if (features->GetTextureMap().tex_coord_index() != -1) { + const TextureMap &texture_map = features->GetTextureMap(); + const std::string texture_stem = TextureUtils::GetOrGenerateTargetStem( + *texture_map.texture(), mesh_features_texture_index_++, + "_MeshFeatures"); + + // Save image as RGBA if the A channel is used to store feature ID. + const auto &channels = features->GetTextureChannels(); + const int num_channels = + std::count(channels.begin(), channels.end(), 3) == 1 ? 4 : 3; + DRACO_ASSIGN_OR_RETURN( + const int image_index, + AddImage(texture_stem, texture_map.texture(), num_channels)); + const int tex_coord_index = texture_map.tex_coord_index(); + Material dummy_material; + DRACO_RETURN_IF_ERROR(EncodeTextureMap("texture", image_index, + tex_coord_index, dummy_material, + texture_map, channels)); + } + if (features->GetNullFeatureId() != -1) { + gltf_json_.OutputValue("nullFeatureId", features->GetNullFeatureId()); + } + gltf_json_.EndObject(); + mesh_features_used_ = true; + } + gltf_json_.EndArray(); // featureIds array. + gltf_json_.EndObject(); // EXT_mesh_features entry. + } + gltf_json_.EndObject(); // extensions entry. + return OkStatus(); +} + +Status GltfAsset::EncodeMaterials(EncoderBuffer *buf_out) { + // Check if we have textures to write. + if (material_library_.NumMaterials() == 0) { + return EncodeDefaultMaterial(buf_out); + } + return EncodeMaterialsProperty(buf_out); +} + +void GltfAsset::EncodeColorMaterial(float red, float green, float blue, + float alpha, float metallic_factor) { + gltf_json_.BeginObject("pbrMetallicRoughness"); + + gltf_json_.BeginArray("baseColorFactor"); + gltf_json_.OutputValue(red); + gltf_json_.OutputValue(green); + gltf_json_.OutputValue(blue); + gltf_json_.OutputValue(alpha); + gltf_json_.EndArray(); + gltf_json_.OutputValue("metallicFactor", metallic_factor); + + gltf_json_.EndObject(); // pbrMetallicRoughness +} + +Status GltfAsset::EncodeDefaultMaterial(EncoderBuffer *buf_out) { + gltf_json_.BeginArray("materials"); + gltf_json_.BeginObject(); + EncodeColorMaterial(0.75, 0.75, 0.75, 1.0, 0.0); + gltf_json_.EndObject(); + gltf_json_.EndArray(); // materials + + const std::string asset_str = gltf_json_.MoveData(); + if (!buf_out->Encode(asset_str.data(), asset_str.length())) { + return Status(Status::DRACO_ERROR, "Error encoding default material."); + } + return OkStatus(); +} + +Status GltfAsset::EncodeTextureMap(const std::string &object_name, + int image_index, int tex_coord_index, + const Material &material, + const TextureMap &texture_map) { + return EncodeTextureMap(object_name, image_index, tex_coord_index, material, + texture_map, {}); +} + +Status GltfAsset::EncodeTextureMap(const std::string &object_name, + int image_index, int tex_coord_index, + const Material &material, + const TextureMap &texture_map, + const std::vector &channels) { + // Create a new texture sampler (or reuse an existing one if possible). + const TextureSampler sampler(texture_map.min_filter(), + texture_map.mag_filter(), + texture_map.wrapping_mode()); + DRACO_ASSIGN_OR_RETURN(const int sampler_index, AddTextureSampler(sampler)); + + // Check if we can reuse an existing texture object. + const GltfTexture texture(image_index, sampler_index); + const auto texture_it = + std::find(textures_.begin(), textures_.end(), texture); + int texture_index; + if (texture_it == textures_.end()) { + // Create a new texture object for this texture map. + texture_index = textures_.size(); + textures_.push_back(GltfTexture(image_index, sampler_index)); + } else { + // Reuse an existing texture object. + texture_index = std::distance(textures_.begin(), texture_it); + } + + gltf_json_.BeginObject(object_name); + gltf_json_.OutputValue("index", texture_index); + gltf_json_.OutputValue("texCoord", tex_coord_index); + if (object_name == "normalTexture") { + const float scale = material.GetNormalTextureScale(); + if (scale != 1.0f) { + gltf_json_.OutputValue("scale", scale); + } + } + + // The "texture" object of the EXT_mesh_features extension has a custom + // property "channels" that is encoded here. + if (object_name == "texture" && !channels.empty()) { + gltf_json_.BeginArray("channels"); + for (const int channel : channels) { + gltf_json_.OutputValue(channel); + } + gltf_json_.EndArray(); // channels array. + } + + // Check if |texture_map| is using the KHR_texture_transform extension. + if (!TextureTransform::IsDefault(texture_map.texture_transform())) { + gltf_json_.BeginObject("extensions"); + gltf_json_.BeginObject("KHR_texture_transform"); + if (texture_map.texture_transform().IsOffsetSet()) { + const std::array &offset = + texture_map.texture_transform().offset(); + gltf_json_.BeginArray("offset"); + gltf_json_.OutputValue(offset[0]); + gltf_json_.OutputValue(offset[1]); + gltf_json_.EndArray(); + } + if (texture_map.texture_transform().IsRotationSet()) { + gltf_json_.OutputValue("rotation", + texture_map.texture_transform().rotation()); + } + if (texture_map.texture_transform().IsScaleSet()) { + const std::array &scale = + texture_map.texture_transform().scale(); + gltf_json_.BeginArray("scale"); + gltf_json_.OutputValue(scale[0]); + gltf_json_.OutputValue(scale[1]); + gltf_json_.EndArray(); + } + // TODO(fgalligan): The spec says the extension is not required if the + // pre-transform and the post-transform tex coords are the same. But I'm not + // sure why. I have filed a bug asking for clarification. + // https://github.com/KhronosGroup/glTF/issues/1724 + if (texture_map.texture_transform().IsTexCoordSet()) { + gltf_json_.OutputValue("texCoord", + texture_map.texture_transform().tex_coord()); + } else { + extensions_required_.insert("KHR_texture_transform"); + } + gltf_json_.EndObject(); + gltf_json_.EndObject(); + + extensions_used_.insert("KHR_texture_transform"); + } + gltf_json_.EndObject(); + return OkStatus(); +} + +Status GltfAsset::EncodeMaterialsProperty(EncoderBuffer *buf_out) { + gltf_json_.BeginArray("materials"); + for (int i = 0; i < material_library_.NumMaterials(); ++i) { + const Material *const material = material_library_.GetMaterial(i); + if (!material) { + return Status(Status::DRACO_ERROR, "Error getting material."); + } + + const TextureMap *const color = + material->GetTextureMapByType(TextureMap::COLOR); + const TextureMap *const metallic = + material->GetTextureMapByType(TextureMap::METALLIC_ROUGHNESS); + const TextureMap *const normal = + material->GetTextureMapByType(TextureMap::NORMAL_TANGENT_SPACE); + const TextureMap *const occlusion = + material->GetTextureMapByType(TextureMap::AMBIENT_OCCLUSION); + const TextureMap *const emissive = + material->GetTextureMapByType(TextureMap::EMISSIVE); + + // Check if material is unlit and does not have a fallback. + if (material->GetUnlit() && + (!color || metallic || normal || occlusion || emissive || + material->GetMetallicFactor() != 0.0 || + material->GetRoughnessFactor() <= 0.5 || + material->GetEmissiveFactor() != Vector3f(0.0, 0.0, 0.0))) { + // If we find one material that is unlit and does not contain a fallback + // we must set "KHR_materials_unlit" in extensions reqruied for the entire + // glTF file. + extensions_required_.insert("KHR_materials_unlit"); + } + + int occlusion_metallic_roughness_image_index = -1; + + gltf_json_.BeginObject(); // material object. + + gltf_json_.BeginObject("pbrMetallicRoughness"); + if (color) { + const bool rgba = true; // Unused for now. + const std::string texture_stem = TextureUtils::GetOrGenerateTargetStem( + *color->texture(), i, "_BaseColor"); + DRACO_ASSIGN_OR_RETURN( + const int color_image_index, + AddImage(texture_stem, color->texture(), rgba ? 4 : 3)); + DRACO_RETURN_IF_ERROR( + EncodeTextureMap("baseColorTexture", color_image_index, + color->tex_coord_index(), *material, *color)); + } + // Try to combine metallic and occlusion only if they have the same tex + // coord index. + // TODO(b/145991271): Check out if we need to check texture indices. + if (metallic && occlusion && + metallic->tex_coord_index() == occlusion->tex_coord_index()) { + if (metallic->texture() == occlusion->texture()) { + const std::string texture_stem = TextureUtils::GetOrGenerateTargetStem( + *metallic->texture(), i, "_OcclusionMetallicRoughness"); + // Metallic and occlusion textures are already combined. + DRACO_ASSIGN_OR_RETURN(occlusion_metallic_roughness_image_index, + AddImage(texture_stem, metallic->texture(), 3)); + } + if (occlusion_metallic_roughness_image_index != -1) + DRACO_RETURN_IF_ERROR(EncodeTextureMap( + "metallicRoughnessTexture", + occlusion_metallic_roughness_image_index, + metallic->tex_coord_index(), *material, *metallic)); + } + + if (metallic && occlusion_metallic_roughness_image_index == -1) { + const std::string texture_stem = TextureUtils::GetOrGenerateTargetStem( + *metallic->texture(), i, "_MetallicRoughness"); + DRACO_ASSIGN_OR_RETURN(const int metallic_roughness_image_index, + AddImage(texture_stem, metallic->texture(), 3)); + DRACO_RETURN_IF_ERROR(EncodeTextureMap( + "metallicRoughnessTexture", metallic_roughness_image_index, + metallic->tex_coord_index(), *material, *metallic)); + } + + EncodeVectorArray("baseColorFactor", material->GetColorFactor()); + gltf_json_.OutputValue("metallicFactor", material->GetMetallicFactor()); + gltf_json_.OutputValue("roughnessFactor", material->GetRoughnessFactor()); + gltf_json_.EndObject(); // pbrMetallicRoughness + + if (normal) { + const std::string texture_stem = TextureUtils::GetOrGenerateTargetStem( + *normal->texture(), i, "_Normal"); + DRACO_ASSIGN_OR_RETURN(const int normal_image_index, + AddImage(texture_stem, normal->texture(), 3)); + DRACO_RETURN_IF_ERROR( + EncodeTextureMap("normalTexture", normal_image_index, + normal->tex_coord_index(), *material, *normal)); + } + + if (occlusion_metallic_roughness_image_index != -1) { + DRACO_RETURN_IF_ERROR(EncodeTextureMap( + "occlusionTexture", occlusion_metallic_roughness_image_index, + metallic->tex_coord_index(), *material, *metallic)); + } else if (occlusion) { + // Store occlusion texture in a grayscale format, unless it is used by + // metallic-roughness map of some other matierial. It is possible that + // this material uses occlusion (R channel) and some other material uses + // metallic-roughness (GB channels) from this texture. + const int num_components = TextureUtils::ComputeRequiredNumChannels( + *occlusion->texture(), material_library_); + const std::string suffix = + (num_components == 1) ? "_Occlusion" : "_OcclusionMetallicRoughness"; + const std::string texture_stem = TextureUtils::GetOrGenerateTargetStem( + *occlusion->texture(), i, suffix); + DRACO_ASSIGN_OR_RETURN( + const int occlusion_image_index, + AddImage(texture_stem, occlusion->texture(), num_components)); + DRACO_RETURN_IF_ERROR(EncodeTextureMap( + "occlusionTexture", occlusion_image_index, + occlusion->tex_coord_index(), *material, *occlusion)); + } + + if (emissive) { + const std::string texture_stem = TextureUtils::GetOrGenerateTargetStem( + *emissive->texture(), i, "_Emissive"); + DRACO_ASSIGN_OR_RETURN(const int emissive_image_index, + AddImage(texture_stem, emissive->texture(), 3)); + DRACO_RETURN_IF_ERROR( + EncodeTextureMap("emissiveTexture", emissive_image_index, + emissive->tex_coord_index(), *material, *emissive)); + } + + EncodeVectorArray("emissiveFactor", + material->GetEmissiveFactor()); + + switch (material->GetTransparencyMode()) { + case Material::TransparencyMode::TRANSPARENCY_MASK: + gltf_json_.OutputValue("alphaMode", "MASK"); + gltf_json_.OutputValue("alphaCutoff", material->GetAlphaCutoff()); + break; + case Material::TransparencyMode::TRANSPARENCY_BLEND: + gltf_json_.OutputValue("alphaMode", "BLEND"); + break; + default: + gltf_json_.OutputValue("alphaMode", "OPAQUE"); + break; + } + if (!material->GetName().empty()) { + gltf_json_.OutputValue("name", material->GetName()); + } + + // Output doubleSided if different than the default. + if (material->GetDoubleSided()) { + gltf_json_.OutputValue("doubleSided", material->GetDoubleSided()); + } + + // Encode material extensions if any. + if (material->GetUnlit() || material->HasSheen() || + material->HasTransmission() || material->HasClearcoat() || + material->HasVolume() || material->HasIor() || + material->HasSpecular()) { + gltf_json_.BeginObject("extensions"); + + // Encode individual material extensions. + if (material->GetUnlit()) { + EncodeMaterialUnlitExtension(*material); + } else { + // PBR extensions can only be added to non-unlit materials. + Material defaults; + if (material->HasSheen()) { + DRACO_RETURN_IF_ERROR( + EncodeMaterialSheenExtension(*material, defaults, i)); + } + if (material->HasTransmission()) { + DRACO_RETURN_IF_ERROR( + EncodeMaterialTransmissionExtension(*material, defaults, i)); + } + if (material->HasClearcoat()) { + DRACO_RETURN_IF_ERROR( + EncodeMaterialClearcoatExtension(*material, defaults, i)); + } + if (material->HasVolume()) { + DRACO_RETURN_IF_ERROR( + EncodeMaterialVolumeExtension(*material, defaults, i)); + } + if (material->HasIor()) { + DRACO_RETURN_IF_ERROR( + EncodeMaterialIorExtension(*material, defaults)); + } + if (material->HasSpecular()) { + DRACO_RETURN_IF_ERROR( + EncodeMaterialSpecularExtension(*material, defaults, i)); + } + } + + gltf_json_.EndObject(); // extensions object. + } + + gltf_json_.EndObject(); // material object. + } + + gltf_json_.EndArray(); // materials array. + + if (!textures_.empty()) { + gltf_json_.BeginArray("textures"); + for (int i = 0; i < textures_.size(); ++i) { + const int image_index = textures_[i].image_index; + gltf_json_.BeginObject(); + if (images_[image_index].mime_type == "image/webp") { + gltf_json_.BeginObject("extensions"); + gltf_json_.BeginObject("EXT_texture_webp"); + gltf_json_.OutputValue("source", image_index); + gltf_json_.EndObject(); + gltf_json_.EndObject(); + } else if (images_[image_index].mime_type == "image/ktx2") { + gltf_json_.BeginObject("extensions"); + gltf_json_.BeginObject("KHR_texture_basisu"); + gltf_json_.OutputValue("source", image_index); + gltf_json_.EndObject(); + gltf_json_.EndObject(); + } else { + gltf_json_.OutputValue("source", image_index); + } + if (textures_[i].sampler_index >= 0) { + gltf_json_.OutputValue("sampler", textures_[i].sampler_index); + } + gltf_json_.EndObject(); + } + gltf_json_.EndArray(); + } + + if (!texture_samplers_.empty()) { + gltf_json_.BeginArray("samplers"); + for (int i = 0; i < texture_samplers_.size(); ++i) { + gltf_json_.BeginObject(); + + const int mode_s = TextureAxisWrappingModeToGltfValue( + texture_samplers_[i].wrapping_mode.s); + const int mode_t = TextureAxisWrappingModeToGltfValue( + texture_samplers_[i].wrapping_mode.t); + gltf_json_.OutputValue("wrapS", mode_s); + gltf_json_.OutputValue("wrapT", mode_t); + + if (texture_samplers_[i].min_filter != TextureMap::UNSPECIFIED) { + gltf_json_.OutputValue( + "minFilter", + TextureFilterTypeToGltfValue(texture_samplers_[i].min_filter)); + } + if (texture_samplers_[i].mag_filter != TextureMap::UNSPECIFIED) { + gltf_json_.OutputValue( + "magFilter", + TextureFilterTypeToGltfValue(texture_samplers_[i].mag_filter)); + } + + gltf_json_.EndObject(); + } + gltf_json_.EndArray(); + } + + if (!images_.empty()) { + gltf_json_.BeginArray("images"); + for (int i = 0; i < images_.size(); ++i) { + if (add_images_to_buffer_) { + DRACO_RETURN_IF_ERROR(SaveImageToBuffer(i)); + } + gltf_json_.BeginObject(); + if (images_[i].buffer_view >= 0) { + gltf_json_.OutputValue("bufferView", images_[i].buffer_view); + gltf_json_.OutputValue("mimeType", images_[i].mime_type); + } else { + gltf_json_.OutputValue("uri", images_[i].image_name); + } + gltf_json_.EndObject(); + } + gltf_json_.EndArray(); + } + + const std::string asset_str = gltf_json_.MoveData(); + if (!buf_out->Encode(asset_str.data(), asset_str.length())) { + return Status(Status::DRACO_ERROR, "Error encoding materials."); + } + return OkStatus(); +} + +void GltfAsset::EncodeMaterialUnlitExtension(const Material &material) { + extensions_used_.insert("KHR_materials_unlit"); + gltf_json_.BeginObject("KHR_materials_unlit"); + gltf_json_.EndObject(); +} + +Status GltfAsset::EncodeMaterialSheenExtension(const Material &material, + const Material &defaults, + int material_index) { + extensions_used_.insert("KHR_materials_sheen"); + gltf_json_.BeginObject("KHR_materials_sheen"); + + // Add sheen color factor, unless it is the default. + if (material.GetSheenColorFactor() != defaults.GetSheenColorFactor()) { + EncodeVectorArray("sheenColorFactor", + material.GetSheenColorFactor()); + } + + // Add sheen roughness factor, unless it is the default. + if (material.GetSheenRoughnessFactor() != + defaults.GetSheenRoughnessFactor()) { + gltf_json_.OutputValue("sheenRoughnessFactor", + material.GetSheenRoughnessFactor()); + } + + // Add sheen color texture (RGB channels) if present. + // TODO(vytyaz): Combine sheen color and roughness images if possible. + DRACO_RETURN_IF_ERROR(EncodeTexture("sheenColorTexture", "_SheenColor", + TextureMap::SHEEN_COLOR, -1, material, + material_index)); + + // Add sheen roughness texture (A channel) if present. + DRACO_RETURN_IF_ERROR( + EncodeTexture("sheenRoughnessTexture", "_SheenRoughness", + TextureMap::SHEEN_ROUGHNESS, 4, material, material_index)); + + gltf_json_.EndObject(); // KHR_materials_sheen object. + + return OkStatus(); +} + +Status GltfAsset::EncodeMaterialTransmissionExtension(const Material &material, + const Material &defaults, + int material_index) { + extensions_used_.insert("KHR_materials_transmission"); + gltf_json_.BeginObject("KHR_materials_transmission"); + + // Add transmission factor, unless it is the default. + if (material.GetTransmissionFactor() != defaults.GetTransmissionFactor()) { + gltf_json_.OutputValue("transmissionFactor", + material.GetTransmissionFactor()); + } + + // Add transmission texture (R channel) if present. + // TODO(vytyaz): Store texture in a grayscale format if possible. + DRACO_RETURN_IF_ERROR(EncodeTexture("transmissionTexture", "_Transmission", + TextureMap::TRANSMISSION, 3, material, + material_index)); + + gltf_json_.EndObject(); // KHR_materials_transmission object. + + return OkStatus(); +} + +Status GltfAsset::EncodeMaterialClearcoatExtension(const Material &material, + const Material &defaults, + int material_index) { + extensions_used_.insert("KHR_materials_clearcoat"); + gltf_json_.BeginObject("KHR_materials_clearcoat"); + + // Add clearcoat factor, unless it is the default. + if (material.GetClearcoatFactor() != defaults.GetClearcoatFactor()) { + gltf_json_.OutputValue("clearcoatFactor", material.GetClearcoatFactor()); + } + + // Add clearcoat roughness factor, unless it is the default. + if (material.GetClearcoatRoughnessFactor() != + defaults.GetClearcoatRoughnessFactor()) { + gltf_json_.OutputValue("clearcoatRoughnessFactor", + material.GetClearcoatRoughnessFactor()); + } + + // Add clearcoat texture (R channel) if present. + // TODO(vytyaz): Combine clearcoat and clearcoat roughness images if possible. + // TODO(vytyaz): Store texture in a grayscale format if possible. + DRACO_RETURN_IF_ERROR(EncodeTexture("clearcoatTexture", "_Clearcoat", + TextureMap::CLEARCOAT, 3, material, + material_index)); + + // Add clearcoat roughness texture (G channel) if present. + DRACO_RETURN_IF_ERROR(EncodeTexture( + "clearcoatRoughnessTexture", "_ClearcoatRoughness", + TextureMap::CLEARCOAT_ROUGHNESS, 3, material, material_index)); + + // Add clearcoat normal texture (RGB channels) if present. + DRACO_RETURN_IF_ERROR( + EncodeTexture("clearcoatNormalTexture", "_ClearcoatNormal", + TextureMap::CLEARCOAT_NORMAL, 3, material, material_index)); + + gltf_json_.EndObject(); // KHR_materials_clearcoat object. + + return OkStatus(); +} + +Status GltfAsset::EncodeMaterialVolumeExtension(const Material &material, + const Material &defaults, + int material_index) { + extensions_used_.insert("KHR_materials_volume"); + gltf_json_.BeginObject("KHR_materials_volume"); + + // Add thickness factor, unless it is the default. + if (material.GetThicknessFactor() != defaults.GetThicknessFactor()) { + gltf_json_.OutputValue("thicknessFactor", material.GetThicknessFactor()); + } + + // Add attenuation distance, unless it is the default. + if (material.GetAttenuationDistance() != defaults.GetAttenuationDistance()) { + gltf_json_.OutputValue("attenuationDistance", + material.GetAttenuationDistance()); + } + + // Add attenuation color, unless it is the default. + if (material.GetAttenuationColor() != defaults.GetAttenuationColor()) { + EncodeVectorArray("attenuationColor", + material.GetAttenuationColor()); + } + + // Add thickness texture (G channel) if present. + DRACO_RETURN_IF_ERROR(EncodeTexture("thicknessTexture", "_Thickness", + TextureMap::THICKNESS, 3, material, + material_index)); + + gltf_json_.EndObject(); // KHR_materials_volume object. + + return OkStatus(); +} + +Status GltfAsset::EncodeMaterialIorExtension(const Material &material, + const Material &defaults) { + extensions_used_.insert("KHR_materials_ior"); + gltf_json_.BeginObject("KHR_materials_ior"); + + // Add ior, unless it is the default. + if (material.GetIor() != defaults.GetIor()) { + gltf_json_.OutputValue("ior", material.GetIor()); + } + + gltf_json_.EndObject(); // KHR_materials_ior object. + + return OkStatus(); +} + +Status GltfAsset::EncodeMaterialSpecularExtension(const Material &material, + const Material &defaults, + int material_index) { + extensions_used_.insert("KHR_materials_specular"); + gltf_json_.BeginObject("KHR_materials_specular"); + + // Add specular factor, unless it is the default. + if (material.GetSpecularFactor() != defaults.GetSpecularFactor()) { + gltf_json_.OutputValue("specularFactor", material.GetSpecularFactor()); + } + + // Add specular color factor, unless it is the default. + if (material.GetSpecularColorFactor() != defaults.GetSpecularColorFactor()) { + EncodeVectorArray("specularColorFactor", + material.GetSpecularColorFactor()); + } + + // Add specular texture (A channel) if present. + // TODO(vytyaz): Combine specular and specular color images if possible. + DRACO_RETURN_IF_ERROR(EncodeTexture("specularTexture", "_Specular", + TextureMap::SPECULAR, 4, material, + material_index)); + + // Add specular color texture (RGB channels) if present. + DRACO_RETURN_IF_ERROR(EncodeTexture("specularColorTexture", "_SpecularColor", + TextureMap::SPECULAR_COLOR, -1, material, + material_index)); + + gltf_json_.EndObject(); // KHR_materials_specular object. + + return OkStatus(); +} + +Status GltfAsset::EncodeTexture(const std::string &name, + const std::string &stem_suffix, + TextureMap::Type type, int num_components, + const Material &material, int material_index) { + const TextureMap *const texture_map = material.GetTextureMapByType(type); + if (texture_map) { + if (num_components == -1) { + const bool rgba = true; // Unused for now. + num_components = rgba ? 4 : 3; + } + const std::string texture_stem = TextureUtils::GetOrGenerateTargetStem( + *texture_map->texture(), material_index, stem_suffix); + DRACO_ASSIGN_OR_RETURN( + const int image_index, + AddImage(texture_stem, texture_map->texture(), num_components)); + DRACO_RETURN_IF_ERROR(EncodeTextureMap(name, image_index, + texture_map->tex_coord_index(), + material, *texture_map)); + } + return OkStatus(); +} + +Status GltfAsset::EncodeAnimationsProperty(EncoderBuffer *buf_out) { + if (animations_.empty()) { + return OkStatus(); + } + + gltf_json_.BeginArray("animations"); + for (int i = 0; i < animations_.size(); ++i) { + gltf_json_.BeginObject(); + + if (!animations_[i]->name.empty()) { + gltf_json_.OutputValue("name", animations_[i]->name); + } + + gltf_json_.BeginArray("samplers"); + for (int j = 0; j < animations_[i]->samplers.size(); ++j) { + gltf_json_.BeginObject(); + gltf_json_.OutputValue("input", animations_[i]->samplers[j]->input_index); + gltf_json_.OutputValue( + "interpolation", + AnimationSampler::InterpolationToString( + animations_[i]->samplers[j]->interpolation_type)); + gltf_json_.OutputValue("output", + animations_[i]->samplers[j]->output_index); + gltf_json_.EndObject(); + } + gltf_json_.EndArray(); + + gltf_json_.BeginArray("channels"); + for (int j = 0; j < animations_[i]->channels.size(); ++j) { + gltf_json_.BeginObject(); + gltf_json_.OutputValue("sampler", + animations_[i]->channels[j]->sampler_index); + + gltf_json_.BeginObject("target"); + gltf_json_.OutputValue("node", animations_[i]->channels[j]->target_index); + gltf_json_.OutputValue( + "path", AnimationChannel::TransformationToString( + animations_[i]->channels[j]->transformation_type)); + gltf_json_.EndObject(); + + gltf_json_.EndObject(); // Channel entry. + } + gltf_json_.EndArray(); + + gltf_json_.EndObject(); // Animmation entry. + } + gltf_json_.EndArray(); + + const std::string asset_str = gltf_json_.MoveData(); + if (!buf_out->Encode(asset_str.data(), asset_str.length())) { + return Status(Status::DRACO_ERROR, "Could not encode animations."); + } + return OkStatus(); +} + +Status GltfAsset::EncodeSkinsProperty(EncoderBuffer *buf_out) { + if (skins_.empty()) { + return OkStatus(); + } + + gltf_json_.BeginArray("skins"); + for (int i = 0; i < skins_.size(); ++i) { + gltf_json_.BeginObject(); + + if (skins_[i]->inverse_bind_matrices_index >= 0) { + gltf_json_.OutputValue("inverseBindMatrices", + skins_[i]->inverse_bind_matrices_index); + } + if (skins_[i]->skeleton_index >= 0) { + gltf_json_.OutputValue("skeleton", skins_[i]->skeleton_index); + } + + if (!skins_[i]->joints.empty()) { + gltf_json_.BeginArray("joints"); + for (int j = 0; j < skins_[i]->joints.size(); ++j) { + gltf_json_.OutputValue(skins_[i]->joints[j]); + } + gltf_json_.EndArray(); + } + gltf_json_.EndObject(); // Skin entry. + } + gltf_json_.EndArray(); + + const std::string asset_str = gltf_json_.MoveData(); + if (!buf_out->Encode(asset_str.data(), asset_str.length())) { + return Status(Status::DRACO_ERROR, "Could not encode animations."); + } + return OkStatus(); +} + +Status GltfAsset::EncodeTopLevelExtensionsProperty(EncoderBuffer *buf_out) { + // Return if there are no top-level asset extensions to encode. + if (lights_.empty() && materials_variants_names_.empty() && + property_tables_.empty()) { + return OkStatus(); + } + + // Encode top-level extensions. + gltf_json_.BeginObject("extensions"); + DRACO_RETURN_IF_ERROR(EncodeLightsProperty(buf_out)); + DRACO_RETURN_IF_ERROR(EncodeMaterialsVariantsNamesProperty(buf_out)); + DRACO_RETURN_IF_ERROR(EncodeStructuralMetadataProperty(buf_out)); + gltf_json_.EndObject(); // extensions entry. + return OkStatus(); +} + +Status GltfAsset::EncodeLightsProperty(EncoderBuffer *buf_out) { + if (lights_.empty()) { + return OkStatus(); + } + + gltf_json_.BeginObject("KHR_lights_punctual"); + gltf_json_.BeginArray("lights"); + const Light defaults; + for (const auto &light : lights_) { + gltf_json_.BeginObject(); + if (light->GetName() != defaults.GetName()) { + gltf_json_.OutputValue("name", light->GetName()); + } + if (light->GetColor() != defaults.GetColor()) { + gltf_json_.BeginArray("color"); + gltf_json_.OutputValue(light->GetColor()[0]); + gltf_json_.OutputValue(light->GetColor()[1]); + gltf_json_.OutputValue(light->GetColor()[2]); + gltf_json_.EndArray(); + } + if (light->GetIntensity() != defaults.GetIntensity()) { + gltf_json_.OutputValue("intensity", light->GetIntensity()); + } + switch (light->GetType()) { + case Light::DIRECTIONAL: + gltf_json_.OutputValue("type", "directional"); + break; + case Light::POINT: + gltf_json_.OutputValue("type", "point"); + break; + case Light::SPOT: + gltf_json_.OutputValue("type", "spot"); + break; + } + if (light->GetRange() != defaults.GetRange()) { + gltf_json_.OutputValue("range", light->GetRange()); + } + if (light->GetType() == Light::SPOT) { + gltf_json_.BeginObject("spot"); + if (light->GetInnerConeAngle() != defaults.GetInnerConeAngle()) { + gltf_json_.OutputValue("innerConeAngle", light->GetInnerConeAngle()); + } + if (light->GetOuterConeAngle() != defaults.GetOuterConeAngle()) { + gltf_json_.OutputValue("outerConeAngle", light->GetOuterConeAngle()); + } + gltf_json_.EndObject(); + } + gltf_json_.EndObject(); + } + gltf_json_.EndArray(); + gltf_json_.EndObject(); // KHR_lights_punctual entry. + return OkStatus(); +} + +Status GltfAsset::EncodeMaterialsVariantsNamesProperty(EncoderBuffer *buf_out) { + if (materials_variants_names_.empty()) { + return OkStatus(); + } + + gltf_json_.BeginObject("KHR_materials_variants"); + gltf_json_.BeginArray("variants"); + for (const std::string &name : materials_variants_names_) { + gltf_json_.BeginObject(); + gltf_json_.OutputValue("name", name); + gltf_json_.EndObject(); + } + gltf_json_.EndArray(); + gltf_json_.EndObject(); // KHR_materials_variants entry. + return OkStatus(); +} + +Status GltfAsset::EncodeStructuralMetadataProperty(EncoderBuffer *buf_out) { + if (property_table_schema_.Empty()) { + return OkStatus(); + } + + gltf_json_.BeginObject("EXT_structural_metadata"); + + // Encodes property table schema. + struct SchemaWriter { + static void Write(const PropertyTable::Schema::Object &object, + JsonWriter *json_writer) { + switch (object.GetType()) { + case PropertyTable::Schema::Object::OBJECT: + json_writer->BeginObject(object.GetName()); + for (const PropertyTable::Schema::Object &obj : object.GetObjects()) { + Write(obj, json_writer); + } + json_writer->EndObject(); + break; + case PropertyTable::Schema::Object::ARRAY: + json_writer->BeginArray(object.GetName()); + for (const PropertyTable::Schema::Object &obj : object.GetArray()) { + Write(obj, json_writer); + } + json_writer->EndArray(); + break; + case PropertyTable::Schema::Object::STRING: + json_writer->OutputValue(object.GetName(), object.GetString()); + break; + case PropertyTable::Schema::Object::INTEGER: + json_writer->OutputValue(object.GetName(), object.GetInteger()); + break; + case PropertyTable::Schema::Object::BOOLEAN: + json_writer->OutputValue(object.GetName(), object.GetBoolean()); + break; + } + } + }; + + // Encode property table schema. + SchemaWriter::Write(property_table_schema_.json, &gltf_json_); + + // Encode all property tables. + gltf_json_.BeginArray("propertyTables"); + for (const PropertyTable *const table : property_tables_) { + gltf_json_.BeginObject(); + if (!table->GetName().empty()) { + gltf_json_.OutputValue("name", table->GetName()); + } + if (!table->GetClass().empty()) { + gltf_json_.OutputValue("class", table->GetClass()); + } + gltf_json_.OutputValue("count", table->GetCount()); + + // Encoder all property table properties. + gltf_json_.BeginObject("properties"); + for (int i = 0; i < table->NumProperties(); ++i) { + const PropertyTable::Property &property = table->GetProperty(i); + gltf_json_.BeginObject(property.GetName()); + + // Encode property values. + DRACO_ASSIGN_OR_RETURN(const int buffer_view_index, + AddBufferView(property.GetData())); + gltf_json_.OutputValue("values", buffer_view_index); + + // Encode offsets for variable-length arrays. + if (!property.GetArrayOffsets().data.data.empty()) { + if (!property.GetArrayOffsets().type.empty()) { + gltf_json_.OutputValue("arrayOffsetType", + property.GetArrayOffsets().type); + } + DRACO_ASSIGN_OR_RETURN(const int buffer_view_index, + AddBufferView(property.GetArrayOffsets().data)); + gltf_json_.OutputValue("arrayOffsets", buffer_view_index); + } + + // Encode offsets for strings. + if (!property.GetStringOffsets().data.data.empty()) { + if (!property.GetStringOffsets().type.empty()) { + gltf_json_.OutputValue("stringOffsetType", + property.GetStringOffsets().type); + } + DRACO_ASSIGN_OR_RETURN(const int buffer_view_index, + AddBufferView(property.GetStringOffsets().data)); + gltf_json_.OutputValue("stringOffsets", buffer_view_index); + } + gltf_json_.EndObject(); // Named property entry. + } + gltf_json_.EndObject(); // properties entry. + gltf_json_.EndObject(); + } + gltf_json_.EndArray(); // propertyTables entry. + gltf_json_.EndObject(); // EXT_structural_metadata entry. + return OkStatus(); +} + +bool GltfAsset::EncodeAccessorsProperty(EncoderBuffer *buf_out) { + gltf_json_.BeginArray("accessors"); + + for (int i = 0; i < accessors_.size(); ++i) { + gltf_json_.BeginObject(); + + if (accessors_[i].buffer_view_index >= 0) { + gltf_json_.OutputValue("bufferView", accessors_[i].buffer_view_index); + if (output_type_ == GltfEncoder::VERBOSE) { + gltf_json_.OutputValue("byteOffset", 0); + } + } + gltf_json_.OutputValue("componentType", accessors_[i].component_type); + gltf_json_.OutputValue("count", accessors_[i].count); + if (accessors_[i].normalized) { + gltf_json_.OutputValue("normalized", accessors_[i].normalized); + } + + if (!accessors_[i].max.empty()) { + gltf_json_.BeginArray("max"); + for (int j = 0; j < accessors_[i].max.size(); ++j) { + gltf_json_.OutputValue(accessors_[i].max[j]); + } + gltf_json_.EndArray(); + } + + if (!accessors_[i].min.empty()) { + gltf_json_.BeginArray("min"); + for (int j = 0; j < accessors_[i].min.size(); ++j) { + gltf_json_.OutputValue(accessors_[i].min[j]); + } + gltf_json_.EndArray(); + } + + gltf_json_.OutputValue("type", accessors_[i].type); + + gltf_json_.EndObject(); + } + + gltf_json_.EndArray(); + + const std::string asset_str = gltf_json_.MoveData(); + return buf_out->Encode(asset_str.data(), asset_str.length()); +} + +bool GltfAsset::EncodeBufferViewsProperty(EncoderBuffer *buf_out) { + // We currently only support one buffer. + gltf_json_.BeginArray("bufferViews"); + + for (int i = 0; i < buffer_views_.size(); ++i) { + gltf_json_.BeginObject(); + gltf_json_.OutputValue("buffer", 0); + gltf_json_.OutputValue("byteOffset", buffer_views_[i].buffer_byte_offset); + gltf_json_.OutputValue("byteLength", buffer_views_[i].byte_length); + if (buffer_views_[i].target != 0) { + gltf_json_.OutputValue("target", buffer_views_[i].target); + } + gltf_json_.EndObject(); + } + + gltf_json_.EndArray(); + + const std::string asset_str = gltf_json_.MoveData(); + return buf_out->Encode(asset_str.data(), asset_str.length()); +} + +bool GltfAsset::EncodeBuffersProperty(EncoderBuffer *buf_out) { + if (buffer_.size() == 0) { + return true; + } + // We currently only support one buffer. + gltf_json_.BeginArray("buffers"); + gltf_json_.BeginObject(); + gltf_json_.OutputValue("byteLength", buffer_.size()); + if (!buffer_name_.empty()) { + gltf_json_.OutputValue("uri", buffer_name_); + } + gltf_json_.EndObject(); + gltf_json_.EndArray(); + + const std::string asset_str = gltf_json_.MoveData(); + return buf_out->Encode(asset_str.data(), asset_str.length()); +} + +Status GltfAsset::EncodeExtensionsProperties(EncoderBuffer *buf_out) { + if (draco_compression_used_) { + const std::string draco_tag = "KHR_draco_mesh_compression"; + extensions_used_.insert(draco_tag); + extensions_required_.insert(draco_tag); + } + if (!lights_.empty()) { + extensions_used_.insert("KHR_lights_punctual"); + } + if (!materials_variants_names_.empty()) { + extensions_used_.insert("KHR_materials_variants"); + } + if (!instance_arrays_.empty()) { + extensions_used_.insert("EXT_mesh_gpu_instancing"); + extensions_required_.insert("EXT_mesh_gpu_instancing"); + } + if (mesh_features_used_) { + extensions_used_.insert("EXT_mesh_features"); + } + if (!property_table_schema_.Empty()) { + extensions_used_.insert("EXT_structural_metadata"); + } + + if (!extensions_required_.empty()) { + gltf_json_.BeginArray("extensionsRequired"); + for (const auto &extension : extensions_required_) { + gltf_json_.OutputValue(extension); + } + gltf_json_.EndArray(); + } + if (!extensions_used_.empty()) { + gltf_json_.BeginArray("extensionsUsed"); + for (const auto &extension : extensions_used_) { + gltf_json_.OutputValue(extension); + } + gltf_json_.EndArray(); + } + + const std::string asset_str = gltf_json_.MoveData(); + if (!asset_str.empty()) { + if (!buf_out->Encode(asset_str.data(), asset_str.length())) { + return Status(Status::DRACO_ERROR, "Could not encode extensions."); + } + } + return OkStatus(); +} + +template <> +GltfAsset::ComponentType GltfAsset::GetComponentType() const { + return BYTE; +} + +template <> +GltfAsset::ComponentType GltfAsset::GetComponentType() const { + return UNSIGNED_BYTE; +} + +template <> +GltfAsset::ComponentType GltfAsset::GetComponentType() const { + return SHORT; +} + +template <> +GltfAsset::ComponentType GltfAsset::GetComponentType() const { + return UNSIGNED_SHORT; +} + +template <> +GltfAsset::ComponentType GltfAsset::GetComponentType() const { + return UNSIGNED_INT; +} + +template <> +GltfAsset::ComponentType GltfAsset::GetComponentType() const { + return FLOAT; +} + +template +int GltfAsset::AddAttribute(const PointAttribute &att, int num_points, + int num_encoded_points, const std::string &type, + bool compress) { + if (att.size() == 0) { + return -1; // Attribute size must be greater than 0. + } + + std::array value; + std::array min_values; + std::array max_values; + + // Set min and max values. + if (!att.ConvertValue(AttributeValueIndex(0), + &min_values[0])) { + return -1; + } + max_values = min_values; + + if (output_type_ == GltfEncoder::VERBOSE || + att.attribute_type() == GeometryAttribute::POSITION) { + for (AttributeValueIndex i(1); i < static_cast(att.size()); ++i) { + if (!att.ConvertValue(i, &value[0])) { + return -1; + } + for (int j = 0; j < att_components_t; ++j) { + if (value[j] < min_values[j]) { + min_values[j] = value[j]; + } + if (value[j] > max_values[j]) { + max_values[j] = value[j]; + } + } + } + } + + const int kComponentSize = sizeof(att_data_t); + + GltfAccessor accessor; + if (!compress) { + const size_t buffer_start_offset = buffer_.size(); + for (PointIndex v(0); v < num_points; ++v) { + if (!att.ConvertValue(att.mapped_index(v), + &value[0])) { + return -1; + } + + for (int j = 0; j < att_components_t; ++j) { + buffer_.Encode(&value[j], kComponentSize); + } + } + + if (!PadBuffer()) { + return -1; + } + + GltfBufferView buffer_view; + buffer_view.buffer_byte_offset = buffer_start_offset; + buffer_view.byte_length = buffer_.size() - buffer_start_offset; + buffer_views_.push_back(buffer_view); + accessor.buffer_view_index = static_cast(buffer_views_.size() - 1); + } + + accessor.component_type = GetComponentType(); + accessor.count = num_encoded_points; + if (output_type_ == GltfEncoder::VERBOSE || + att.attribute_type() == GeometryAttribute::POSITION) { + for (int j = 0; j < att_components_t; ++j) { + accessor.max.push_back(GltfValue(max_values[j])); + accessor.min.push_back(GltfValue(min_values[j])); + } + } + accessor.type = type; + accessor.normalized = att.attribute_type() == GeometryAttribute::COLOR && + att.data_type() != DT_FLOAT32; + accessors_.push_back(accessor); + return static_cast(accessors_.size() - 1); +} + +const char GltfEncoder::kDracoMetadataGltfAttributeName[] = + "//GLTF/ApplicationSpecificAttributeName"; + +GltfEncoder::GltfEncoder() : out_buffer_(nullptr), output_type_(COMPACT) {} + +template +bool GltfEncoder::EncodeToFile(const T &geometry, const std::string &file_name, + const std::string &base_dir) { + const std::string buffer_name = base_dir + "/buffer0.bin"; + return EncodeFile(geometry, file_name, buffer_name, base_dir).ok(); +} + +template +Status GltfEncoder::EncodeFile(const T &geometry, const std::string &filename) { + if (filename.empty()) { + return Status(Status::DRACO_ERROR, "Output parameter is empty."); + } + + std::string dir_path; + std::string basename; + draco::SplitPath(filename, &dir_path, &basename); + const std::string bin_basename = ReplaceFileExtension(basename, "bin"); + const std::string bin_filename = dir_path + "/" + bin_basename; + return EncodeFile(geometry, filename, bin_filename, dir_path); +} + +template +Status GltfEncoder::EncodeFile(const T &geometry, const std::string &filename, + const std::string &bin_filename) { + if (filename.empty()) { + return Status(Status::DRACO_ERROR, "Output parameter is empty."); + } + + std::string dir_path; + std::string basename; + draco::SplitPath(filename, &dir_path, &basename); + return EncodeFile(geometry, filename, bin_filename, dir_path); +} + +template +Status GltfEncoder::EncodeFile(const T &geometry, const std::string &filename, + const std::string &bin_filename, + const std::string &resource_dir) { + if (filename.empty() || bin_filename.empty() || resource_dir.empty()) { + return Status(Status::DRACO_ERROR, "Output parameter is empty."); + } + const std::string extension = LowercaseFileExtension(filename); + if (extension != "gltf" && extension != "glb") { + return Status(Status::DRACO_ERROR, + "gltf_encoder only supports .gltf or .glb output."); + } + + GltfAsset gltf_asset; + gltf_asset.set_output_type(output_type_); + + if (extension == "gltf") { + std::string bin_path; + std::string bin_basename; + draco::SplitPath(bin_filename, &bin_path, &bin_basename); + gltf_asset.buffer_name(bin_basename); + } else { + gltf_asset.buffer_name(""); + gltf_asset.set_add_images_to_buffer(true); + } + + // Encode the geometry into a buffer. + EncoderBuffer buffer; + DRACO_RETURN_IF_ERROR(EncodeToBuffer(geometry, &gltf_asset, &buffer)); + if (extension == "glb") { + return WriteGlbFile(gltf_asset, buffer, filename); + } + return WriteGltfFiles(gltf_asset, buffer, filename, bin_filename, + resource_dir); +} + +template +Status GltfEncoder::EncodeToBuffer(const T &geometry, + EncoderBuffer *out_buffer) { + GltfAsset gltf_asset; + gltf_asset.set_output_type(output_type_); + gltf_asset.buffer_name(""); + gltf_asset.set_add_images_to_buffer(true); + + // Encode the geometry into a buffer. + EncoderBuffer buffer; + DRACO_RETURN_IF_ERROR(EncodeToBuffer(geometry, &gltf_asset, &buffer)); + + // Define a function for concatenating GLB file chunks into a single buffer. + const auto encode_chunk_to_buffer = + [&out_buffer](const EncoderBuffer &chunk) -> Status { + if (!out_buffer->Encode(chunk.data(), chunk.size())) { + return Status(Status::DRACO_ERROR, "Error writing to buffer."); + } + return OkStatus(); + }; + + // Create GLB file chunks and concatenate them to a single buffer. + return ProcessGlbFileChunks(gltf_asset, buffer, encode_chunk_to_buffer); +} + +// Explicit instantiation for Mesh and Scene. +template bool GltfEncoder::EncodeToFile(const Mesh &geometry, + const std::string &file_name, + const std::string &base_dir); +template bool GltfEncoder::EncodeToFile(const Scene &geometry, + const std::string &file_name, + const std::string &base_dir); +template Status GltfEncoder::EncodeFile(const Mesh &geometry, + const std::string &filename); +template Status GltfEncoder::EncodeFile(const Scene &geometry, + const std::string &filename); +template Status GltfEncoder::EncodeFile(const Mesh &geometry, + const std::string &filename, + const std::string &bin_filename); +template Status GltfEncoder::EncodeFile(const Scene &geometry, + const std::string &filename, + const std::string &bin_filename); +template Status GltfEncoder::EncodeFile(const Mesh &geometry, + const std::string &filename, + const std::string &bin_filename, + const std::string &resource_dir); +template Status GltfEncoder::EncodeFile(const Scene &geometry, + const std::string &filename, + const std::string &bin_filename, + const std::string &resource_dir); +template Status GltfEncoder::EncodeToBuffer(const Mesh &geometry, + EncoderBuffer *out_buffer); +template Status GltfEncoder::EncodeToBuffer(const Scene &geometry, + EncoderBuffer *out_buffer); + +Status GltfEncoder::EncodeToBuffer(const Mesh &mesh, GltfAsset *gltf_asset, + EncoderBuffer *out_buffer) { + out_buffer_ = out_buffer; + SetJsonWriterMode(gltf_asset); + if (!gltf_asset->AddDracoMesh(mesh)) { + return Status(Status::DRACO_ERROR, "Error adding Draco mesh."); + } + return gltf_asset->Output(out_buffer); +} + +Status GltfEncoder::EncodeToBuffer(const Scene &scene, GltfAsset *gltf_asset, + EncoderBuffer *out_buffer) { + out_buffer_ = out_buffer; + SetJsonWriterMode(gltf_asset); + DRACO_RETURN_IF_ERROR(gltf_asset->AddScene(scene)); + return gltf_asset->Output(out_buffer); +} + +void GltfEncoder::SetJsonWriterMode(class GltfAsset *gltf_asset) { + if (gltf_asset->output_type() == COMPACT && + gltf_asset->add_images_to_buffer()) { + gltf_asset->set_json_output_mode(JsonWriter::COMPACT); + } else { + gltf_asset->set_json_output_mode(JsonWriter::READABLE); + } +} + +Status GltfEncoder::WriteGltfFiles(const GltfAsset &gltf_asset, + const EncoderBuffer &buffer, + const std::string &filename, + const std::string &bin_filename, + const std::string &resource_dir) { + std::unique_ptr file = + FileWriterFactory::OpenWriter(filename); + if (!file) { + return Status(Status::DRACO_ERROR, "Output glTF file could not be opened."); + } + std::unique_ptr bin_file = + FileWriterFactory::OpenWriter(bin_filename); + if (!bin_file) { + return Status(Status::DRACO_ERROR, + "Output glTF bin file could not be opened."); + } + + // Write the glTF data into the file. + if (!file->Write(buffer.data(), buffer.size())) { + return Status(Status::DRACO_ERROR, "Error writing to glTF file."); + } + + // Write the glTF buffer into the file. + if (!bin_file->Write(gltf_asset.Buffer()->data(), + gltf_asset.Buffer()->size())) { + return Status(Status::DRACO_ERROR, "Error writing to glTF bin file."); + } + + for (int i = 0; i < gltf_asset.NumImages(); ++i) { + const std::string name = resource_dir + "/" + gltf_asset.image_name(i); + const GltfImage *const image = gltf_asset.GetImage(i); + if (!image) { + return Status(Status::DRACO_ERROR, "Error getting glTF image."); + } + DRACO_RETURN_IF_ERROR(WriteTextureToFile(name, *image->texture)); + } + return OkStatus(); +} + +Status GltfEncoder::WriteGlbFile(const GltfAsset &gltf_asset, + const EncoderBuffer &json_data, + const std::string &filename) { + std::unique_ptr file = + FileWriterFactory::OpenWriter(filename); + if (!file) { + return Status(Status::DRACO_ERROR, "Output glb file could not be opened."); + } + + // Define a function for writing GLB file chunks to |file|. + const auto write_chunk_to_file = + [&file](const EncoderBuffer &chunk) -> Status { + if (!file->Write(chunk.data(), chunk.size())) { + return Status(Status::DRACO_ERROR, "Error writing to glb file."); + } + return OkStatus(); + }; + + // Create GLB file chunks and write them to file. + return ProcessGlbFileChunks(gltf_asset, json_data, write_chunk_to_file); +} + +Status GltfEncoder::ProcessGlbFileChunks( + const class GltfAsset &gltf_asset, const EncoderBuffer &json_data, + const std::function &process_chunk) const { + // The json data must be padded so the next chunk starts on a 4-byte boundary. + const uint32_t json_pad_length = + (json_data.size() % 4) ? 4 - json_data.size() % 4 : 0; + const uint32_t json_length = json_data.size() + json_pad_length; + const uint32_t total_length = + 12 + 8 + json_length + 8 + gltf_asset.Buffer()->size(); + + EncoderBuffer header; + // Write the glb file header. + const uint32_t gltf_version = 2; + if (!header.Encode("glTF", 4)) { + return Status(Status::DRACO_ERROR, "Error writing to glb file."); + } + if (!header.Encode(gltf_version)) { + return Status(Status::DRACO_ERROR, "Error writing to glb file."); + } + if (!header.Encode(total_length)) { + return Status(Status::DRACO_ERROR, "Error writing to glb file."); + } + + // Write the JSON chunk. + const uint32_t json_chunk_type = 0x4E4F534A; + if (!header.Encode(json_length)) { + return Status(Status::DRACO_ERROR, "Error writing to glb file."); + } + if (!header.Encode(json_chunk_type)) { + return Status(Status::DRACO_ERROR, "Error writing to glb file."); + } + DRACO_RETURN_IF_ERROR(process_chunk(header)); + DRACO_RETURN_IF_ERROR(process_chunk(json_data)); + + // Pad the data if needed. + header.Clear(); + if (json_pad_length > 0) { + if (!header.Encode(" ", json_pad_length)) { + return Status(Status::DRACO_ERROR, "Error writing to glb file."); + } + } + + // Write the binary buffer chunk. + const uint32_t bin_chunk_type = 0x004E4942; + const uint32_t gltf_bin_size = gltf_asset.Buffer()->size(); + if (!header.Encode(gltf_bin_size)) { + return Status(Status::DRACO_ERROR, "Error writing to glb file."); + } + if (!header.Encode(bin_chunk_type)) { + return Status(Status::DRACO_ERROR, "Error writing to glb file."); + } + DRACO_RETURN_IF_ERROR(process_chunk(header)); + DRACO_RETURN_IF_ERROR(process_chunk(*gltf_asset.Buffer())); + return OkStatus(); +} + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED diff --git a/contrib/draco/src/draco/io/gltf_encoder.h b/contrib/draco/src/draco/io/gltf_encoder.h new file mode 100644 index 000000000..16403ac31 --- /dev/null +++ b/contrib/draco/src/draco/io/gltf_encoder.h @@ -0,0 +1,134 @@ +// Copyright 2018 The Draco Authors. +// +// 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 DRACO_IO_GLTF_ENCODER_H_ +#define DRACO_IO_GLTF_ENCODER_H_ + +#include "draco/draco_features.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED +#include +#include +#include +#include +#include +#include + +#include "draco/core/encoder_buffer.h" +#include "draco/io/file_writer_factory.h" +#include "draco/io/file_writer_interface.h" +#include "draco/io/texture_io.h" +#include "draco/mesh/mesh.h" +#include "draco/scene/scene.h" + +namespace draco { + +// Class for encoding draco::Mesh into the glTF file format. +class GltfEncoder { + public: + // Types of output modes for the glTF data encoder. |COMPACT| will output + // required and non-default glTF data. |VERBOSE| will output required and + // default glTF data as well as readable JSON even when the output is saved in + // a glTF-Binary file. + enum OutputType { COMPACT, VERBOSE }; + + GltfEncoder(); + + // Encodes the geometry and saves it into a file. Returns false when either + // the encoding failed or when the file couldn't be opened. + template + bool EncodeToFile(const T &geometry, const std::string &file_name, + const std::string &base_dir); + + // Saves |geometry| into glTF 2.0 format. |filename| is the name of the + // glTF file. The glTF bin file (if needed) will be named stem(|filename|) + + // “.bin”. The other files (if needed) will be saved to basedir(|filename|). + // If |filename| has the extension "glb" then |filename| will be written as a + // glTF-Binary file. Otherwise |filename| will be written as non-binary glTF + // file. + template + Status EncodeFile(const T &geometry, const std::string &filename); + + // Saves |geometry| into glTF 2.0 format. |filename| is the name of the + // glTF file. |bin_filename| is the name of the glTF bin file. The other + // files (if needed) will be saved to basedir(|filename|). |bin_filename| will + // be ignored if output is glTF-Binary. + template + Status EncodeFile(const T &geometry, const std::string &filename, + const std::string &bin_filename); + + // Saves |geometry| into glTF 2.0 format. |filename| is the name of the + // glTF file. |bin_filename| is the name of the glTF bin file. The other + // files will be saved to |resource_dir|. |bin_filename| and |resource_dir| + // will be ignored if output is glTF-Binary. + template + Status EncodeFile(const T &geometry, const std::string &filename, + const std::string &bin_filename, + const std::string &resource_dir); + + // Encodes |geometry| to |out_buffer| in glTF 2.0 GLB format. + template + Status EncodeToBuffer(const T &geometry, EncoderBuffer *out_buffer); + + void set_output_type(OutputType type) { output_type_ = type; } + OutputType output_type() const { return output_type_; } + + // The name of the attribute metadata that contains the glTF attribute + // name. For application-specific generic attributes, if the metadata for + // an attribute contains this key, then the value will be used as the + // encoded attribute name in the output GLTF. + static const char kDracoMetadataGltfAttributeName[]; + + private: + // Encodes the mesh or the point cloud into a buffer. + Status EncodeToBuffer(const Mesh &mesh, class GltfAsset *gltf_asset, + EncoderBuffer *out_buffer); + Status EncodeToBuffer(const Scene &scene, class GltfAsset *gltf_asset, + EncoderBuffer *out_buffer); + + // Sets appropriate Json writer mode based on the provided |gltf_asset| + // options. + static void SetJsonWriterMode(class GltfAsset *gltf_asset); + + // Writes the ".gltf" and associted files. |gltf_asset| holds the glTF data. + // |buffer| is the encoded glTF json data. |filename| is the name of the + // ".gltf" file. |bin_filename| is the name of the glTF bin file. The other + // files will be saved to |resource_dir|. + Status WriteGltfFiles(const class GltfAsset &gltf_asset, + const EncoderBuffer &buffer, + const std::string &filename, + const std::string &bin_filename, + const std::string &resource_dir); + + // Writes the ".glb" file. |gltf_asset| holds the glTF data. |json_data| is + // the encoded glTF json data. |filename| is the name of the ".glb" file. + Status WriteGlbFile(const class GltfAsset &gltf_asset, + const EncoderBuffer &json_data, + const std::string &filename); + + // Creates GLB file chunks and passes them to |process_chunk| function for + // processing. |gltf_asset| holds the glTF data. |json_data| is the encoded + // glTF json data. + Status ProcessGlbFileChunks( + const class GltfAsset &gltf_asset, const EncoderBuffer &json_data, + const std::function &process_chunk) const; + + EncoderBuffer *out_buffer_; + OutputType output_type_; +}; + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED +#endif // DRACO_IO_GLTF_ENCODER_H_ diff --git a/contrib/draco/src/draco/io/gltf_encoder_test.cc b/contrib/draco/src/draco/io/gltf_encoder_test.cc new file mode 100644 index 000000000..179256dfd --- /dev/null +++ b/contrib/draco/src/draco/io/gltf_encoder_test.cc @@ -0,0 +1,1717 @@ +// Copyright 2018 The Draco Authors. +// +// 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 "draco/io/gltf_encoder.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED +#include +#include +#include +#include + +#include "draco/core/draco_test_base.h" +#include "draco/core/draco_test_utils.h" +#include "draco/io/file_reader_factory.h" +#include "draco/io/file_reader_interface.h" +#include "draco/io/file_utils.h" +#include "draco/io/gltf_decoder.h" +#include "draco/io/gltf_test_helper.h" +#include "draco/io/parser_utils.h" +#include "draco/io/texture_io.h" +#include "draco/material/material_utils.h" +#include "draco/mesh/mesh_utils.h" +#include "draco/scene/mesh_group.h" +#include "draco/scene/scene.h" +#include "draco/scene/scene_utils.h" +#include "draco/texture/texture_utils.h" + +namespace draco { + +namespace { +std::unique_ptr DecodeFullPathGltfFileToScene( + const std::string &file_name) { + GltfDecoder decoder; + + auto maybe_scene = decoder.DecodeFromFileToScene(file_name); + if (!maybe_scene.ok()) { + std::cout << maybe_scene.status().error_msg_string() << std::endl; + return nullptr; + } + std::unique_ptr scene = std::move(maybe_scene).value(); + return scene; +} + +std::unique_ptr DecodeTestGltfFileToScene(const std::string &file_name) { + const std::string path = GetTestFileFullPath(file_name); + return DecodeFullPathGltfFileToScene(path); +} +} // namespace + +class GltfEncoderTest : public ::testing::Test { + protected: + // This function searches for the |search| string and checks that there are at + // least |count| occurrences. + void CheckGltfFileAtLeastStringCount(const std::string &gltf_file, + const std::string &search, int count) { + std::vector data; + ASSERT_TRUE(ReadFileToBuffer(gltf_file, &data)); + + draco::DecoderBuffer buffer; + buffer.Init(data.data(), data.size()); + + int strings_found = 0; + do { + std::string gltf_line; + draco::parser::ParseLine(&buffer, &gltf_line); + if (gltf_line.empty()) { + break; + } + + if (gltf_line.find(search) != std::string::npos) { + strings_found++; + } + // No need to keep counting pass |count|. + } while (strings_found < count); + ASSERT_GE(strings_found, count); + } + + // This function searches for the |search| string and checks that there no + // occurrences. + void CheckGltfFileNoString(const std::string &gltf_file, + const std::string &search) { + std::vector data; + ASSERT_TRUE(ReadFileToBuffer(gltf_file, &data)); + + draco::DecoderBuffer buffer; + buffer.Init(data.data(), data.size()); + + do { + std::string gltf_line; + draco::parser::ParseLine(&buffer, &gltf_line); + if (gltf_line.empty()) { + break; + } + ASSERT_TRUE(gltf_line.find(search) == std::string::npos); + } while (true); + } + + void CheckAnimationAccessors(const Scene &scene, + int expected_num_input_accessors, + int expected_num_output_accessors) { + int num_input_accessors = 0; + int num_output_accessors = 0; + + for (int i = 0; i < scene.NumAnimations(); ++i) { + const Animation *anim = scene.GetAnimation(AnimationIndex(i)); + ASSERT_NE(anim, nullptr); + + // The animation accessors in Draco are relative to the Animation object. + // While in glTF the animation accessors are relative to the global + // accessors. + std::unordered_set seen_accessors; + + for (int j = 0; j < anim->NumSamplers(); ++j) { + const AnimationSampler *const sampler = anim->GetSampler(j); + ASSERT_NE(sampler, nullptr); + + if (seen_accessors.find(sampler->input_index) == seen_accessors.end()) { + seen_accessors.insert(sampler->input_index); + num_input_accessors++; + } + if (seen_accessors.find(sampler->output_index) == + seen_accessors.end()) { + seen_accessors.insert(sampler->output_index); + num_output_accessors++; + } + } + } + + EXPECT_EQ(expected_num_input_accessors, num_input_accessors); + EXPECT_EQ(expected_num_output_accessors, num_output_accessors); + } + + void CompareMeshes(const Mesh *mesh0, const Mesh *mesh1) { + ASSERT_EQ(mesh0->num_faces(), mesh1->num_faces()); + ASSERT_EQ(mesh0->num_attributes(), mesh1->num_attributes()); + for (int att_id = 0; att_id < mesh0->num_attributes(); ++att_id) { + const GeometryAttribute::Type att_type = + mesh0->attribute(att_id)->attribute_type(); + const PointAttribute *const att = mesh1->GetNamedAttribute(att_type); + ASSERT_EQ(mesh0->attribute(att_id)->size(), att->size()) + << "Attribute id:" << att_id << " is not equal."; + } + + // Check materials are the same. + if (mesh0->GetMaterialLibrary().NumMaterials() == 0) { + // We add a default material if the source had no materials. + ASSERT_EQ(mesh1->GetMaterialLibrary().NumMaterials(), 1); + } else if (mesh1->GetMaterialLibrary().NumMaterials() == 0) { + // We add a default material if the source had no materials. + ASSERT_EQ(mesh0->GetMaterialLibrary().NumMaterials(), 1); + } else { + ASSERT_EQ(mesh0->GetMaterialLibrary().NumMaterials(), + mesh1->GetMaterialLibrary().NumMaterials()); + for (int i = 0; i < mesh0->GetMaterialLibrary().NumMaterials(); ++i) { + ASSERT_EQ(mesh0->GetMaterialLibrary().GetMaterial(i)->NumTextureMaps(), + mesh1->GetMaterialLibrary().GetMaterial(i)->NumTextureMaps()); + ASSERT_EQ(mesh0->GetMaterialLibrary().GetMaterial(i)->GetName(), + mesh1->GetMaterialLibrary().GetMaterial(i)->GetName()); + } + } + } + + void CompareScenes(const Scene *scene0, const Scene *scene1) { + ASSERT_EQ(scene0->NumMeshes(), scene1->NumMeshes()); + ASSERT_EQ(scene0->NumMeshGroups(), scene1->NumMeshGroups()); + ASSERT_EQ(scene0->NumNodes(), scene1->NumNodes()); + ASSERT_EQ(scene0->GetMaterialLibrary().NumMaterials(), + scene1->GetMaterialLibrary().NumMaterials()); + ASSERT_EQ(scene0->NumAnimations(), scene1->NumAnimations()); + ASSERT_EQ(scene0->NumSkins(), scene1->NumSkins()); + ASSERT_EQ(scene0->NumLights(), scene1->NumLights()); + + // Check materials are the same. + for (int i = 0; i < scene0->GetMaterialLibrary().NumMaterials(); ++i) { + ASSERT_EQ(scene0->GetMaterialLibrary().GetMaterial(i)->NumTextureMaps(), + scene1->GetMaterialLibrary().GetMaterial(i)->NumTextureMaps()); + ASSERT_EQ(scene0->GetMaterialLibrary().GetMaterial(i)->GetName(), + scene1->GetMaterialLibrary().GetMaterial(i)->GetName()); + } + + // Check that materials variants names are the same. + ASSERT_EQ(scene0->GetMaterialLibrary().NumMaterialsVariants(), + scene1->GetMaterialLibrary().NumMaterialsVariants()); + for (int i = 0; i < scene0->GetMaterialLibrary().NumMaterialsVariants(); + i++) { + ASSERT_EQ(scene0->GetMaterialLibrary().GetMaterialsVariantName(i), + scene1->GetMaterialLibrary().GetMaterialsVariantName(i)); + } + + // Check Nodes are the same. + for (draco::SceneNodeIndex i(0); i < scene0->NumNodes(); ++i) { + const SceneNode *const scene_node0 = scene0->GetNode(i); + const SceneNode *const scene_node1 = scene1->GetNode(i); + ASSERT_NE(scene_node0, nullptr); + ASSERT_NE(scene_node1, nullptr); + ASSERT_EQ(scene_node0->GetName(), scene_node1->GetName()); + ASSERT_EQ(scene_node0->GetLightIndex(), scene_node1->GetLightIndex()); + } + + // Check MeshGroups are the same. + for (draco::MeshGroupIndex i(0); i < scene0->NumMeshGroups(); ++i) { + const MeshGroup *const mesh_group0 = scene0->GetMeshGroup(i); + const MeshGroup *const mesh_group1 = scene1->GetMeshGroup(i); + ASSERT_NE(mesh_group0, nullptr); + ASSERT_NE(mesh_group1, nullptr); + ASSERT_EQ(mesh_group0->GetName(), mesh_group1->GetName()); + ASSERT_EQ(mesh_group0->NumMeshInstances(), + mesh_group1->NumMeshInstances()); + + // Check that mesh instanes are the same. + for (int j = 0; j < mesh_group1->NumMeshInstances(); j++) { + const MeshGroup::MeshInstance &instance0 = + mesh_group0->GetMeshInstance(j); + const MeshGroup::MeshInstance &instance1 = + mesh_group1->GetMeshInstance(j); + ASSERT_EQ(instance0.mesh_index, instance1.mesh_index); + ASSERT_EQ(instance0.material_index, instance1.material_index); + ASSERT_EQ(instance0.materials_variants_mappings.size(), + instance1.materials_variants_mappings.size()); + + // Check that materials variants mappings are the same. + for (int k = 0; k < instance0.materials_variants_mappings.size(); k++) { + const MeshGroup::MaterialsVariantsMapping &mapping0 = + instance0.materials_variants_mappings[k]; + const MeshGroup::MaterialsVariantsMapping &mapping1 = + instance1.materials_variants_mappings[k]; + ASSERT_EQ(mapping0.material, mapping1.material); + ASSERT_EQ(mapping0.variants.size(), mapping1.variants.size()); + for (int l = 0; l < mapping0.variants.size(); l++) { + ASSERT_EQ(mapping0.variants[l], mapping1.variants[l]); + } + } + } + } + + // Check Animations are the same. + for (draco::AnimationIndex i(0); i < scene0->NumAnimations(); ++i) { + const Animation *const animation0 = scene0->GetAnimation(i); + const Animation *const animation1 = scene1->GetAnimation(i); + ASSERT_NE(animation0, nullptr); + ASSERT_NE(animation1, nullptr); + ASSERT_EQ(animation0->NumSamplers(), animation1->NumSamplers()); + ASSERT_EQ(animation0->NumChannels(), animation1->NumChannels()); + ASSERT_EQ(animation0->NumNodeAnimationData(), + animation1->NumNodeAnimationData()); + } + + // Check that lights are the same. + for (draco::LightIndex i(0); i < scene0->NumLights(); ++i) { + const Light *const light0 = scene0->GetLight(i); + const Light *const light1 = scene1->GetLight(i); + ASSERT_NE(light0, nullptr); + ASSERT_NE(light1, nullptr); + ASSERT_EQ(light0->GetName(), light1->GetName()); + ASSERT_EQ(light0->GetColor(), light1->GetColor()); + ASSERT_EQ(light0->GetIntensity(), light1->GetIntensity()); + ASSERT_EQ(light0->GetType(), light1->GetType()); + ASSERT_EQ(light0->GetRange(), light1->GetRange()); + if (light0->GetType() == Light::SPOT) { + ASSERT_EQ(light0->GetInnerConeAngle(), light1->GetInnerConeAngle()); + ASSERT_EQ(light0->GetOuterConeAngle(), light1->GetOuterConeAngle()); + } + } + } + + void EncodeMeshToFile(const Mesh &mesh, + const std::string &gltf_file_full_path) { + std::string folder_path; + std::string gltf_file_name; + draco::SplitPath(gltf_file_full_path, &folder_path, &gltf_file_name); + GltfEncoder gltf_encoder; + ASSERT_TRUE( + gltf_encoder.EncodeToFile(mesh, gltf_file_full_path, folder_path)) + << "Failed gltf_file_full_path:" << gltf_file_full_path + << " folder_path:" << folder_path; + } + + void EncodeSceneToFile(const Scene &scene, + const std::string &gltf_file_full_path) { + std::string folder_path; + std::string gltf_file_name; + draco::SplitPath(gltf_file_full_path, &folder_path, &gltf_file_name); + GltfEncoder gltf_encoder; + ASSERT_TRUE(gltf_encoder.EncodeToFile(scene, gltf_file_full_path, + folder_path)) + << "Failed gltf_file_full_path:" << gltf_file_full_path + << " folder_path:" << folder_path; + } + + // Encode |mesh| to a temporary glTF file. Then decode the glTF file and + // return the mesh in |mesh_gltf|. + void MeshToDecodedGltfMesh(const Mesh &mesh, + std::unique_ptr *mesh_gltf) { + const std::string gltf_file_full_path = + draco::GetTestTempFileFullPath("test.gltf"); + EncodeMeshToFile(mesh, gltf_file_full_path); + *mesh_gltf = std::move(ReadMeshFromFile(gltf_file_full_path)).value(); + ASSERT_NE(*mesh_gltf, nullptr); + } + + // Encode |mesh| to a temporary glTF file. Then decode the glTF file as a + // scene and return it in |scene_gltf|. + void MeshToDecodedGltfScene(const Mesh &mesh, + std::unique_ptr *scene_gltf) { + const std::string gltf_file_full_path = + draco::GetTestTempFileFullPath("test.gltf"); + EncodeMeshToFile(mesh, gltf_file_full_path); + *scene_gltf = std::move(ReadSceneFromFile(gltf_file_full_path)).value(); + ASSERT_NE(*scene_gltf, nullptr); + } + + // Encode |scene| to a temporary glTF file. Then decode the glTF file and + // return the scene in |scene_gltf|. + void SceneToDecodedGltfScene(const Scene &scene, + const std::string &temp_basename, + std::unique_ptr *scene_gltf) { + const std::string gltf_file_full_path = + draco::GetTestTempFileFullPath(temp_basename); + EncodeSceneToFile(scene, gltf_file_full_path); + + *scene_gltf = DecodeFullPathGltfFileToScene(gltf_file_full_path); + if (SceneUtils::IsDracoCompressionEnabled(scene)) { + // Two occurrences of the Draco compression string is the least amount for + // a valid Draco compressed glTF file. + const std::string khr_draco_compression = "KHR_draco_mesh_compression"; + CheckGltfFileAtLeastStringCount(gltf_file_full_path, + khr_draco_compression, 2); + } + ASSERT_NE((*scene_gltf).get(), nullptr); + } + + void SceneToDecodedGltfScene(const Scene &scene, + std::unique_ptr *scene_gltf) { + SceneToDecodedGltfScene(scene, "test.gltf", scene_gltf); + } + + void EncodeMeshToGltfAndCompare(Mesh *mesh) { + ASSERT_GT(mesh->num_faces(), 0); + + std::unique_ptr mesh_from_gltf; + MeshToDecodedGltfMesh(*mesh, &mesh_from_gltf); + + mesh->DeduplicatePointIds(); + ASSERT_TRUE(mesh->DeduplicateAttributeValues()); + CompareMeshes(mesh, mesh_from_gltf.get()); + } + + void EncodeSceneToGltfAndCompare(Scene *scene) { + std::unique_ptr scene_from_gltf; + SceneToDecodedGltfScene(*scene, &scene_from_gltf); + if (!SceneUtils::IsDracoCompressionEnabled(*scene)) { + CompareScenes(scene, scene_from_gltf.get()); + } + } + + void test_encoding(const std::string &file_name) { + const std::unique_ptr mesh(ReadMeshFromTestFile(file_name, true)); + + ASSERT_NE(mesh, nullptr) << "Failed to load test model " << file_name; + EncodeMeshToGltfAndCompare(mesh.get()); + } +}; + +TEST_F(GltfEncoderTest, TestGltfEncodingAll) { + // Test decoded mesh from encoded glTF file stays the same. + test_encoding("test_nm.obj.edgebreaker.cl4.2.2.drc"); + test_encoding("cube_att.drc"); + test_encoding("car.drc"); + test_encoding("bunny_gltf.drc"); +} + +TEST_F(GltfEncoderTest, ImportTangentAttribute) { + auto mesh = draco::ReadMeshFromTestFile("sphere.gltf"); + ASSERT_NE(mesh, nullptr); + + const draco::PointAttribute *const tangent_att = + mesh->GetNamedAttribute(draco::GeometryAttribute::TANGENT); + ASSERT_NE(tangent_att, nullptr); + + std::unique_ptr mesh_from_gltf; + MeshToDecodedGltfMesh(*mesh, &mesh_from_gltf); + ASSERT_EQ(mesh->num_attributes(), mesh_from_gltf->num_attributes()); +} + +TEST_F(GltfEncoderTest, EncodeColorTexture) { + const std::string tex_file_name = draco::GetTestFileFullPath("test.png"); + std::unique_ptr texture = + draco::ReadTextureFromFile(tex_file_name).value(); + ASSERT_NE(texture, nullptr); + + std::unique_ptr mesh = + draco::ReadMeshFromTestFile("cube_att.obj"); + ASSERT_NE(mesh, nullptr); + + mesh->GetMaterialLibrary().MutableMaterial(0)->SetTextureMap( + std::move(texture), draco::TextureMap::COLOR, 0); + + EncodeMeshToGltfAndCompare(mesh.get()); +} + +TEST_F(GltfEncoderTest, EncodeColors) { + auto mesh = draco::ReadMeshFromTestFile("test_pos_color.ply"); + ASSERT_NE(mesh, nullptr); + + const draco::PointAttribute *const color_att = + mesh->GetNamedAttribute(draco::GeometryAttribute::COLOR); + ASSERT_NE(color_att, nullptr); + + std::unique_ptr mesh_from_gltf; + MeshToDecodedGltfMesh(*mesh, &mesh_from_gltf); + + ASSERT_EQ(mesh->num_faces(), mesh_from_gltf->num_faces()); + ASSERT_EQ(mesh->num_attributes(), mesh_from_gltf->num_attributes()); + ASSERT_EQ( + mesh->NumNamedAttributes(draco::GeometryAttribute::COLOR), + mesh_from_gltf->NumNamedAttributes(draco::GeometryAttribute::COLOR)); +} + +TEST_F(GltfEncoderTest, EncodeNamedGenericAttribute) { + // Load some base mesh. + auto mesh = draco::ReadMeshFromTestFile("test_generic.ply"); + ASSERT_NE(mesh, nullptr); + const draco::PointAttribute *const pos_att = + mesh->GetNamedAttribute(draco::GeometryAttribute::POSITION); + ASSERT_NE(pos_att, nullptr); + int num_vertices = pos_att->size(); + + // Add two new scalar attributes where each value corresponds to the position + // value index (vertex). The first attribute will have metadata, the second + // attribute won't. + std::unique_ptr pa_0(new draco::PointAttribute()); + std::unique_ptr pa_1(new draco::PointAttribute()); + pa_0->Init(draco::GeometryAttribute::GENERIC, /* scalar */ 1, + draco::DT_FLOAT32, false, + /* one value per position value */ num_vertices); + pa_1->Init(draco::GeometryAttribute::GENERIC, /* scalar */ 1, + draco::DT_FLOAT32, false, + /* one value per position value */ num_vertices); + + // Set the values for the new attributes. + for (draco::AttributeValueIndex avi(0); avi < num_vertices; ++avi) { + const float att_value = avi.value(); + pa_0->SetAttributeValue(avi, &att_value); + pa_1->SetAttributeValue(avi, &att_value); + } + + // Add the attribute to the existing mesh. + const int new_att_id_0 = mesh->AddPerVertexAttribute(std::move(pa_0)); + const int new_att_id_1 = mesh->AddPerVertexAttribute(std::move(pa_1)); + ASSERT_NE(new_att_id_0, -1); + ASSERT_NE(new_att_id_1, -1); + + // Set metadata for first attribute so it gets written out by glTF encoder. + std::unique_ptr am(new draco::AttributeMetadata()); + constexpr char kAttributeName[] = "MyAttributeName"; + constexpr char kDracoMetadataGltfAttributeName[] = + "//GLTF/ApplicationSpecificAttributeName"; + am->AddEntryString(kDracoMetadataGltfAttributeName, kAttributeName); + mesh->AddAttributeMetadata(new_att_id_0, std::move(am)); + + // Make sure the GLTF contains a reference to the named attribute. + const std::string gltf_file_full_path = + draco::GetTestTempFileFullPath("GenericAttribute.gltf"); + std::string folder_path; + std::string gltf_file_name; + draco::SplitPath(gltf_file_full_path, &folder_path, &gltf_file_name); + GltfEncoder gltf_encoder; + ASSERT_TRUE(gltf_encoder.EncodeToFile(*(mesh.get()), + gltf_file_full_path, folder_path)) + << "Failed gltf_file_full_path:" << gltf_file_full_path + << " folder_path:" << folder_path; + CheckGltfFileAtLeastStringCount(gltf_file_full_path, kAttributeName, 1); + + // The decoder does not yet support generic attribute names, so instead of + // using the decoder we compare against a golden file. + const std::string gltf_generated_bin_filename = + draco::GetTestTempFileFullPath("buffer0.bin"); + std::vector generated_buffer; + ASSERT_TRUE(ReadFileToBuffer(gltf_generated_bin_filename, &generated_buffer)); + std::string generated_str(generated_buffer.data(), generated_buffer.size()); + + const std::string gltf_expected_bin_filename = + GetTestFileFullPath("test_generic_golden.bin"); + const bool kUpdateGoldens = false; + if (kUpdateGoldens) { + ASSERT_TRUE(WriteBufferToFile(generated_buffer.data(), + generated_buffer.size(), + gltf_expected_bin_filename)); + } + std::vector expected_buffer; + ASSERT_TRUE(ReadFileToBuffer(gltf_expected_bin_filename, &expected_buffer)); + std::string expected_str(expected_buffer.data(), expected_buffer.size()); + + EXPECT_TRUE(generated_str == expected_str); +} + +TEST_F(GltfEncoderTest, EncodeMetallicRoughnessTexture) { + const std::string tex_file_name = draco::GetTestFileFullPath("test.png"); + std::unique_ptr texture = + draco::ReadTextureFromFile(tex_file_name).value(); + ASSERT_NE(texture, nullptr); + + std::unique_ptr mesh = + draco::ReadMeshFromTestFile("cube_att.obj"); + ASSERT_NE(mesh, nullptr); + + mesh->GetMaterialLibrary().MutableMaterial(0)->SetTextureMap( + std::move(texture), draco::TextureMap::METALLIC_ROUGHNESS, 0); + + EncodeMeshToGltfAndCompare(mesh.get()); +} + +TEST_F(GltfEncoderTest, EncodeOcclusionTexture) { + const std::string tex_file_name = draco::GetTestFileFullPath("test.png"); + std::unique_ptr texture = + draco::ReadTextureFromFile(tex_file_name).value(); + ASSERT_NE(texture, nullptr); + + std::unique_ptr mesh = + draco::ReadMeshFromTestFile("cube_att.obj"); + ASSERT_NE(mesh, nullptr); + + mesh->GetMaterialLibrary().MutableMaterial(0)->SetTextureMap( + std::move(texture), draco::TextureMap::AMBIENT_OCCLUSION, 0); + + EncodeMeshToGltfAndCompare(mesh.get()); +} + +TEST_F(GltfEncoderTest, EncodeEmissiveTexture) { + const std::string tex_file_name = draco::GetTestFileFullPath("test.png"); + std::unique_ptr texture = + draco::ReadTextureFromFile(tex_file_name).value(); + ASSERT_NE(texture, nullptr); + + std::unique_ptr mesh = + draco::ReadMeshFromTestFile("cube_att.obj"); + ASSERT_NE(mesh, nullptr); + + mesh->GetMaterialLibrary().MutableMaterial(0)->SetTextureMap( + std::move(texture), draco::TextureMap::EMISSIVE, 0); + + EncodeMeshToGltfAndCompare(mesh.get()); +} + +// Tests splitting the mesh into multiple primitives. +TEST_F(GltfEncoderTest, EncodeSplitMesh) { + std::unique_ptr mesh = + draco::ReadMeshFromTestFile("CesiumMilkTruck/glTF/CesiumMilkTruck.gltf"); + ASSERT_NE(mesh, nullptr); + const int32_t material_att_id = + mesh->GetNamedAttributeId(draco::GeometryAttribute::MATERIAL); + ASSERT_NE(material_att_id, -1); + EncodeMeshToGltfAndCompare(mesh.get()); +} + +// Tests encoding a scene from a glTF with multiple meshes and primitives, +// including mesh instances. +TEST_F(GltfEncoderTest, EncodeInstancedScene) { + const std::string file_name = "CesiumMilkTruck/glTF/CesiumMilkTruck.gltf"; + const std::unique_ptr scene(DecodeTestGltfFileToScene(file_name)); + ASSERT_NE(scene, nullptr); + + std::unique_ptr transcoded_scene; + SceneToDecodedGltfScene(*scene, "EncodeInstancedScene.gltf", + &transcoded_scene); + ASSERT_NE(transcoded_scene, nullptr); + CompareScenes(scene.get(), transcoded_scene.get()); + EXPECT_EQ(transcoded_scene->NumAnimations(), 1); + + const int num_input_accessors = 2; + const int num_output_accessors = 2; + CheckAnimationAccessors(*transcoded_scene, num_input_accessors, + num_output_accessors); +} + +// Tests encoding a scene from a glTF with multiple meshes and primitives, +// including mesh instances. +TEST_F(GltfEncoderTest, EncodeBoneAnimation) { + const std::string file_name = "CesiumMan/glTF/CesiumMan.gltf"; + const std::unique_ptr scene(DecodeTestGltfFileToScene(file_name)); + ASSERT_NE(scene, nullptr); + + std::unique_ptr transcoded_scene; + SceneToDecodedGltfScene(*scene, "EncodeBoneAnimation.gltf", + &transcoded_scene); + ASSERT_NE(transcoded_scene, nullptr); + CompareScenes(scene.get(), transcoded_scene.get()); + EXPECT_EQ(transcoded_scene->NumAnimations(), 1); + + const Animation *anim = scene->GetAnimation(AnimationIndex(0)); + ASSERT_NE(anim, nullptr); + ASSERT_TRUE(anim->GetName().empty()); + + // TODO(b/145703399): Figure out how to test that all of the input accessors + // in animation channels in the encoded glTF file will be the same for this + // test file. + const int num_input_accessors = 57; + const int num_output_accessors = 57; + CheckAnimationAccessors(*transcoded_scene, num_input_accessors, + num_output_accessors); +} + +// Tests encoding a scene from a glTF with nodes that have names. +TEST_F(GltfEncoderTest, EncodeSceneWithNodeNames) { + const std::string file_name = "Lantern/glTF/Lantern.gltf"; + const std::unique_ptr scene(DecodeTestGltfFileToScene(file_name)); + ASSERT_NE(scene, nullptr); + EncodeSceneToGltfAndCompare(scene.get()); +} + +// Tests encoding a simple glTF with Draco compression. +TEST_F(GltfEncoderTest, EncodeWithDracoCompression) { + const std::string file_name = "Box/glTF/Box.gltf"; + const std::unique_ptr scene(DecodeTestGltfFileToScene(file_name)); + ASSERT_NE(scene, nullptr); + const DracoCompressionOptions options; + SceneUtils::SetDracoCompressionOptions(&options, scene.get()); + EncodeSceneToGltfAndCompare(scene.get()); +} + +TEST_F(GltfEncoderTest, EncodeWeightsJointsWithDracoCompression) { + const std::string file_name = "CesiumMan/glTF/CesiumMan.gltf"; + const std::unique_ptr scene(DecodeTestGltfFileToScene(file_name)); + ASSERT_NE(scene, nullptr); + const DracoCompressionOptions options; + SceneUtils::SetDracoCompressionOptions(&options, scene.get()); + EncodeSceneToGltfAndCompare(scene.get()); +} + +TEST_F(GltfEncoderTest, EncodeTangentsWithDracoCompression) { + const std::string file_name = "Lantern/glTF/Lantern.gltf"; + const std::unique_ptr scene(DecodeTestGltfFileToScene(file_name)); + ASSERT_NE(scene, nullptr); + const DracoCompressionOptions options; + SceneUtils::SetDracoCompressionOptions(&options, scene.get()); + EncodeSceneToGltfAndCompare(scene.get()); +} + +TEST_F(GltfEncoderTest, TestDracoCompressionWithGeneratedPoints) { + const std::string basename = "test_nm.obj"; + std::unique_ptr mesh = draco::ReadMeshFromTestFile(basename); + ASSERT_NE(mesh, nullptr) << "Failed to load " << basename; + + auto maybe_scene = draco::SceneUtils::MeshToScene(std::move(mesh)); + ASSERT_TRUE(maybe_scene.ok()) << "Failed Mesh to Scene conversion."; + const std::unique_ptr scene = std::move(maybe_scene).value(); + ASSERT_NE(scene, nullptr); + const DracoCompressionOptions options; + SceneUtils::SetDracoCompressionOptions(&options, scene.get()); + EncodeSceneToGltfAndCompare(scene.get()); +} + +TEST_F(GltfEncoderTest, TestDracoCompressionWithDegenerateFaces) { + const std::string basename = "deg_faces.obj"; + std::unique_ptr mesh = draco::ReadMeshFromTestFile(basename); + ASSERT_NE(mesh, nullptr) << "Failed to load " << basename; + ASSERT_EQ(mesh->num_faces(), 4); + + auto maybe_scene = draco::SceneUtils::MeshToScene(std::move(mesh)); + ASSERT_TRUE(maybe_scene.ok()) << "Failed Mesh to Scene conversion."; + const std::unique_ptr scene = std::move(maybe_scene).value(); + ASSERT_NE(scene, nullptr); + const Mesh &scene_first_mesh = scene->GetMesh(MeshIndex(0)); + ASSERT_EQ(scene_first_mesh.num_faces(), 4); + + std::unique_ptr scene_from_gltf; + const DracoCompressionOptions options; + SceneUtils::SetDracoCompressionOptions(&options, scene.get()); + SceneToDecodedGltfScene(*scene, &scene_from_gltf); + const Mesh &scene_from_gltf_first_mesh = + scene_from_gltf->GetMesh(MeshIndex(0)); + ASSERT_EQ(scene_from_gltf_first_mesh.num_faces(), 3); + + CompareScenes(scene.get(), scene_from_gltf.get()); +} + +TEST_F(GltfEncoderTest, DracoCompressionCheckOptions) { + const std::string file_name = "CesiumMan/glTF/CesiumMan.gltf"; + const std::unique_ptr scene(DecodeTestGltfFileToScene(file_name)); + ASSERT_NE(scene, nullptr); + + const std::string gltf_file_full_path = + draco::GetTestTempFileFullPath("test.gltf"); + std::string folder_path; + std::string gltf_file_name; + draco::SplitPath(gltf_file_full_path, &folder_path, &gltf_file_name); + GltfEncoder gltf_encoder; + DracoCompressionOptions options; + SceneUtils::SetDracoCompressionOptions(&options, scene.get()); + + ASSERT_TRUE(gltf_encoder.EncodeToFile(*scene, gltf_file_full_path, + folder_path)) + << "Failed gltf_file_full_path:" << gltf_file_full_path + << " folder_path:" << folder_path; + + const std::string gltf_bin_filename = + draco::GetTestTempFileFullPath("buffer0.bin"); + const size_t default_bin_size = draco::GetFileSize(gltf_bin_filename); + + // Test applying more quantization will make the compressed size smaller. + options.quantization_position.SetQuantizationBits(6); + options.quantization_bits_normal = 6; + options.quantization_bits_tex_coord = 6; + SceneUtils::SetDracoCompressionOptions(&options, scene.get()); + + ASSERT_TRUE(gltf_encoder.EncodeToFile(*scene, gltf_file_full_path, + folder_path)) + << "Failed gltf_file_full_path:" << gltf_file_full_path + << " folder_path:" << folder_path; + const size_t more_quantization_bin_size = + draco::GetFileSize(gltf_bin_filename); + ASSERT_LT(more_quantization_bin_size, default_bin_size); + + // Test setting more weight quantization then the default makes the compressed + // size smaller. + options.quantization_bits_weight = 6; + SceneUtils::SetDracoCompressionOptions(&options, scene.get()); + + ASSERT_TRUE(gltf_encoder.EncodeToFile(*scene, gltf_file_full_path, + folder_path)) + << "Failed gltf_file_full_path:" << gltf_file_full_path + << " folder_path:" << folder_path; + const size_t more_weight_quantization_bin_size = + draco::GetFileSize(gltf_bin_filename); + ASSERT_LT(more_weight_quantization_bin_size, more_quantization_bin_size); + + options.quantization_position.SetQuantizationBits(20); + options.quantization_bits_normal = 20; + options.quantization_bits_tex_coord = 20; + options.quantization_bits_weight = 20; + SceneUtils::SetDracoCompressionOptions(&options, scene.get()); + + ASSERT_TRUE(gltf_encoder.EncodeToFile(*scene, gltf_file_full_path, + folder_path)) + << "Failed gltf_file_full_path:" << gltf_file_full_path + << " folder_path:" << folder_path; + const size_t less_quantization_bin_size = + draco::GetFileSize(gltf_bin_filename); + ASSERT_GT(less_quantization_bin_size, default_bin_size); + + DracoCompressionOptions level_options; + level_options.compression_level = 10; // compression level [0-10]. + SceneUtils::SetDracoCompressionOptions(&level_options, scene.get()); + ASSERT_TRUE(gltf_encoder.EncodeToFile(*scene, gltf_file_full_path, + folder_path)) + << "Failed gltf_file_full_path:" << gltf_file_full_path + << " folder_path:" << folder_path; + const size_t most_compression_bin_size = + draco::GetFileSize(gltf_bin_filename); + ASSERT_LT(most_compression_bin_size, default_bin_size); + + level_options.compression_level = 4; + SceneUtils::SetDracoCompressionOptions(&level_options, scene.get()); + ASSERT_TRUE(gltf_encoder.EncodeToFile(*scene, gltf_file_full_path, + folder_path)) + << "Failed gltf_file_full_path:" << gltf_file_full_path + << " folder_path:" << folder_path; + const size_t less_compression_bin_size = + draco::GetFileSize(gltf_bin_filename); + ASSERT_GT(less_compression_bin_size, default_bin_size); + + level_options.compression_level = 0; + SceneUtils::SetDracoCompressionOptions(&level_options, scene.get()); + ASSERT_TRUE(gltf_encoder.EncodeToFile(*scene, gltf_file_full_path, + folder_path)) + << "Failed gltf_file_full_path:" << gltf_file_full_path + << " folder_path:" << folder_path; + const size_t least_compression_bin_size = + draco::GetFileSize(gltf_bin_filename); + ASSERT_GT(least_compression_bin_size, less_compression_bin_size); +} + +TEST_F(GltfEncoderTest, TestQuantizationPerAttribute) { + const std::string file_name = "sphere.gltf"; + const std::unique_ptr scene(DecodeTestGltfFileToScene(file_name)); + ASSERT_NE(scene, nullptr); + + const std::string gltf_file_full_path = + draco::GetTestTempFileFullPath("test.gltf"); + std::string folder_path; + std::string gltf_file_name; + draco::SplitPath(gltf_file_full_path, &folder_path, &gltf_file_name); + GltfEncoder gltf_encoder; + DracoCompressionOptions options; + SceneUtils::SetDracoCompressionOptions(&options, scene.get()); + + ASSERT_TRUE(gltf_encoder.EncodeToFile(*scene, gltf_file_full_path, + folder_path)) + << "Failed gltf_file_full_path:" << gltf_file_full_path + << " folder_path:" << folder_path; + + const std::string gltf_bin_filename = + draco::GetTestTempFileFullPath("buffer0.bin"); + const size_t default_bin_size = draco::GetFileSize(gltf_bin_filename); + + // Test setting more position quantization then the default makes the + // compressed size smaller. + options.quantization_position.SetQuantizationBits(6); + SceneUtils::SetDracoCompressionOptions(&options, scene.get()); + ASSERT_TRUE(gltf_encoder.EncodeToFile(*scene, gltf_file_full_path, + folder_path)) + << "Failed gltf_file_full_path:" << gltf_file_full_path + << " folder_path:" << folder_path; + const size_t position_quantization_bin_size = + draco::GetFileSize(gltf_bin_filename); + ASSERT_LT(position_quantization_bin_size, default_bin_size); + + // Test setting more normal quantization then the default makes the compressed + // size smaller. + options.quantization_bits_normal = 6; + SceneUtils::SetDracoCompressionOptions(&options, scene.get()); + ASSERT_TRUE(gltf_encoder.EncodeToFile(*scene, gltf_file_full_path, + folder_path)) + << "Failed gltf_file_full_path:" << gltf_file_full_path + << " folder_path:" << folder_path; + const size_t normal_quantization_bin_size = + draco::GetFileSize(gltf_bin_filename); + ASSERT_LT(normal_quantization_bin_size, position_quantization_bin_size); + + // Test setting more tex_coord quantization then the default makes the + // compressed size smaller. + options.quantization_bits_tex_coord = 6; + SceneUtils::SetDracoCompressionOptions(&options, scene.get()); + ASSERT_TRUE(gltf_encoder.EncodeToFile(*scene, gltf_file_full_path, + folder_path)) + << "Failed gltf_file_full_path:" << gltf_file_full_path + << " folder_path:" << folder_path; + const size_t tex_coord_quantization_bin_size = + draco::GetFileSize(gltf_bin_filename); + ASSERT_LT(tex_coord_quantization_bin_size, normal_quantization_bin_size); + + // Test setting more tangent quantization then the default makes the + // compressed size smaller. Weight is tested in DracoCompressionCheckOptions. + options.quantization_bits_tangent = 6; + SceneUtils::SetDracoCompressionOptions(&options, scene.get()); + ASSERT_TRUE(gltf_encoder.EncodeToFile(*scene, gltf_file_full_path, + folder_path)) + << "Failed gltf_file_full_path:" << gltf_file_full_path + << " folder_path:" << folder_path; + const size_t tangent_quantization_bin_size = + draco::GetFileSize(gltf_bin_filename); + ASSERT_LT(tangent_quantization_bin_size, tex_coord_quantization_bin_size); +} + +// Tests encoding a glTF with multiple scaled instances with Draco compression +// using grid options for position quantization. +TEST_F(GltfEncoderTest, TestDracoCompressionWithGridOptions) { + const std::string file_name = + "SpheresScaledInstances/glTF/spheres_scaled_instances.gltf"; + std::unique_ptr scene(DecodeTestGltfFileToScene(file_name)); + ASSERT_NE(scene, nullptr); + + const auto bbox = scene->GetMesh(MeshIndex(0)).ComputeBoundingBox(); + const float mesh_size = bbox.Size().MaxCoeff(); + + // All dimensions of the original mesh are between [-1, 1]. Let's move the + // mesh to [0, 2] which will allow us to match grid quantization with the + // regular quantization (grid quantization is always aligned with 0). + Mesh &mesh = scene->GetMesh(MeshIndex(0)); + PointAttribute *pos_att = + mesh.attribute(mesh.GetNamedAttributeId(GeometryAttribute::POSITION)); + for (AttributeValueIndex avi(0); avi < pos_att->size(); ++avi) { + Vector3f pos; + pos_att->GetValue(avi, &pos[0]); + pos += Vector3f(1.f, 1.f, 1.f); + pos_att->SetAttributeValue(avi, &pos[0]); + } + + DracoCompressionOptions options; + + // First quantize the scene with 8 bits and save the result. + options.quantization_position.SetQuantizationBits(8); + SceneUtils::SetDracoCompressionOptions(&options, scene.get()); + + const std::string gltf_filename = draco::GetTestTempFileFullPath("temp.glb"); + GltfEncoder encoder; + DRACO_ASSERT_OK(encoder.EncodeFile(*scene, gltf_filename)); + // Get the size of the generated file. + const size_t qb_file_size = draco::GetFileSize(gltf_filename); + + // Now set grid quantization and ensure the encoded file size is about the + // same. The max instance scale is 3 and model size is |mesh_size| so the grid + // scale must account for that. + options.quantization_position.SetGrid(mesh_size * 3. / 255.); + SceneUtils::SetDracoCompressionOptions(&options, scene.get()); + + DRACO_ASSERT_OK(encoder.EncodeFile(*scene, gltf_filename)); + // Get the size of the generated file. + const size_t grid_file_size = draco::GetFileSize(gltf_filename); + + ASSERT_EQ(grid_file_size, qb_file_size); + + // Now set grid quantization to different settings and ensure the encoded size + // changed. We reduce spacing which should increase the size. + options.quantization_position.SetGrid(mesh_size * 3. / 511.); + SceneUtils::SetDracoCompressionOptions(&options, scene.get()); + + DRACO_ASSERT_OK(encoder.EncodeFile(*scene, gltf_filename)); + + // Get the size of the generated file. + const size_t grid_file_size_2 = draco::GetFileSize(gltf_filename); + ASSERT_GT(grid_file_size_2, grid_file_size); +} + +TEST_F(GltfEncoderTest, TestOutputType) { + const std::string file_name = "sphere.gltf"; + const std::unique_ptr scene(DecodeTestGltfFileToScene(file_name)); + ASSERT_NE(scene, nullptr); + + const std::string gltf_file_full_path = + draco::GetTestTempFileFullPath("test.gltf"); + std::string folder_path; + std::string gltf_file_name; + draco::SplitPath(gltf_file_full_path, &folder_path, &gltf_file_name); + GltfEncoder gltf_encoder; + + ASSERT_TRUE(gltf_encoder.EncodeToFile(*scene, gltf_file_full_path, + folder_path)) + << "Failed gltf_file_full_path:" << gltf_file_full_path + << " folder_path:" << folder_path; + + const size_t default_gltf_size = draco::GetFileSize(gltf_file_full_path); + + // Test setting VERBOSE output type will increase the size of the gltf file. + gltf_encoder.set_output_type(GltfEncoder::VERBOSE); + ASSERT_TRUE(gltf_encoder.EncodeToFile(*scene, gltf_file_full_path, + folder_path)) + << "Failed gltf_file_full_path:" << gltf_file_full_path + << " folder_path:" << folder_path; + const size_t verbose_gltf_size = draco::GetFileSize(gltf_file_full_path); + ASSERT_GT(verbose_gltf_size, default_gltf_size); +} + +// Tests copying the name of the input texture file to the encoded texture file. +TEST_F(GltfEncoderTest, CopyTextureName) { + std::unique_ptr mesh = + draco::ReadMeshFromTestFile("CesiumMilkTruck/glTF/CesiumMilkTruck.gltf"); + ASSERT_NE(mesh, nullptr); + + std::unique_ptr mesh_from_gltf; + MeshToDecodedGltfMesh(*mesh, &mesh_from_gltf); + const Material *material = mesh->GetMaterialLibrary().GetMaterial(0); + ASSERT_NE(material, nullptr); + const Texture *texture = + mesh->GetMaterialLibrary().GetTextureLibrary().GetTexture(0); + ASSERT_NE(texture, nullptr); + ASSERT_EQ(draco::TextureUtils::GetTargetStem(*texture), "CesiumMilkTruck"); + ASSERT_EQ(draco::TextureUtils::GetTargetFormat(*texture), + draco::ImageFormat::PNG); +} + +TEST_F(GltfEncoderTest, EncodeTexCoord1) { + std::unique_ptr mesh = + draco::ReadMeshFromTestFile("MultiUVTest/glTF/MultiUVTest.gltf"); + + std::unique_ptr mesh_from_gltf; + MeshToDecodedGltfMesh(*mesh, &mesh_from_gltf); + ASSERT_EQ(mesh_from_gltf->GetMaterialLibrary().NumMaterials(), 1); + ASSERT_EQ( + mesh_from_gltf->GetMaterialLibrary().GetMaterial(0)->NumTextureMaps(), 2); + ASSERT_EQ( + mesh_from_gltf->GetMaterialLibrary().GetTextureLibrary().NumTextures(), + 2); + const std::vector textures = { + mesh_from_gltf->GetMaterialLibrary().GetTextureLibrary().GetTexture(0), + mesh_from_gltf->GetMaterialLibrary().GetTextureLibrary().GetTexture(1)}; + EXPECT_EQ(draco::TextureUtils::GetTargetStem(*textures[0]), "uv0"); + EXPECT_EQ(draco::TextureUtils::GetTargetStem(*textures[1]), "uv1"); + EXPECT_EQ(draco::TextureUtils::GetTargetFormat(*textures[0]), + draco::ImageFormat::PNG); + EXPECT_EQ(draco::TextureUtils::GetTargetFormat(*textures[1]), + draco::ImageFormat::PNG); + ASSERT_EQ(mesh_from_gltf->NumNamedAttributes(GeometryAttribute::TEX_COORD), + 2); + ASSERT_EQ(mesh_from_gltf->NumNamedAttributes(GeometryAttribute::POSITION), 1); + ASSERT_EQ(mesh_from_gltf->NumNamedAttributes(GeometryAttribute::NORMAL), 1); + ASSERT_EQ(mesh_from_gltf->NumNamedAttributes(GeometryAttribute::TANGENT), 1); +} + +TEST_F(GltfEncoderTest, TestEncodeFileFunctions) { + const std::string file_name = "sphere.gltf"; + const std::unique_ptr scene(DecodeTestGltfFileToScene(file_name)); + ASSERT_NE(scene, nullptr); + + // Test encoding with only the gltf filename parameter will output the correct + // bin filename and the textures will be in the same directory as the output + // glTF file. + const std::string output_gltf_filename = + draco::GetTestTempFileFullPath("encoded_example.gltf"); + std::string output_gltf_dir; + std::string output_gltf_basename; + draco::SplitPath(output_gltf_filename, &output_gltf_dir, + &output_gltf_basename); + + GltfEncoder gltf_encoder; + ASSERT_TRUE(gltf_encoder.EncodeFile(*scene, output_gltf_filename).ok()) + << "Failed to encode glTF filename:" << output_gltf_filename; + + const std::string output_bin_filename = + draco::GetTestTempFileFullPath("encoded_example.bin"); + const size_t output_bin_size = draco::GetFileSize(output_bin_filename); + ASSERT_GT(output_bin_size, 0); + const std::string output_png_filename = + draco::GetTestTempFileFullPath("sphere_Texture0_Normal.png"); + const size_t output_png_size = draco::GetFileSize(output_png_filename); + ASSERT_GT(output_png_size, 0); + + // Test encoding with the gltf and bin filename parameter, the textures will + // be in the same directory as the output glTF file. + const std::string new_bin_filename = + draco::GetTestTempFileFullPath("different_stem_name.bin"); + ASSERT_TRUE( + gltf_encoder + .EncodeFile(*scene, output_gltf_filename, new_bin_filename) + .ok()) + << "Failed to encode glTF filename:" << output_gltf_filename; + + const size_t new_bin_size = draco::GetFileSize(new_bin_filename); + ASSERT_GT(new_bin_size, 0); + ASSERT_EQ(new_bin_size, output_bin_size); + + // Test encoding with the gltf and bin filename and resource_dir parameter, + // the textures will be in the resource_dir directory. + const std::string new_resource_dir = output_gltf_dir + "/textures"; + ASSERT_TRUE(gltf_encoder + .EncodeFile(*scene, output_gltf_filename, + new_bin_filename, new_resource_dir) + .ok()) + << "Failed to encode glTF filename:" << output_gltf_filename; + + const std::string new_png_filename = + draco::GetTestTempFileFullPath("textures/sphere_Texture0_Normal.png"); + const size_t newest_bin_size = draco::GetFileSize(new_bin_filename); + ASSERT_GT(new_bin_size, 0); + ASSERT_EQ(new_bin_size, output_bin_size); + ASSERT_EQ(newest_bin_size, new_bin_size); + const size_t new_png_size = draco::GetFileSize(new_png_filename); + ASSERT_GT(new_png_size, 0); + ASSERT_EQ(new_png_size, output_png_size); +} + +TEST_F(GltfEncoderTest, DoubleSidedMaterial) { + const std::string file_name = "TwoSidedPlane/glTF/TwoSidedPlane.gltf"; + const std::unique_ptr scene(DecodeTestGltfFileToScene(file_name)); + ASSERT_NE(scene, nullptr); + EXPECT_EQ(scene->GetMaterialLibrary().NumMaterials(), 1); + EXPECT_EQ(scene->GetMaterialLibrary().GetMaterial(0)->GetDoubleSided(), true); + + std::unique_ptr scene_from_gltf; + SceneToDecodedGltfScene(*scene, &scene_from_gltf); + EXPECT_EQ(scene_from_gltf->GetMaterialLibrary().NumMaterials(), 1); + EXPECT_EQ( + scene_from_gltf->GetMaterialLibrary().GetMaterial(0)->GetDoubleSided(), + true); +} + +TEST_F(GltfEncoderTest, EncodeGlb) { + const std::string file_name = "sphere.gltf"; + const std::unique_ptr scene(DecodeTestGltfFileToScene(file_name)); + ASSERT_NE(scene, nullptr); + + std::unique_ptr scene_from_gltf; + SceneToDecodedGltfScene(*scene, "temp.gltf", &scene_from_gltf); + + std::unique_ptr scene_from_glb; + SceneToDecodedGltfScene(*scene, "temp.glb", &scene_from_glb); + + CompareScenes(scene_from_gltf.get(), scene_from_glb.get()); +} + +TEST_F(GltfEncoderTest, EncodeVertexColor) { + const std::string file_name = "VertexColorTest/glTF/VertexColorTest.gltf"; + const std::unique_ptr scene(DecodeTestGltfFileToScene(file_name)); + ASSERT_NE(scene, nullptr); + EXPECT_EQ(scene->NumMeshes(), 2); + const Mesh &mesh = scene->GetMesh(MeshIndex(1)); + EXPECT_EQ(mesh.NumNamedAttributes(GeometryAttribute::COLOR), 1); + + std::unique_ptr scene_from_gltf; + SceneToDecodedGltfScene(*scene, "temp.gltf", &scene_from_gltf); + EXPECT_EQ(scene_from_gltf->NumMeshes(), 2); + const Mesh &encoded_mesh = scene_from_gltf->GetMesh(MeshIndex(1)); + EXPECT_EQ(encoded_mesh.NumNamedAttributes(GeometryAttribute::COLOR), 1); +} + +TEST_F(GltfEncoderTest, InterpolationTest) { + const std::string file_name = "InterpolationTest/glTF/InterpolationTest.gltf"; + const std::unique_ptr scene(DecodeTestGltfFileToScene(file_name)); + ASSERT_NE(scene, nullptr); + + std::unique_ptr transcoded_scene; + SceneToDecodedGltfScene(*scene, "InterpolationTest.gltf", &transcoded_scene); + ASSERT_NE(transcoded_scene, nullptr); + CompareScenes(scene.get(), transcoded_scene.get()); + EXPECT_EQ(transcoded_scene->NumAnimations(), 9); + + const std::vector animation_names{ + "Step Scale", "Linear Scale", + "CubicSpline Scale", "Step Rotation", + "CubicSpline Rotation", "Linear Rotation", + "Step Translation", "CubicSpline Translation", + "Linear Translation"}; + for (int i = 0; i < scene->NumAnimations(); ++i) { + const Animation *const anim = scene->GetAnimation(AnimationIndex(i)); + ASSERT_NE(anim, nullptr); + ASSERT_EQ(anim->GetName(), animation_names[i]); + } + + // Currently all animation data is unique. See b/145703399. + const int num_input_accessors = 9; + const int num_output_accessors = 9; + CheckAnimationAccessors(*transcoded_scene, num_input_accessors, + num_output_accessors); +} + +TEST_F(GltfEncoderTest, KhrMaterialUnlit) { + const std::string filename = + "KhronosSampleModels/UnlitTest/glTF/UnlitTest.gltf"; + const std::unique_ptr scene(DecodeTestGltfFileToScene(filename)); + ASSERT_NE(scene, nullptr); + + const std::string output_gltf_filename = + draco::GetTestTempFileFullPath("encoded_example.gltf"); + std::string output_gltf_dir; + std::string output_gltf_basename; + draco::SplitPath(output_gltf_filename, &output_gltf_dir, + &output_gltf_basename); + + GltfEncoder gltf_encoder; + ASSERT_TRUE(gltf_encoder.EncodeFile(*scene, output_gltf_filename).ok()) + << "Failed to encode glTF filename:" << output_gltf_filename; + // glTF file should have four occurences of "KHR_materials_unlit". Two in the + // materials and one in extensionsUsed and one in extensionsRequired. + CheckGltfFileAtLeastStringCount(output_gltf_filename, "KHR_materials_unlit", + 4); +} + +TEST_F(GltfEncoderTest, OneMaterialUnlitWithFallback) { + const std::string filename = + "UnlitWithFallback/one_material_all_fallback/" + "one_material_all_fallback.gltf"; + const std::unique_ptr scene(DecodeTestGltfFileToScene(filename)); + ASSERT_NE(scene, nullptr); + + const std::string output_gltf_filename = + draco::GetTestTempFileFullPath("encoded_example.gltf"); + std::string output_gltf_dir; + std::string output_gltf_basename; + draco::SplitPath(output_gltf_filename, &output_gltf_dir, + &output_gltf_basename); + + GltfEncoder gltf_encoder; + ASSERT_TRUE(gltf_encoder.EncodeFile(*scene, output_gltf_filename).ok()) + << "Failed to encode glTF filename:" << output_gltf_filename; + + // glTF file should have two occurences of "KHR_materials_unlit". One in the + // materials and one in extensionsUsed. + CheckGltfFileAtLeastStringCount(output_gltf_filename, "KHR_materials_unlit", + 2); + + // The glTF file should provide a fallback to "KHR_materials_unlit", so there + // should be no "extensionsRequired" element. + CheckGltfFileNoString(output_gltf_filename, "extensionsRequired"); +} + +TEST_F(GltfEncoderTest, MultipleMaterialsUnlitWithFallback) { + std::string filename = + "UnlitWithFallback/three_materials_all_fallback/" + "three_materials_all_fallback.gltf"; + const std::unique_ptr scene_all_fallback( + DecodeTestGltfFileToScene(filename)); + ASSERT_NE(scene_all_fallback, nullptr); + + const std::string output_gltf_filename = + draco::GetTestTempFileFullPath("encoded_example.gltf"); + std::string output_gltf_dir; + std::string output_gltf_basename; + draco::SplitPath(output_gltf_filename, &output_gltf_dir, + &output_gltf_basename); + + GltfEncoder gltf_encoder; + ASSERT_TRUE( + gltf_encoder.EncodeFile(*scene_all_fallback, output_gltf_filename) + .ok()) + << "Failed to encode glTF filename:" << output_gltf_filename; + + // glTF file should have four occurences of "KHR_materials_unlit". Three in + // the materials and one in extensionsUsed. + CheckGltfFileAtLeastStringCount(output_gltf_filename, "KHR_materials_unlit", + 4); + + // The glTF file should provide a fallback to "KHR_materials_unlit", so there + // should be no "extensionsRequired" element. + CheckGltfFileNoString(output_gltf_filename, "extensionsRequired"); + + filename = + "UnlitWithFallback/three_materials_one_fallback/" + "three_materials_one_fallback.gltf"; + const std::unique_ptr scene_one_fallback( + DecodeTestGltfFileToScene(filename)); + ASSERT_NE(scene_one_fallback, nullptr); + + ASSERT_TRUE( + gltf_encoder.EncodeFile(*scene_one_fallback, output_gltf_filename) + .ok()) + << "Failed to encode glTF filename:" << output_gltf_filename; + + // glTF file should have three occurences of "KHR_materials_unlit". One in the + // materials, one in extensionsUsed, and one in extensionsRequired. + CheckGltfFileAtLeastStringCount(output_gltf_filename, "KHR_materials_unlit", + 3); + + // The glTF file only has one material with a fallback for + // "KHR_materials_unlit". The other two materials have "KHR_materials_unlit" + // set without a fallback, so there should be an "extensionsRequired" element. + CheckGltfFileAtLeastStringCount(output_gltf_filename, "extensionsRequired", + 1); +} + +TEST_F(GltfEncoderTest, KhrMaterialsSheenExtension) { + const std::string filename = + "KhronosSampleModels/SheenCloth/glTF/SheenCloth.gltf"; + const std::unique_ptr scene(DecodeTestGltfFileToScene(filename)); + ASSERT_NE(scene, nullptr); + + const std::string out_filename = + draco::GetTestTempFileFullPath("encoded_example.gltf"); + std::string output_gltf_dir; + std::string output_gltf_basename; + draco::SplitPath(out_filename, &output_gltf_dir, &output_gltf_basename); + + GltfEncoder gltf_encoder; + ASSERT_TRUE(gltf_encoder.EncodeFile(*scene, out_filename).ok()) + << "Failed to encode glTF filename:" << out_filename; + + // The "KHR_materials_sheen" should be in material and in extensionsUsed. + CheckGltfFileAtLeastStringCount(out_filename, "KHR_materials_sheen", 2); + CheckGltfFileAtLeastStringCount(out_filename, "sheenColorFactor", 1); + CheckGltfFileAtLeastStringCount(out_filename, "sheenColorTexture", 1); + CheckGltfFileAtLeastStringCount(out_filename, "sheenRoughnessFactor", 1); + CheckGltfFileAtLeastStringCount(out_filename, "sheenRoughnessTexture", 1); +} + +TEST_F(GltfEncoderTest, PbrNextExtensions) { + // Check that a model with PBR material extensions is encoded correctly. This + // is done by encoding an original model with all PBR material extension + // properties and textures, then decoding it and checking that it matches the + // original model. + // TODO(vytyaz): Test multiple materials with various sets of extensions. + + // Read the original model. + const std::string orig_name = "pbr_next/sphere/glTF/sphere.gltf"; + const std::unique_ptr original(DecodeTestGltfFileToScene(orig_name)); + ASSERT_NE(original, nullptr); + const Material &original_mat = *original->GetMaterialLibrary().GetMaterial(0); + + // Check that the original material has PBR extensions. + EXPECT_TRUE(original_mat.HasSheen()); + EXPECT_TRUE(original_mat.HasTransmission()); + EXPECT_TRUE(original_mat.HasClearcoat()); + EXPECT_TRUE(original_mat.HasVolume()); + EXPECT_TRUE(original_mat.HasIor()); + EXPECT_TRUE(original_mat.HasSpecular()); + + // Write the original model to a temporary file. + GltfEncoder encoder; + const std::string tmp_name = draco::GetTestTempFileFullPath("tmp.gltf"); + DRACO_ASSERT_OK(encoder.EncodeFile(*original, tmp_name)); + + // Read model from the temporay file. + GltfDecoder decoder; + DRACO_ASSIGN_OR_ASSERT(auto encoded, decoder.DecodeFromFileToScene(tmp_name)); + ASSERT_NE(encoded, nullptr); +} + +TEST_F(GltfEncoderTest, KhrTextureTransformWithoutFallback) { + // This is the example from Khronos, which should have "KHR_texture_transform" + // listed in the extensionsRequired, but does not for testing out client + // implementations. + const std::string filename = + "KhronosSampleModels/TextureTransformTest/glTF/TextureTransformTest.gltf"; + const std::unique_ptr scene(DecodeTestGltfFileToScene(filename)); + ASSERT_NE(scene, nullptr); + + const std::string output_gltf_filename = + draco::GetTestTempFileFullPath("encoded_example.gltf"); + std::string output_gltf_dir; + std::string output_gltf_basename; + draco::SplitPath(output_gltf_filename, &output_gltf_dir, + &output_gltf_basename); + + GltfEncoder gltf_encoder; + ASSERT_TRUE(gltf_encoder.EncodeFile(*scene, output_gltf_filename).ok()) + << "Failed to encode glTF filename:" << output_gltf_filename; + // glTF file should have eight occurences of "KHR_materials_unlit". Six in the + // materials and one in extensionsUsed and one in extensionsRequired. + CheckGltfFileAtLeastStringCount(output_gltf_filename, "KHR_texture_transform", + 8); + + // glTF file should still contain only two occurences of '"sampler": 0'. + CheckGltfFileAtLeastStringCount(output_gltf_filename, "\"sampler\": 0", 2); + + // glTF file should have one occurence of "wrapS", "wrapT", "minFilter", and + // "magFilter". + CheckGltfFileAtLeastStringCount(output_gltf_filename, "wrapS", 1); + CheckGltfFileAtLeastStringCount(output_gltf_filename, "wrapT", 1); + CheckGltfFileAtLeastStringCount(output_gltf_filename, "minFilter", 1); + CheckGltfFileAtLeastStringCount(output_gltf_filename, "magFilter", 1); +} + +TEST_F(GltfEncoderTest, KhrTextureTransformWithoutFallbackRequried) { + // This is the example from Khronos, changed to list "KHR_texture_transform" + // in extensionsRequired. + const std::string filename = + "glTF/TextureTransformTestWithRequired/" + "TextureTransformTestWithRequired.gltf"; + const std::unique_ptr scene(DecodeTestGltfFileToScene(filename)); + ASSERT_NE(scene, nullptr); + + const std::string output_gltf_filename = + draco::GetTestTempFileFullPath("encoded_example.gltf"); + std::string output_gltf_dir; + std::string output_gltf_basename; + draco::SplitPath(output_gltf_filename, &output_gltf_dir, + &output_gltf_basename); + + GltfEncoder gltf_encoder; + ASSERT_TRUE(gltf_encoder.EncodeFile(*scene, output_gltf_filename).ok()) + << "Failed to encode glTF filename:" << output_gltf_filename; + // glTF file should have eight occurences of "KHR_materials_unlit". Six in the + // materials and one in extensionsUsed and one in extensionsRequired. + CheckGltfFileAtLeastStringCount(output_gltf_filename, "KHR_texture_transform", + 8); +} + +TEST_F(GltfEncoderTest, KhrTextureTransformWithFallback) { + // This is an example of "KHR_texture_transform" extension with fallback data. + const std::string filename = + "glTF/KhrTextureTransformWithFallback/" + "KhrTextureTransformWithFallback.gltf"; + const std::unique_ptr scene(DecodeTestGltfFileToScene(filename)); + ASSERT_NE(scene, nullptr); + + const std::string output_gltf_filename = + draco::GetTestTempFileFullPath("encoded_example.gltf"); + std::string output_gltf_dir; + std::string output_gltf_basename; + draco::SplitPath(output_gltf_filename, &output_gltf_dir, + &output_gltf_basename); + + GltfEncoder gltf_encoder; + ASSERT_TRUE(gltf_encoder.EncodeFile(*scene, output_gltf_filename).ok()) + << "Failed to encode glTF filename:" << output_gltf_filename; + // glTF file should have two occurences of "KHR_materials_unlit". One in the + // materials and one in extensionsUsed. + CheckGltfFileAtLeastStringCount(output_gltf_filename, "KHR_texture_transform", + 2); +} + +// Tests if the source file has a node with an identity matrix, that we do not +// output the identiy matrix. +TEST_F(GltfEncoderTest, MeshWithIdentityTransformation) { + const std::string gltf_source_full_path = + GetTestFileFullPath("Triangle/glTF/Triangle_identity_matrix.gltf"); + + // Check that the source file contains one "matrix" and no "translation" + // strings. + CheckGltfFileAtLeastStringCount(gltf_source_full_path, "matrix", 1); + CheckGltfFileNoString(gltf_source_full_path, "translation"); + + std::unique_ptr scene = draco::ReadSceneFromTestFile( + "Triangle/glTF/Triangle_identity_matrix.gltf"); + ASSERT_NE(scene, nullptr); + SceneNode *scene_node = scene->GetNode(SceneNodeIndex(0)); + ASSERT_NE(scene_node, nullptr); + const TrsMatrix &trs_matrix = scene_node->GetTrsMatrix(); + + // gltf_decoder will not set the trs matrix if the matrix is identity. + ASSERT_FALSE(trs_matrix.MatrixSet()); + + // Add the identity matrix. + TrsMatrix trsm; + trsm.SetMatrix(Eigen::Matrix4d::Identity()); + scene_node->SetTrsMatrix(trsm); + + const TrsMatrix &check_trs_matrix = scene_node->GetTrsMatrix(); + ASSERT_TRUE(check_trs_matrix.MatrixSet()); + ASSERT_EQ(check_trs_matrix.IsMatrixIdentity(), true); + + const std::string gltf_file_full_path = + draco::GetTestTempFileFullPath("MeshWithIdentityTransformation.gltf"); + std::string folder_path; + std::string gltf_file_name; + draco::SplitPath(gltf_file_full_path, &folder_path, &gltf_file_name); + GltfEncoder gltf_encoder; + + ASSERT_TRUE(gltf_encoder.EncodeToFile( + *scene.get(), gltf_file_full_path, folder_path)) + << "Failed gltf_file_full_path:" << gltf_file_full_path + << " folder_path:" << folder_path; + std::unique_ptr scene_gltf = + std::move(ReadSceneFromFile(gltf_file_full_path)).value(); + ASSERT_NE(scene_gltf, nullptr); + // Check that the output file contains no "matrix" or "translation" strings. + CheckGltfFileNoString(gltf_file_full_path, "matrix"); + CheckGltfFileNoString(gltf_file_full_path, "translation"); +} + +// Tests if the source file has a node with a matrix that only has the +// translation values set. If it does then instead of outputting the full matrix +// we only output the "translation" glTF element. +TEST_F(GltfEncoderTest, MeshWithTranslationOnlyMatrix) { + std::unique_ptr scene = draco::ReadSceneFromTestFile( + "Triangle/glTF/Triangle_translation_only_matrix.gltf"); + ASSERT_NE(scene, nullptr); + SceneNode *scene_node = scene->GetNode(SceneNodeIndex(0)); + ASSERT_NE(scene_node, nullptr); + const TrsMatrix &input_trs_matrix = scene_node->GetTrsMatrix(); + ASSERT_TRUE(input_trs_matrix.MatrixSet()); + ASSERT_FALSE(input_trs_matrix.TranslationSet()); + ASSERT_FALSE(input_trs_matrix.RotationSet()); + ASSERT_FALSE(input_trs_matrix.ScaleSet()); + ASSERT_TRUE(input_trs_matrix.IsMatrixTranslationOnly()); + + const std::string gltf_file_full_path = + draco::GetTestTempFileFullPath("MeshWithTranslationOnlyMatrix.gltf"); + std::string folder_path; + std::string gltf_file_name; + draco::SplitPath(gltf_file_full_path, &folder_path, &gltf_file_name); + GltfEncoder gltf_encoder; + + ASSERT_TRUE(gltf_encoder.EncodeToFile( + *scene.get(), gltf_file_full_path, folder_path)) + << "Failed gltf_file_full_path:" << gltf_file_full_path + << " folder_path:" << folder_path; + std::unique_ptr scene_gltf = + std::move(ReadSceneFromFile(gltf_file_full_path)).value(); + ASSERT_NE(scene_gltf, nullptr); + SceneNode *output_scene_node = scene_gltf->GetNode(SceneNodeIndex(0)); + ASSERT_NE(output_scene_node, nullptr); + const TrsMatrix &output_trs_matrix = output_scene_node->GetTrsMatrix(); + ASSERT_FALSE(output_trs_matrix.MatrixSet()); + ASSERT_TRUE(output_trs_matrix.TranslationSet()); + ASSERT_FALSE(output_trs_matrix.RotationSet()); + ASSERT_FALSE(output_trs_matrix.ScaleSet()); +} + +// Tests that a scene can be encoded to buffer in GLB format. +TEST_F(GltfEncoderTest, EncodeToBuffer) { + // Load scene from file. + const std::string file_name = "CesiumMilkTruck/glTF/CesiumMilkTruck.gltf"; + const std::unique_ptr scene = ReadSceneFromTestFile(file_name); + ASSERT_NE(scene, nullptr); + + // Encode scene to buffer in GLB format. + GltfEncoder encoder; + EncoderBuffer buffer; + DRACO_ASSERT_OK(encoder.EncodeToBuffer(*scene, &buffer)); + ASSERT_NE(buffer.size(), 0); + + // Write scene to file in GLB format. + const std::string glb_file_path = draco::GetTestTempFileFullPath("temp.glb"); + std::string folder_path; + std::string glb_file_name; + draco::SplitPath(glb_file_path, &folder_path, &glb_file_name); + encoder.EncodeToFile(*scene, glb_file_path, folder_path); + + // Check that the buffer contents match the GLB file contents. + ASSERT_EQ(buffer.size(), draco::GetFileSize(glb_file_path)); + std::vector file_data; + ASSERT_TRUE(ReadFileToBuffer(glb_file_path, &file_data)); + ASSERT_EQ(std::memcmp(file_data.data(), buffer.data(), buffer.size()), 0); +} + +// Tests that a scene with lights can be encoded into a file. +TEST_F(GltfEncoderTest, EncodeLights) { + const std::string file_name = "sphere_lights.gltf"; + const std::unique_ptr scene = ReadSceneFromTestFile(file_name); + ASSERT_NE(scene, nullptr); + ASSERT_EQ(scene->NumLights(), 4); + EncodeSceneToGltfAndCompare(scene.get()); +} + +// Helper method for adding mesh group GPU instancing to the milk truck scene. +draco::Status AddGpuInstancingToMilkTruck(draco::Scene *scene) { + // Create an instance and set its transformation TRS vectors. + draco::InstanceArray::Instance instance_0; + instance_0.trs.SetTranslation(Eigen::Vector3d(-0.2, 0.0, 0.0)); + instance_0.trs.SetScale(Eigen::Vector3d(1.0, 1.0, 1.0)); + + // Create another instance. + draco::InstanceArray::Instance instance_1; + instance_1.trs.SetTranslation(Eigen::Vector3d(1.0, 0.0, 0.0)); + instance_1.trs.SetScale(Eigen::Vector3d(2.0, 2.0, 2.0)); + + // Add an empty GPU instancing object to the scene. + const draco::InstanceArrayIndex index = scene->AddInstanceArray(); + draco::InstanceArray *gpu_instancing = scene->GetInstanceArray(index); + + // Add two instances to the GPU instancing object stored in the scene. + DRACO_RETURN_IF_ERROR(gpu_instancing->AddInstance(instance_0)); + DRACO_RETURN_IF_ERROR(gpu_instancing->AddInstance(instance_1)); + + // Assign the GPU instancing object to two mesh groups in two scene nodes. + scene->GetNode(draco::SceneNodeIndex(2))->SetInstanceArrayIndex(index); + scene->GetNode(draco::SceneNodeIndex(4))->SetInstanceArrayIndex(index); + + return draco::OkStatus(); +} + +// Tests that a scene with instance arrays can be encoded into a file. Decoder +// has no GPU instancing support, so we will compare encoded file to a golden +// file. +TEST_F(GltfEncoderTest, EncodeInstanceArrays) { + // Read the milk truck. + auto scene = + draco::ReadSceneFromTestFile("CesiumMilkTruck/glTF/CesiumMilkTruck.gltf"); + ASSERT_NE(scene, nullptr); + + // Add GPU instancing to the scene for testing. + DRACO_ASSERT_OK(AddGpuInstancingToMilkTruck(scene.get())); + ASSERT_EQ(scene->NumInstanceArrays(), 1); + ASSERT_EQ(scene->NumNodes(), 5); + + // Prepare file paths. + const std::string temp_path = draco::GetTestTempFileFullPath("Truck.glb"); + const std::string golden_path = + GetTestFileFullPath("CesiumRowingTruckWithGpuInstancing.glb"); + + // Encode scene to a temporary file in GLB format. + std::string folder; + std::string name; + draco::SplitPath(temp_path, &folder, &name); + GltfEncoder encoder; + ASSERT_TRUE(encoder.EncodeToFile(*scene, temp_path, folder)) + << "Failed to encode to temporary file:" << temp_path; + + // Read encoded file to buffer. + std::vector encoded_data; + ASSERT_TRUE(ReadFileToBuffer(temp_path, &encoded_data)); +} + +// Tests that a scene with materials variants can be encoded into a file. +TEST_F(GltfEncoderTest, EncodeMaterialsVariants) { + const std::string file_name = + "KhronosSampleModels/DragonAttenuation/glTF/DragonAttenuation.gltf"; + const std::unique_ptr scene = ReadSceneFromTestFile(file_name); + ASSERT_NE(scene, nullptr); + ASSERT_EQ(scene->GetMaterialLibrary().NumMaterialsVariants(), 2); + EncodeSceneToGltfAndCompare(scene.get()); +} + +// Tests encoding of draco::Scene to glTF with various mesh feature ID sets and +// structural metadata property table. +TEST_F(GltfEncoderTest, EncodeSceneWithMeshFeaturesWithStructuralMetadata) { + const std::string file_name = "BoxMeta/glTF/BoxMeta.gltf"; + constexpr bool kHasMeshFeatures = true; + constexpr bool kHasStructuralMetadata = true; + constexpr bool kHasDracoCompression = false; + + // Read test file from file. + const std::unique_ptr scene(DecodeTestGltfFileToScene(file_name)); + ASSERT_NE(scene, nullptr); + + // Encode the scene to glTF and decode it back to draco::Scene and check. + std::unique_ptr scene_from_gltf; + SceneToDecodedGltfScene(*scene, &scene_from_gltf); + ASSERT_NE(scene_from_gltf, nullptr); + GltfTestHelper::CheckBoxMetaMeshFeatures(*scene_from_gltf, + kHasDracoCompression); + GltfTestHelper::CheckBoxMetaStructuralMetadata(*scene_from_gltf); +} + +// Tests encoding of draco::Scene with Draco compression to glTF with various +// mesh feature ID sets. +TEST_F(GltfEncoderTest, EncodeSceneWithMeshFeaturesWithDracoCompression) { + const std::string file_name = "BoxMetaDraco/glTF/BoxMetaDraco.gltf"; + constexpr bool kHasMeshFeatures = true; + constexpr bool kHasStructuralMetadata = false; + constexpr bool kHasDracoCompression = true; + + // Read test file from file. + const std::unique_ptr scene(DecodeTestGltfFileToScene(file_name)); + ASSERT_NE(scene, nullptr); + + // Encode the scene to glTF and decode it back to draco::Scene and check. + std::unique_ptr scene_from_gltf; + SceneToDecodedGltfScene(*scene, &scene_from_gltf); + ASSERT_NE(scene_from_gltf, nullptr); + GltfTestHelper::CheckBoxMetaMeshFeatures(*scene_from_gltf, + kHasDracoCompression); +} + +// Tests encoding of draco::Mesh to glTF with various mesh feature ID sets and +// structural metadata property table. +TEST_F(GltfEncoderTest, EncodeMeshWithMeshFeaturesWithStructuralMetadata) { + const std::string file_name = "BoxMeta/glTF/BoxMeta.gltf"; + constexpr bool kHasDracoCompression = false; + + // Read test file from file. + const std::unique_ptr mesh(ReadMeshFromTestFile(file_name)); + ASSERT_NE(mesh, nullptr); + + // Encode the scene to glTF and decode it back to draco::Mesh and check. + std::unique_ptr mesh_from_gltf; + MeshToDecodedGltfMesh(*mesh, &mesh_from_gltf); + ASSERT_NE(mesh_from_gltf, nullptr); + GltfTestHelper::CheckBoxMetaMeshFeatures(*mesh_from_gltf, + kHasDracoCompression); + GltfTestHelper::CheckBoxMetaStructuralMetadata(*mesh_from_gltf); +} + +// Tests encoding of draco::Mesh with Draco compression to glTF with various +// mesh feature ID sets. +TEST_F(GltfEncoderTest, EncodeMeshWithMeshFeaturesWithDracoCompression) { + constexpr bool kHasDracoCompression = true; + const std::string file_name = "BoxMetaDraco/glTF/BoxMetaDraco.gltf"; + + // Read test file from file. + const std::unique_ptr mesh(ReadMeshFromTestFile(file_name)); + ASSERT_NE(mesh, nullptr); + + // Encode the scene to glTF and decode it back to draco::Mesh and check. + std::unique_ptr mesh_from_gltf; + MeshToDecodedGltfMesh(*mesh, &mesh_from_gltf); + ASSERT_NE(mesh_from_gltf, nullptr); + GltfTestHelper::CheckBoxMetaMeshFeatures(*mesh_from_gltf, + kHasDracoCompression); +} + +// Tests encoding of draco::Mesh with mesh features associated with different +// mesh primitives. +TEST_F(GltfEncoderTest, EncodeMeshWithMeshFeaturesWithMultiplePrimitives) { + const std::string file_name = "BoxesMeta/glTF/BoxesMeta.gltf"; + + // Read test file from file. + const std::unique_ptr mesh(ReadMeshFromTestFile(file_name)); + ASSERT_NE(mesh, nullptr); + // All mesh features should share two textures. + ASSERT_EQ(mesh->GetNonMaterialTextureLibrary().NumTextures(), 2); + + // Encode the scene to glTF and decode it back to draco::Mesh and check. + std::unique_ptr mesh_from_gltf; + MeshToDecodedGltfMesh(*mesh, &mesh_from_gltf); + ASSERT_NE(mesh_from_gltf, nullptr); + + ASSERT_EQ(mesh_from_gltf->GetMaterialLibrary().NumMaterials(), 2); + ASSERT_EQ(mesh_from_gltf->NumMeshFeatures(), 5); + + // First two mesh features should be used by material 0 and the reamining by + // material 1. + for (draco::MeshFeaturesIndex mfi(0); mfi < 5; ++mfi) { + // Each mesh feature should be used by a single material. + ASSERT_EQ(mesh_from_gltf->NumMeshFeaturesMaterialMasks(mfi), 1); + if (mfi.value() < 2) { + ASSERT_EQ(mesh_from_gltf->GetMeshFeaturesMaterialMask(mfi, 0), 0); + } else { + ASSERT_EQ(mesh_from_gltf->GetMeshFeaturesMaterialMask(mfi, 0), 1); + } + } + // All mesh features should share two textures. + ASSERT_EQ(mesh_from_gltf->GetNonMaterialTextureLibrary().NumTextures(), 2); + + // Ensure it still works correctly when we re-encode the source |mesh| as a + // scene. + std::unique_ptr scene_from_gltf; + MeshToDecodedGltfScene(*mesh, &scene_from_gltf); + ASSERT_NE(scene_from_gltf, nullptr); + + ASSERT_EQ(scene_from_gltf->NumMeshes(), 2); + + // First mesh should have 2 mesh features and the other one 3 mesh features. + ASSERT_EQ(scene_from_gltf->GetMesh(draco::MeshIndex(0)).NumMeshFeatures(), 2); + ASSERT_EQ(scene_from_gltf->GetMesh(draco::MeshIndex(1)).NumMeshFeatures(), 3); + + // All mesh features should share two textures. + ASSERT_EQ(scene_from_gltf->GetNonMaterialTextureLibrary().NumTextures(), 2); +} + +// Tests encoding of draco::Mesh containing a point cloud and two materials. +TEST_F(GltfEncoderTest, EncodePointCloudWithMaterials) { + const std::string file_name = + "SphereTwoMaterials/sphere_two_materials_point_cloud.gltf"; + + // Read test file from file. + const std::unique_ptr mesh(ReadMeshFromTestFile(file_name)); + ASSERT_NE(mesh, nullptr); + + // Input should have no faces. + ASSERT_EQ(mesh->num_faces(), 0); + + // There should be two materials + ASSERT_EQ(mesh->GetMaterialLibrary().NumMaterials(), 2); + + // Encode the mesh to glTF and decode it back to draco::Mesh and check. + std::unique_ptr mesh_from_gltf; + MeshToDecodedGltfMesh(*mesh, &mesh_from_gltf); + ASSERT_NE(mesh_from_gltf, nullptr); + + ASSERT_EQ(mesh_from_gltf->num_faces(), 0); + ASSERT_EQ(mesh_from_gltf->GetMaterialLibrary().NumMaterials(), 2); +} + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED diff --git a/contrib/draco/src/draco/io/gltf_test_helper.cc b/contrib/draco/src/draco/io/gltf_test_helper.cc new file mode 100644 index 000000000..13cce6f4e --- /dev/null +++ b/contrib/draco/src/draco/io/gltf_test_helper.cc @@ -0,0 +1,823 @@ +// Copyright 2022 The Draco Authors. +// +// 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 "draco/io/gltf_test_helper.h" + +#include +#include +#include +#include +#include + +#include "draco/core/draco_test_base.h" +#include "draco/core/draco_test_utils.h" +#include "draco/metadata/property_table.h" +#include "draco/texture/texture_library.h" + +namespace draco { + +#ifdef DRACO_TRANSCODER_SUPPORTED + +void GltfTestHelper::AddBoxMetaMeshFeatures(Scene *scene) { + // Check the scene. + ASSERT_NE(scene, nullptr); + ASSERT_EQ(scene->NumMeshes(), 1); + TextureLibrary &texture_library = scene->GetNonMaterialTextureLibrary(); + ASSERT_EQ(texture_library.NumTextures(), 0); + + // Check the mesh. + Mesh &mesh = scene->GetMesh(MeshIndex(0)); + ASSERT_EQ(mesh.num_faces(), 12); + ASSERT_EQ(mesh.num_attributes(), 2); + ASSERT_EQ(mesh.num_points(), 24); + + // Get mesh element counts. + const int num_faces = mesh.num_faces(); + const int num_corners = 3 * mesh.num_faces(); + const int num_vertices = + mesh.GetNamedAttribute(GeometryAttribute::POSITION)->size(); + + // Add feature ID set with per-face Uint8 attribute named _FEATURE_ID_0. + { + // Create feature ID attribute. + constexpr DataType kType = DataType::DT_UINT8; + std::unique_ptr pa(new PointAttribute()); + pa->Init(GeometryAttribute::GENERIC, 1, kType, false, mesh.num_faces()); + for (AttributeValueIndex avi(0); avi < num_faces; ++avi) { + const int8_t val = avi.value(); + pa->SetAttributeValue(avi, &val); + } + const int att_id = mesh.AddPerFaceAttribute(std::move(pa)); + std::unique_ptr metadata(new AttributeMetadata()); + metadata->AddEntryString("attribute_name", "_FEATURE_ID_0"); + mesh.AddAttributeMetadata(att_id, std::move(metadata)); + + // Add feature ID set to the mesh. + std::unique_ptr features(new MeshFeatures()); + features->SetLabel("faces"); + features->SetFeatureCount(num_faces); + features->SetNullFeatureId(100); + features->SetPropertyTableIndex(0); + features->SetAttributeIndex(0); + mesh.AddMeshFeatures(std::move(features)); + } + + // Add feature ID set with per-vertex Uint16 attribute named _FEATURE_ID_1. + { + // Create feature ID attribute. + constexpr DataType kType = DataType::DT_UINT16; + std::unique_ptr pa(new PointAttribute()); + pa->Init(GeometryAttribute::GENERIC, 1, kType, false, num_vertices); + for (AttributeValueIndex avi(0); avi < num_vertices; ++avi) { + const uint16_t val = avi.value(); + pa->SetAttributeValue(avi, &val); + } + const int att_id = mesh.AddPerVertexAttribute(std::move(pa)); + std::unique_ptr metadata(new AttributeMetadata()); + metadata->AddEntryString("attribute_name", "_FEATURE_ID_1"); + mesh.AddAttributeMetadata(att_id, std::move(metadata)); + + // Add feature ID set to the mesh. + std::unique_ptr features(new MeshFeatures()); + features->SetLabel("vertices"); + features->SetFeatureCount(num_vertices); + features->SetNullFeatureId(101); + features->SetPropertyTableIndex(1); + features->SetAttributeIndex(1); + mesh.AddMeshFeatures(std::move(features)); + } + + // Add feature ID set with per-corner Float attribute named _FEATURE_ID_2. + { + // Create feature ID attribute. + constexpr DataType kType = DataType::DT_FLOAT32; + std::unique_ptr pa(new PointAttribute()); + pa->Init(GeometryAttribute::GENERIC, 1, kType, false, num_corners); + IndexTypeVector corner_to_value( + num_corners); + for (AttributeValueIndex avi(0); avi < num_corners; ++avi) { + const float val = avi.value(); + pa->SetAttributeValue(avi, &val); + corner_to_value[CornerIndex(avi.value())] = avi; + } + const int att_id = + mesh.AddAttributeWithConnectivity(std::move(pa), corner_to_value); + std::unique_ptr metadata(new AttributeMetadata()); + metadata->AddEntryString("attribute_name", "_FEATURE_ID_2"); + mesh.AddAttributeMetadata(att_id, std::move(metadata)); + + // Add feature ID set to the mesh. + std::unique_ptr features(new MeshFeatures()); + features->SetFeatureCount(num_corners); + features->SetAttributeIndex(2); + mesh.AddMeshFeatures(std::move(features)); + } + + // Add feature ID set with the IDs stored in the R texture channel and + // accessible via the first texture coordinate attribute. + { + // Add the first texture coordinate attribute. + constexpr DataType kType = DataType::DT_FLOAT32; + std::unique_ptr pa(new PointAttribute()); + pa->Init(GeometryAttribute::TEX_COORD, 2, kType, false, num_vertices); + std::vector> uv = { + {0.0000f, 0.0000f}, {0.0000f, 0.5000f}, {0.0000f, 1.0000f}, + {0.5000f, 0.0000f}, {0.5000f, 0.5000f}, {0.5000f, 1.0000f}, + {1.0000f, 0.0000f}, {1.0000f, 0.5000f}}; + for (AttributeValueIndex avi(0); avi < num_vertices; ++avi) { + const int index = avi.value(); + pa->SetAttributeValue(avi, uv[index].data()); + } + mesh.AddPerVertexAttribute(std::move(pa)); + } + + // Add feature ID set with the IDs stored in the GBA texture channels and + // accessible via the second texture coordinate attribute. + { + // Add the second texture coordinate attribute. + constexpr DataType kType = DataType::DT_FLOAT32; + std::unique_ptr pa(new PointAttribute()); + pa->Init(GeometryAttribute::TEX_COORD, 2, kType, false, num_vertices); + std::vector> uv = { + {0.0000f, 0.0000f}, {0.0000f, 0.5000f}, {0.0000f, 1.0000f}, + {0.5000f, 0.0000f}, {0.5000f, 0.5000f}, {0.5000f, 1.0000f}, + {1.0000f, 0.0000f}, {1.0000f, 0.5000f}}; + for (AttributeValueIndex avi(0); avi < num_vertices; ++avi) { + const int index = avi.value(); + pa->SetAttributeValue(avi, uv[index].data()); + } + mesh.AddPerVertexAttribute(std::move(pa)); + ASSERT_EQ(mesh.NumNamedAttributes(GeometryAttribute::TEX_COORD), 2); + } +} + +void GltfTestHelper::AddBoxMetaStructuralMetadata(Scene *scene) { + // Add structural metadata property table schema in the following JSON: + // "schema": { + // "id": "galaxy", + // "classes": { + // "planet": { + // "properties": { + // "color": { + // "componentType": "UINT8", + // "description": "The RGB color.", + // "required": true, + // "type": "VEC3" + // }, + // "name": { + // "description": "The name.", + // "required": true, + // "type": "STRING" + // } + // "sequence": { + // "description": "The number sequence.", + // "required": false, + // "type": "SCALAR" + // } + // } + // } + // }, + // "enums": { + // "classifications": { + // "description": "Classifications of planets.", + // "name": "classifications", + // "values": [ + // { "name": "Unspecified", "value": 0 }, + // { "name": "Gas Giant", "value": 1 }, + // { "name": "Waterworld", "value": 2 }, + // { "name": "Agriworld", "value": 3 }, + // { "name": "Ordnance", "value": 4 } + // ] + // } + // } + // } + typedef PropertyTable::Schema::Object Object; + PropertyTable::Schema schema; + Object &json = schema.json; + json.SetObjects().emplace_back("id", "galaxy"); + json.SetObjects().emplace_back("classes"); + json.SetObjects().back().SetObjects().emplace_back("planet"); + Object &planet = json.SetObjects().back().SetObjects().back(); + planet.SetObjects().emplace_back("properties"); + Object &properties = planet.SetObjects().back(); + + properties.SetObjects().emplace_back("color"); + Object &color = properties.SetObjects().back(); + color.SetObjects().emplace_back("componentType", "UINT8"); + color.SetObjects().emplace_back("description", "The RGB color."); + color.SetObjects().emplace_back("required", true); + color.SetObjects().emplace_back("type", "VEC3"); + + properties.SetObjects().emplace_back("name"); + Object &name = properties.SetObjects().back(); + name.SetObjects().emplace_back("description", "The name."); + name.SetObjects().emplace_back("required", true); + name.SetObjects().emplace_back("type", "STRING"); + + properties.SetObjects().emplace_back("sequence"); + Object &sequence = properties.SetObjects().back(); + sequence.SetObjects().emplace_back("description", "The number sequence."); + sequence.SetObjects().emplace_back("required", false); + sequence.SetObjects().emplace_back("type", "SCALAR"); + + json.SetObjects().emplace_back("enums"); + json.SetObjects().back().SetObjects().emplace_back("classifications"); + Object &classifications = json.SetObjects().back().SetObjects().back(); + classifications.SetObjects().emplace_back("description", + "Classifications of planets."); + classifications.SetObjects().emplace_back("name", "classifications"); + classifications.SetObjects().emplace_back("values"); + Object &values = classifications.SetObjects().back(); + + values.SetArray().emplace_back(); + values.SetArray().back().SetObjects().emplace_back("name", "Unspecified"); + values.SetArray().back().SetObjects().emplace_back("value", 0); + + values.SetArray().emplace_back(); + values.SetArray().back().SetObjects().emplace_back("name", "Gas Giant"); + values.SetArray().back().SetObjects().emplace_back("value", 1); + + values.SetArray().emplace_back(); + values.SetArray().back().SetObjects().emplace_back("name", "Waterworld"); + values.SetArray().back().SetObjects().emplace_back("value", 2); + + values.SetArray().emplace_back(); + values.SetArray().back().SetObjects().emplace_back("name", "Agriworld"); + values.SetArray().back().SetObjects().emplace_back("value", 3); + + values.SetArray().emplace_back(); + values.SetArray().back().SetObjects().emplace_back("name", "Ordnance"); + values.SetArray().back().SetObjects().emplace_back("value", 4); + + // Add property table schema to the scene. + scene->GetStructuralMetadata().SetPropertyTableSchema(schema); + + // Add structural metadata property table. + std::unique_ptr table(new PropertyTable()); + table->SetName("Galaxy far far away."); + table->SetClass("planet"); + table->SetCount(16); + + // Add property describing RGB color components of the planet class. + { + std::unique_ptr property( + new PropertyTable::Property()); + property->SetName("color"); + property->GetData().target = 34962; // ARRAY_BUFFER. + property->GetData().data = {94, 94, 194, // Tatooine + 94, 145, 161, // Corusant + 118, 171, 91, // Naboo + 103, 139, 178, // Alderaan + 83, 98, 154, // Dagobah + 91, 177, 175, // Mandalore + 190, 92, 108, // Corellia + 72, 69, 169, // Kamino + 154, 90, 101, // Kashyyyk + 174, 85, 175, // Dantooine + 184, 129, 96, // Hoth + 185, 91, 180, // Mustafar + 194, 150, 83, // Bespin + 204, 111, 134, // Yavin + 182, 90, 89, // Geonosis + 0, 0, 0}; // UNLABELED + table->AddProperty(std::move(property)); + } + + // Add property that describes names of the planet class. + { + std::unique_ptr property( + new PropertyTable::Property()); + property->SetName("name"); + property->GetData().target = 34963; // ELEMENT_ARRAY_BUFFER. + const std::string data = + "named_class:Tatooine" + "named_class:Corusant" + "named_class:Naboo" + "named_class:Alderaan" + "named_class:Dagobah" + "named_class:Mandalore" + "named_class:Corellia" + "named_class:Kamino" + "named_class:Kashyyyk" + "named_class:Dantooine" + "named_class:Hoth" + "named_class:Mustafar" + "named_class:Bespin" + "named_class:Yavin" + "named_class:Geonosis" + "UNLABELED"; + property->GetData().data.assign(data.begin(), data.end()); + property->GetStringOffsets().type = "UINT32"; + property->GetStringOffsets().data.target = 34963; // ELEMENT_ARRAY_BUFFER. + property->GetStringOffsets().data.data = {0, 0, 0, 0, // Tatooine + 20, 0, 0, 0, // Corusant + 40, 0, 0, 0, // Naboo + 57, 0, 0, 0, // Alderaan + 77, 0, 0, 0, // Dagobah + 96, 0, 0, 0, // Mandalore + 117, 0, 0, 0, // Corellia + 137, 0, 0, 0, // Kamino + 155, 0, 0, 0, // Kashyyyk + 175, 0, 0, 0, // Dantooine + 196, 0, 0, 0, // Hoth + 212, 0, 0, 0, // Mustafar + 232, 0, 0, 0, // Bespin + 250, 0, 0, 0, // Yavin + 12, 1, 0, 0, // Geonosis + 32, 1, 0, 0, // UNLABELED + 41, 1, 0, 0}; + table->AddProperty(std::move(property)); + } + + // Add property that contains variable-length number sequence of the planet + // class. + { + std::unique_ptr property( + new PropertyTable::Property()); + property->SetName("sequence"); + property->GetData().target = 34963; // ELEMENT_ARRAY_BUFFER. + const std::vector data = { + 0.5f, 1.5f, 2.5f, 3.5f, 4.5f, 5.5f, // Tatooine + 6.5f, 7.5f, // Corusant + 8.5f, // Naboo + 9.5f, // Alderaan + 10.5f, 11.5f, // Dagobah + 12.5f, 13.5f, 14.5f, 15.5f, // Mandalore + 16.5f, 17.5f, // Corellia + 18.5f, 19.5f, // Kamino + 20.5f, 21.5f, 22.5f, // Kashyyyk + 23.5f, 24.5f, 25.5f, // Dantooine + 26.5f, 27.5f, // Hoth + 28.5f, 29.5f, // Mustafar + 30.5f, 31.5f, 32.5f, // Bespin + 33.5f, 34.5f, 35.5f, // Yavin + 36.5f, 37.5f, 38.5f, 39.5f, 40.5f // Geonosis + }; // UNLABELED (empty array). + property->GetData().data.resize(4 * data.size()); + memcpy(property->GetData().data.data(), data.data(), 4 * data.size()); + property->GetArrayOffsets().type = "UINT8"; + property->GetArrayOffsets().data.target = 34963; // ELEMENT_ARRAY_BUFFER. + property->GetArrayOffsets().data.data = { + 0 * 4, // Tatooine + 6 * 4, // Corusant + 8 * 4, // Naboo + 9 * 4, // Alderaan + 10 * 4, // Dagobah + 12 * 4, // Mandalore + 16 * 4, // Corellia + 18 * 4, // Kamino + 20 * 4, // Kashyyyk + 23 * 4, // Dantooine + 26 * 4, // Hoth + 28 * 4, // Mustafar + 30 * 4, // Bespin + 33 * 4, // Yavin + 36 * 4, // Geonosis + 41 * 4, // UNLABELED (empty array). + 41 * 4}; + table->AddProperty(std::move(property)); + } + + // Add property table to the scene. + scene->GetStructuralMetadata().AddPropertyTable(std::move(table)); +} + +template <> +void GltfTestHelper::CheckBoxMetaMeshFeatures(const Mesh &geometry, + bool has_draco_compression) { + CheckBoxMetaMeshFeatures(geometry, geometry.GetNonMaterialTextureLibrary(), + has_draco_compression); +} + +template <> +void GltfTestHelper::CheckBoxMetaMeshFeatures(const Scene &geometry, + bool has_draco_compression) { + ASSERT_EQ(geometry.NumMeshes(), 1); + CheckBoxMetaMeshFeatures(geometry.GetMesh(MeshIndex(0)), + geometry.GetNonMaterialTextureLibrary(), + has_draco_compression); +} + +void GltfTestHelper::CheckBoxMetaMeshFeatures(const Mesh &mesh, + const TextureLibrary &texture_lib, + bool has_draco_compression) { + // Check texture library. + ASSERT_EQ(texture_lib.NumTextures(), 2); + + // Check basic mesh properties. + ASSERT_EQ(mesh.NumMeshFeatures(), 5); + ASSERT_EQ(mesh.num_faces(), 12); + ASSERT_EQ(mesh.num_attributes(), 7); + ASSERT_EQ(mesh.num_points(), 36); + ASSERT_EQ(mesh.NumNamedAttributes(GeometryAttribute::GENERIC), 3); + ASSERT_EQ(mesh.NumNamedAttributes(GeometryAttribute::TEX_COORD), 2); + + // Get mesh element counts. + const int num_faces = mesh.num_faces(); + const int num_corners = 3 * mesh.num_faces(); + const int num_vertices = + mesh.GetNamedAttribute(GeometryAttribute::POSITION)->size(); + + // Check mesh feature ID set at index 0. + { + // Check mesh features. + const MeshFeatures &features = mesh.GetMeshFeatures(MeshFeaturesIndex(0)); + ASSERT_EQ(features.GetLabel(), "faces"); + ASSERT_EQ(features.GetFeatureCount(), num_faces); + ASSERT_EQ(features.GetNullFeatureId(), 100); + ASSERT_EQ(features.GetPropertyTableIndex(), 0); + ASSERT_EQ(features.GetAttributeIndex(), 0); + ASSERT_TRUE(features.GetTextureChannels().empty()); + ASSERT_EQ(features.GetTextureMap().texture(), nullptr); + ASSERT_EQ(features.GetTextureMap().tex_coord_index(), -1); + + // Check per-face Uint8 attribute named _FEATURE_ID_0. + const int att_id = + mesh.GetAttributeIdByMetadataEntry("attribute_name", "_FEATURE_ID_0"); + auto att = mesh.GetAttributeByUniqueId(att_id); + ASSERT_NE(att, nullptr); + ASSERT_EQ(att->attribute_type(), GeometryAttribute::GENERIC); + ASSERT_EQ(att->data_type(), DataType::DT_UINT8); + ASSERT_EQ(att->num_components(), 1); + ASSERT_EQ(att->size(), num_faces); + ASSERT_EQ(att->indices_map_size(), num_corners); + + // Check that the values are all the numbers from 0 to 12. + const std::vector expected_values = + has_draco_compression + ? std::vector{7, 11, 10, 3, 2, 5, 4, 1, 6, 9, 8, 0} + : std::vector{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}; + for (int i = 0; i < num_faces; i++) { + uint8_t val; + att->GetValue(AttributeValueIndex(i), &val); + ASSERT_EQ(val, expected_values[i]); + } + + // Check that the corners of each face have a common value. + for (int i = 0; i < num_faces; i++) { + const auto face = mesh.face(FaceIndex(i)); + ASSERT_EQ(*att->GetAddressOfMappedIndex(face[0]), + *att->GetAddressOfMappedIndex(face[1])); + ASSERT_EQ(*att->GetAddressOfMappedIndex(face[0]), + *att->GetAddressOfMappedIndex(face[2])); + } + } + + // Check the 2nd mesh feature ID set at index 1. + { + // Check mesh features. + const MeshFeatures &features = mesh.GetMeshFeatures(MeshFeaturesIndex(1)); + ASSERT_EQ(features.GetLabel(), "vertices"); + ASSERT_EQ(features.GetFeatureCount(), num_vertices); + ASSERT_EQ(features.GetNullFeatureId(), 101); + ASSERT_EQ(features.GetPropertyTableIndex(), 1); + ASSERT_EQ(features.GetAttributeIndex(), 1); + ASSERT_TRUE(features.GetTextureChannels().empty()); + ASSERT_EQ(features.GetTextureMap().texture(), nullptr); + ASSERT_EQ(features.GetTextureMap().tex_coord_index(), -1); + + // Check per-vertex Uint16 attribute named _FEATURE_ID_1. + const int att_id = + mesh.GetAttributeIdByMetadataEntry("attribute_name", "_FEATURE_ID_1"); + auto att = mesh.GetAttributeByUniqueId(att_id); + ASSERT_NE(att, nullptr); + ASSERT_EQ(att->attribute_type(), GeometryAttribute::GENERIC); + ASSERT_EQ(att->data_type(), DataType::DT_UINT16); + ASSERT_EQ(att->num_components(), 1); + ASSERT_EQ(att->size(), num_vertices); + ASSERT_EQ(att->indices_map_size(), num_corners); + + // Check that the values are all the numbers from 0 to 7. + const std::vector expected_values = + has_draco_compression ? std::vector{3, 6, 7, 4, 5, 0, 1, 2} + : std::vector{0, 1, 2, 3, 4, 5, 6, 7}; + for (int i = 0; i < num_vertices; i++) { + uint16_t val; + att->GetValue(AttributeValueIndex(i), &val); + ASSERT_EQ(val, expected_values[i]); + } + + // Check that the corners of a face have unique values. + for (int i = 0; i < num_faces; i++) { + const auto face = mesh.face(FaceIndex(i)); + ASSERT_NE(*att->GetAddressOfMappedIndex(face[0]), + *att->GetAddressOfMappedIndex(face[1])); + ASSERT_NE(*att->GetAddressOfMappedIndex(face[1]), + *att->GetAddressOfMappedIndex(face[2])); + ASSERT_NE(*att->GetAddressOfMappedIndex(face[2]), + *att->GetAddressOfMappedIndex(face[0])); + } + } + + // Check the 3rd mesh feature ID set at index 2. + { + // Check mesh features. + const MeshFeatures &features = mesh.GetMeshFeatures(MeshFeaturesIndex(2)); + ASSERT_TRUE(features.GetLabel().empty()); + ASSERT_EQ(features.GetFeatureCount(), num_corners); + ASSERT_EQ(features.GetNullFeatureId(), -1); + ASSERT_EQ(features.GetPropertyTableIndex(), -1); + ASSERT_EQ(features.GetAttributeIndex(), 2); + ASSERT_TRUE(features.GetTextureChannels().empty()); + ASSERT_EQ(features.GetTextureMap().texture(), nullptr); + ASSERT_EQ(features.GetTextureMap().tex_coord_index(), -1); + + // Check per-corner Float attribute named _FEATURE_ID_2. + const int att_id = + mesh.GetAttributeIdByMetadataEntry("attribute_name", "_FEATURE_ID_2"); + auto att = mesh.GetAttributeByUniqueId(att_id); + ASSERT_NE(att, nullptr); + ASSERT_EQ(att->attribute_type(), GeometryAttribute::GENERIC); + ASSERT_EQ(att->data_type(), DataType::DT_FLOAT32); + ASSERT_EQ(att->num_components(), 1); + ASSERT_EQ(att->size(), num_corners); + ASSERT_EQ(att->indices_map_size(), 0); + ASSERT_TRUE(att->is_mapping_identity()); + + // Check that the values are from 0 to 35. + const std::vector expected_values = + has_draco_compression + ? std::vector{23, 21, 22, 33, 34, 35, 31, 32, 30, 9, 10, 11, + 7, 8, 6, 15, 16, 17, 14, 12, 13, 5, 3, 4, + 19, 20, 18, 27, 28, 29, 26, 24, 25, 1, 2, 0} + : std::vector{0, 1, 2, 3, 4, 5, 6, 7, 8, + 9, 10, 11, 12, 13, 14, 15, 16, 17, + 18, 19, 20, 21, 22, 23, 24, 25, 26, + 27, 28, 29, 30, 31, 32, 33, 34, 35}; + for (int i = 0; i < num_corners; i++) { + float val; + att->GetValue(AttributeValueIndex(i), &val); + ASSERT_EQ(val, expected_values[i]); + } + + // Check that the corners have unique values. + for (int i = 0; i < num_faces; i++) { + const auto face = mesh.face(FaceIndex(i)); + float v0, v1, v2; + att->GetMappedValue(face[0], &v0); + att->GetMappedValue(face[1], &v1); + att->GetMappedValue(face[2], &v2); + ASSERT_EQ(v0, expected_values[3 * i + 0]); + ASSERT_EQ(v1, expected_values[3 * i + 1]); + ASSERT_EQ(v2, expected_values[3 * i + 2]); + } + } + + // Check mesh feature ID set at index 3. + { + // Check mesh features. + const MeshFeatures &features = mesh.GetMeshFeatures(MeshFeaturesIndex(3)); + ASSERT_TRUE(features.GetLabel().empty()); + ASSERT_EQ(features.GetFeatureCount(), 6); + ASSERT_EQ(features.GetNullFeatureId(), -1); + ASSERT_EQ(features.GetPropertyTableIndex(), -1); + ASSERT_EQ(features.GetAttributeIndex(), -1); + } + + // Check mesh feature ID set at index 4. + { + // Check mesh features. + const MeshFeatures &features = mesh.GetMeshFeatures(MeshFeaturesIndex(4)); + ASSERT_EQ(features.GetLabel(), "water"); + ASSERT_EQ(features.GetFeatureCount(), 2); + ASSERT_EQ(features.GetNullFeatureId(), -1); + ASSERT_EQ(features.GetPropertyTableIndex(), -1); + ASSERT_EQ(features.GetAttributeIndex(), -1); + } +} + +void GltfTestHelper::CheckBoxMetaStructuralMetadata( + const StructuralMetadata &structural_metadata) { + // Check property table schema. + { + const PropertyTable::Schema &schema = + structural_metadata.GetPropertyTableSchema(); + ASSERT_FALSE(schema.Empty()); + const PropertyTable::Schema::Object &json = schema.json; + ASSERT_EQ(json.GetObjects().size(), 3); + ASSERT_EQ(json.GetObjects()[0].GetName(), "classes"); + ASSERT_EQ(json.GetObjects()[0].GetObjects().size(), 1); + ASSERT_EQ(json.GetObjects()[0].GetObjects()[0].GetName(), "planet"); + ASSERT_EQ(json.GetObjects()[0].GetObjects()[0].GetObjects().size(), 1); + + const auto &properties = + json.GetObjects()[0].GetObjects()[0].GetObjects()[0]; + ASSERT_EQ(properties.GetName(), "properties"); + ASSERT_EQ(properties.GetObjects().size(), 3); + + const auto &color = properties.GetObjects()[0]; + ASSERT_EQ(color.GetName(), "color"); + ASSERT_EQ(color.GetObjects().size(), 4); + ASSERT_EQ(color.GetObjects()[0].GetName(), "componentType"); + ASSERT_EQ(color.GetObjects()[1].GetName(), "description"); + ASSERT_EQ(color.GetObjects()[2].GetName(), "required"); + ASSERT_EQ(color.GetObjects()[3].GetName(), "type"); + ASSERT_EQ(color.GetObjects()[0].GetString(), "UINT8"); + ASSERT_EQ(color.GetObjects()[1].GetString(), "The RGB color."); + ASSERT_TRUE(color.GetObjects()[2].GetBoolean()); + ASSERT_EQ(color.GetObjects()[3].GetString(), "VEC3"); + + const auto &name = properties.GetObjects()[1]; + ASSERT_EQ(name.GetName(), "name"); + ASSERT_EQ(name.GetObjects().size(), 3); + ASSERT_EQ(name.GetObjects()[0].GetName(), "description"); + ASSERT_EQ(name.GetObjects()[1].GetName(), "required"); + ASSERT_EQ(name.GetObjects()[2].GetName(), "type"); + ASSERT_EQ(name.GetObjects()[0].GetString(), "The name."); + ASSERT_TRUE(name.GetObjects()[1].GetBoolean()); + ASSERT_EQ(name.GetObjects()[2].GetString(), "STRING"); + + const auto &sequence = properties.GetObjects()[2]; + ASSERT_EQ(sequence.GetName(), "sequence"); + ASSERT_EQ(sequence.GetObjects().size(), 3); + ASSERT_EQ(sequence.GetObjects()[0].GetName(), "description"); + ASSERT_EQ(sequence.GetObjects()[1].GetName(), "required"); + ASSERT_EQ(sequence.GetObjects()[2].GetName(), "type"); + ASSERT_EQ(sequence.GetObjects()[0].GetString(), "The number sequence."); + ASSERT_FALSE(sequence.GetObjects()[1].GetBoolean()); + ASSERT_EQ(sequence.GetObjects()[2].GetString(), "SCALAR"); + + ASSERT_EQ(json.GetObjects()[1].GetName(), "enums"); + const auto &classifications = json.GetObjects()[1].GetObjects()[0]; + ASSERT_EQ(classifications.GetName(), "classifications"); + ASSERT_EQ(classifications.GetObjects()[0].GetName(), "description"); + ASSERT_EQ(classifications.GetObjects()[0].GetString(), + "Classifications of planets."); + ASSERT_EQ(classifications.GetObjects()[1].GetName(), "name"); + ASSERT_EQ(classifications.GetObjects()[1].GetString(), "classifications"); + ASSERT_EQ(classifications.GetObjects()[2].GetName(), "values"); + const auto &values = classifications.GetObjects()[2]; + ASSERT_EQ(values.GetArray()[0].GetObjects()[0].GetName(), "name"); + ASSERT_EQ(values.GetArray()[1].GetObjects()[0].GetName(), "name"); + ASSERT_EQ(values.GetArray()[2].GetObjects()[0].GetName(), "name"); + ASSERT_EQ(values.GetArray()[3].GetObjects()[0].GetName(), "name"); + ASSERT_EQ(values.GetArray()[4].GetObjects()[0].GetName(), "name"); + ASSERT_EQ(values.GetArray()[0].GetObjects()[0].GetString(), "Unspecified"); + ASSERT_EQ(values.GetArray()[1].GetObjects()[0].GetString(), "Gas Giant"); + ASSERT_EQ(values.GetArray()[2].GetObjects()[0].GetString(), "Waterworld"); + ASSERT_EQ(values.GetArray()[3].GetObjects()[0].GetString(), "Agriworld"); + ASSERT_EQ(values.GetArray()[4].GetObjects()[0].GetString(), "Ordnance"); + ASSERT_EQ(values.GetArray()[0].GetObjects()[1].GetName(), "value"); + ASSERT_EQ(values.GetArray()[1].GetObjects()[1].GetName(), "value"); + ASSERT_EQ(values.GetArray()[2].GetObjects()[1].GetName(), "value"); + ASSERT_EQ(values.GetArray()[3].GetObjects()[1].GetName(), "value"); + ASSERT_EQ(values.GetArray()[4].GetObjects()[1].GetName(), "value"); + ASSERT_EQ(values.GetArray()[0].GetObjects()[1].GetInteger(), 0); + ASSERT_EQ(values.GetArray()[1].GetObjects()[1].GetInteger(), 1); + ASSERT_EQ(values.GetArray()[2].GetObjects()[1].GetInteger(), 2); + ASSERT_EQ(values.GetArray()[3].GetObjects()[1].GetInteger(), 3); + ASSERT_EQ(values.GetArray()[4].GetObjects()[1].GetInteger(), 4); + + ASSERT_EQ(json.GetObjects()[2].GetName(), "id"); + ASSERT_EQ(json.GetObjects()[2].GetString(), "galaxy"); + } + + // Check property table. + constexpr int kRows = 16; + ASSERT_EQ(structural_metadata.NumPropertyTables(), 1); + const PropertyTable &table = structural_metadata.GetPropertyTable(0); + ASSERT_EQ(table.GetName(), "Galaxy far far away."); + ASSERT_EQ(table.GetClass(), "planet"); + ASSERT_EQ(table.GetCount(), kRows); + ASSERT_EQ(table.NumProperties(), 3); + + // Check property that describes RGB color components of the planet class. + { + const PropertyTable::Property &property = table.GetProperty(0); + ASSERT_EQ(property.GetName(), "color"); + + ASSERT_EQ(property.GetData().data.size(), kRows * 3); // RGB components. + ASSERT_EQ(property.GetData().target, 34962); // ARRAY_BUFFER. + + ASSERT_EQ(property.GetData().data[0], 94); // Tatooine [94, 94, 194]. + ASSERT_EQ(property.GetData().data[1], 94); + ASSERT_EQ(property.GetData().data[2], 194); + ASSERT_EQ(property.GetData().data[18], 190); // Corellia [190, 92, 108]. + ASSERT_EQ(property.GetData().data[19], 92); + ASSERT_EQ(property.GetData().data[20], 108); + ASSERT_EQ(property.GetData().data[45], 0); // UNLABELED [0, 0, 0]. + ASSERT_EQ(property.GetData().data[46], 0); + ASSERT_EQ(property.GetData().data[47], 0); + + ASSERT_TRUE(property.GetArrayOffsets().type.empty()); + ASSERT_TRUE(property.GetArrayOffsets().data.data.empty()); + ASSERT_EQ(property.GetArrayOffsets().data.target, 0); + ASSERT_TRUE(property.GetStringOffsets().type.empty()); + ASSERT_TRUE(property.GetStringOffsets().data.data.empty()); + ASSERT_EQ(property.GetStringOffsets().data.target, 0); + } + + // Check property that describes names of the planet class. + { + const PropertyTable::Property &property = table.GetProperty(1); + ASSERT_EQ(property.GetName(), "name"); + const std::vector &data = property.GetData().data; + const std::vector &offsets = property.GetStringOffsets().data.data; + + ASSERT_EQ(data.size(), 296); // Concatenated label strings. + ASSERT_EQ(property.GetData().target, 34963); // ELEMENT_ARRAY_BUFFER. + + ASSERT_EQ(property.GetStringOffsets().type, "UINT32"); + ASSERT_EQ(offsets.size(), 4 * (kRows + 1)); + ASSERT_EQ(property.GetStringOffsets().data.target, 34963); + + ASSERT_EQ(offsets[0], 0); // Tatooine 0. + ASSERT_EQ(offsets[1], 0); + ASSERT_EQ(offsets[2], 0); + ASSERT_EQ(offsets[3], 0); + ASSERT_EQ(offsets[60], 32); // UNLABELED 287. + ASSERT_EQ(offsets[61], 1); + ASSERT_EQ(offsets[62], 0); + ASSERT_EQ(offsets[63], 0); + ASSERT_EQ(offsets[64], 41); // Beyond UNLABELED 296. + ASSERT_EQ(offsets[65], 1); + ASSERT_EQ(offsets[66], 0); + ASSERT_EQ(offsets[67], 0); + + struct Name { + static std::string Extract(const std::vector &data, + const std::vector &offsets, int row) { + const int b = offsets[4 * (row + 0)] + 255 * offsets[4 * (row + 0) + 1]; + const int e = offsets[4 * (row + 1)] + 255 * offsets[4 * (row + 1) + 1]; + return std::string(data.begin() + b, data.begin() + e); + } + }; + + // Check that the names can be extracted from the data. + ASSERT_EQ(Name::Extract(data, offsets, 0), "named_class:Tatooine"); + ASSERT_EQ(Name::Extract(data, offsets, 6), "named_class:Corellia"); + ASSERT_EQ(Name::Extract(data, offsets, 15), "UNLABELED"); + + ASSERT_TRUE(property.GetArrayOffsets().type.empty()); + ASSERT_TRUE(property.GetArrayOffsets().data.data.empty()); + ASSERT_EQ(property.GetArrayOffsets().data.target, 0); + } + + // Check property that describes number sequence of the planet class. + { + const PropertyTable::Property &property = table.GetProperty(2); + ASSERT_EQ(property.GetName(), "sequence"); + const std::vector &data = property.GetData().data; + const std::vector &offsets = property.GetArrayOffsets().data.data; + + ASSERT_EQ(data.size(), 41 * 4); // Concatenated float arrays. + ASSERT_EQ(property.GetData().target, 34963); // ELEMENT_ARRAY_BUFFER. + + ASSERT_EQ(property.GetArrayOffsets().type, "UINT8"); + ASSERT_EQ(offsets.size(), 20); // kRows + 1 + padding. + ASSERT_EQ(property.GetArrayOffsets().data.target, 34963); + + ASSERT_EQ(offsets[0], 0 * 4); // Tatooine + ASSERT_EQ(offsets[1], 6 * 4); // Corusant + ASSERT_EQ(offsets[6], 16 * 4); // Corellia + ASSERT_EQ(offsets[14], 36 * 4); // Geonosis + ASSERT_EQ(offsets[15], 41 * 4); // UNLABELED (empty array). + ASSERT_EQ(offsets[16], 41 * 4); // Beyond UNLABELED (empty array). + + struct Sequence { + static std::vector Extract(const std::vector &data, + const std::vector &offsets, + int row) { + const int n = (offsets[row + 1] - offsets[row]) / 4; + std::vector result; + result.reserve(n); + for (int i = 0; i < n; ++i) { + const void *const pointer = &data[offsets[row] + 4 * i]; + result.push_back(*static_cast(pointer)); + } + return result; + } + }; + + // Check that the number sequence arrays can be extracted from the data. + ASSERT_EQ( + Sequence::Extract(data, offsets, 0), + (std::vector{0.5f, 1.5f, 2.5f, 3.5f, 4.5f, 5.5f})); // Tatooine + ASSERT_EQ(Sequence::Extract(data, offsets, 1), + (std::vector{6.5f, 7.5f})); // Corusant + ASSERT_EQ( + Sequence::Extract(data, offsets, 14), + (std::vector{36.5f, 37.5f, 38.5f, 39.5f, 40.5f})); // Geonosis + ASSERT_TRUE(Sequence::Extract(data, offsets, 15) + .empty()); // UNLABELED (empty array). + + ASSERT_TRUE(property.GetStringOffsets().type.empty()); + ASSERT_TRUE(property.GetStringOffsets().data.data.empty()); + ASSERT_EQ(property.GetStringOffsets().data.target, 0); + } +} + +#endif // DRACO_TRANSCODER_SUPPORTED + +} // namespace draco diff --git a/contrib/draco/src/draco/io/gltf_test_helper.h b/contrib/draco/src/draco/io/gltf_test_helper.h new file mode 100644 index 000000000..91aec9b08 --- /dev/null +++ b/contrib/draco/src/draco/io/gltf_test_helper.h @@ -0,0 +1,61 @@ +// Copyright 2022 The Draco Authors. +// +// 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 DRACO_IO_GLTF_DECODER_TEST_HELPER_H_ +#define DRACO_IO_GLTF_DECODER_TEST_HELPER_H_ + +#include "draco/draco_features.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED +#include "draco/scene/scene.h" + +namespace draco { + +// Helper class for testing Draco glTF encoder and decoder. +class GltfTestHelper { + public: + // Adds various mesh feature ID sets (via attributes and via textures) and + // structural metadata property table and property table schema to the box + // |scene| loaded from the test file testdata/Box/glTF/Box.gltf. + static void AddBoxMetaMeshFeatures(Scene *scene); + static void AddBoxMetaStructuralMetadata(Scene *scene); + + // Checks the box |geometry| (draco::Mesh or draco::Scene) with mesh features + // loaded from one of these test files, with or without Draco compression: + // 1. testdata/BoxMeta/glTF/BoxMeta.gltf + // 2. testdata/BoxMetaDraco/glTF/BoxMetaDraco.gltf + template + static void CheckBoxMetaMeshFeatures(const GeometryT &geometry, + bool has_draco_compression); + + // Checks the box |geometry| (draco::Mesh or draco::Scene) with structural + // metadata that includes property table and property table schema loaded from + // test file testdata/BoxMeta/glTF/BoxMeta.gltf. + template + static void CheckBoxMetaStructuralMetadata(const GeometryT &geometry) { + CheckBoxMetaStructuralMetadata(geometry.GetStructuralMetadata()); + } + + private: + static void CheckBoxMetaMeshFeatures(const Mesh &mesh, + const TextureLibrary &texture_lib, + bool has_draco_compression); + static void CheckBoxMetaStructuralMetadata( + const StructuralMetadata &structural_metadata); +}; + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED +#endif // DRACO_IO_GLTF_DECODER_TEST_HELPER_H_ diff --git a/contrib/draco/src/draco/io/gltf_utils.cc b/contrib/draco/src/draco/io/gltf_utils.cc new file mode 100644 index 000000000..bf5c048ef --- /dev/null +++ b/contrib/draco/src/draco/io/gltf_utils.cc @@ -0,0 +1,154 @@ +// Copyright 2018 The Draco Authors. +// +// 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 "draco/io/gltf_utils.h" + +#include +#include + +#ifdef DRACO_TRANSCODER_SUPPORTED +namespace draco { + +std::ostream &operator<<(std::ostream &os, const GltfValue &value) { + if (value.type_ == GltfValue::INT) { + os << value.value_int_; + } else { + os << value.value_double_; + } + return os; +} + +Indent::Indent() : indent_space_count_(2) {} + +void Indent::Increase() { indent_ += std::string(indent_space_count_, ' '); } + +void Indent::Decrease() { indent_.erase(0, indent_space_count_); } + +std::ostream &operator<<(std::ostream &os, const Indent &indent) { + return os << indent.indent_; +} + +std::ostream &operator<<(std::ostream &os, + const JsonWriter::IndentWrapper &indent) { + if (indent.writer.mode_ == JsonWriter::READABLE) { + os << indent.writer.indent_writer_; + } + return os; +} + +std::ostream &operator<<(std::ostream &os, + const JsonWriter::Separator &separator) { + if (separator.writer.mode_ == JsonWriter::READABLE) { + os << " "; + } + return os; +} + +void JsonWriter::Reset() { + last_type_ = START; + o_.clear(); + o_.str(""); +} + +void JsonWriter::BeginObject() { BeginObject(""); } + +void JsonWriter::BeginObject(const std::string &name) { + FinishPreviousLine(BEGIN); + o_ << indent_; + if (!name.empty()) { + o_ << "\"" << name << "\":" << separator_; + } + o_ << "{"; + indent_writer_.Increase(); +} + +void JsonWriter::EndObject() { + FinishPreviousLine(END); + indent_writer_.Decrease(); + o_ << indent_ << "}"; +} + +void JsonWriter::BeginArray(const std::string &name) { + FinishPreviousLine(BEGIN); + o_ << indent_ << "\"" << name << "\":" << separator_ << "["; + indent_writer_.Increase(); +} + +void JsonWriter::EndArray() { + FinishPreviousLine(END); + indent_writer_.Decrease(); + o_ << indent_ << "]"; +} + +void JsonWriter::FinishPreviousLine(OutputType curr_type) { + if (last_type_ != START) { + if ((last_type_ == VALUE && curr_type == VALUE) || + (last_type_ == VALUE && curr_type == BEGIN) || + (last_type_ == END && curr_type == BEGIN) || + (last_type_ == END && curr_type == VALUE)) { + o_ << ","; + } + if (mode_ == READABLE) { + o_ << std::endl; + } + } + last_type_ = curr_type; +} + +std::string JsonWriter::MoveData() { + const std::string str = o_.str(); + o_.str(""); + return str; +} + +std::string JsonWriter::EscapeCharacter(const std::string &str, + const char character) { + size_t start = 0; + if ((start = str.find(character, start)) != std::string::npos) { + std::string s = str; + std::string escaped_character = "\\"; + escaped_character += character; + do { + s.replace(start, 1, escaped_character); + start += escaped_character.length(); + } while ((start = s.find(character, start)) != std::string::npos); + return s; + } + return str; +} + +std::string JsonWriter::EscapeJsonSpecialCharacters(const std::string &str) { + std::string s = str; + const char backspace = '\b'; + const char form_feed = '\f'; + const char newline = '\n'; + const char carriage_return = '\r'; + const char tab = '\t'; + const char double_quote = '\"'; + const char backslash = '\\'; + + // Backslash must come first. + s = EscapeCharacter(s, backslash); + s = EscapeCharacter(s, backspace); + s = EscapeCharacter(s, form_feed); + s = EscapeCharacter(s, newline); + s = EscapeCharacter(s, carriage_return); + s = EscapeCharacter(s, tab); + s = EscapeCharacter(s, double_quote); + return s; +} + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED diff --git a/contrib/draco/src/draco/io/gltf_utils.h b/contrib/draco/src/draco/io/gltf_utils.h new file mode 100644 index 000000000..2cf12fdc7 --- /dev/null +++ b/contrib/draco/src/draco/io/gltf_utils.h @@ -0,0 +1,186 @@ +// Copyright 2018 The Draco Authors. +// +// 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 DRACO_IO_GLTF_UTILS_H_ +#define DRACO_IO_GLTF_UTILS_H_ + +#include "draco/draco_features.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED +#include +#include +#include + +namespace draco { + +// Class used to store integer or float values supported by glTF. +class GltfValue { + public: + enum ValueType { INT, DOUBLE }; + + explicit GltfValue(int8_t value) + : type_(INT), value_int_(value), value_double_(-1.0) {} + + explicit GltfValue(uint8_t value) + : type_(INT), value_int_(value), value_double_(-1.0) {} + + explicit GltfValue(int16_t value) + : type_(INT), value_int_(value), value_double_(-1.0) {} + + explicit GltfValue(uint16_t value) + : type_(INT), value_int_(value), value_double_(-1.0) {} + + explicit GltfValue(uint32_t value) + : type_(INT), value_int_(value), value_double_(-1.0) {} + + explicit GltfValue(float value) + : type_(DOUBLE), value_int_(-1), value_double_(value) {} + + friend std::ostream &operator<<(std::ostream &os, const GltfValue &value); + + private: + ValueType type_; + int64_t value_int_; + double value_double_; +}; + +// Utility class used to help with indentation of glTF file. +class Indent { + public: + Indent(); + + void Increase(); + void Decrease(); + + friend std::ostream &operator<<(std::ostream &os, const Indent &indent); + + private: + // Variables used for spacing of the glTF file. + std::string indent_; + const int indent_space_count_; +}; + +// Class used to keep track of the json state. +class JsonWriter { + public: + enum OutputType { START, BEGIN, END, VALUE }; + enum Mode { READABLE, COMPACT }; + + JsonWriter() + : last_type_(START), mode_(READABLE), indent_(*this), separator_(*this) {} + void SetMode(Mode mode) { mode_ = mode; } + + // Clear the stringstream and set last type to START. + void Reset(); + + // Every call to BeginObject should have a matching call to EndObject. + void BeginObject(); + void BeginObject(const std::string &name); + void EndObject(); + + // Every call to BeginArray should have a matching call to EndArray. + void BeginArray(const std::string &name); + void EndArray(); + + template + void OutputValue(const T &value) { + FinishPreviousLine(VALUE); + o_ << indent_ << std::setprecision(17) << value; + } + + void OutputValue(const bool &value) { + FinishPreviousLine(VALUE); + o_ << indent_ << ToString(value); + } + + void OutputValue(const std::string &name) { + const std::string escaped_name = EscapeJsonSpecialCharacters(name); + FinishPreviousLine(VALUE); + o_ << indent_ << "\"" << escaped_name << "\""; + } + + void OutputValue(const std::string &name, const std::string &value) { + const std::string escaped_name = EscapeJsonSpecialCharacters(name); + const std::string escaped_value = EscapeJsonSpecialCharacters(value); + FinishPreviousLine(VALUE); + o_ << indent_ << "\"" << escaped_name << "\":" << separator_ << "\"" + << escaped_value << "\""; + } + + void OutputValue(const std::string &name, const char *value) { + const std::string escaped_name = EscapeJsonSpecialCharacters(name); + const std::string escaped_value = EscapeJsonSpecialCharacters(value); + FinishPreviousLine(VALUE); + o_ << indent_ << "\"" << escaped_name << "\":" << separator_ << "\"" + << escaped_value << "\""; + } + + template + void OutputValue(const std::string &name, const T &value) { + const std::string escaped_name = EscapeJsonSpecialCharacters(name); + FinishPreviousLine(VALUE); + o_ << indent_ << "\"" << escaped_name << "\":" << separator_ << value; + } + + void OutputValue(const std::string &name, const bool &value) { + const std::string escaped_name = EscapeJsonSpecialCharacters(name); + FinishPreviousLine(VALUE); + o_ << indent_ << "\"" << escaped_name << "\":" << separator_ + << ToString(value); + } + + // Return the current output and then clear the stringstream. + std::string MoveData(); + + private: + // Check if a comma needs to be added to the output and then add a new line. + void FinishPreviousLine(OutputType curr_type); + + // Returns a string escaping all instances of |character| in |str|. + std::string EscapeCharacter(const std::string &str, const char character); + + // Returns a string escaping all of the Json special characters in |str|. + // Carriage return is not handled. + std::string EscapeJsonSpecialCharacters(const std::string &str); + + // Returns string representation of a Boolean |value|. + static std::string ToString(bool value) { return value ? "true" : "false"; } + + // Helper struct used for conditional indent writing to the output stream. + struct IndentWrapper { + explicit IndentWrapper(const JsonWriter &writer) : writer(writer) {} + const JsonWriter &writer; + }; + friend std::ostream &operator<<(std::ostream &os, + const IndentWrapper &indent); + + // Helper struct used for conditional separator writing to the output stream. + struct Separator { + explicit Separator(const JsonWriter &writer) : writer(writer) {} + const JsonWriter &writer; + }; + friend std::ostream &operator<<(std::ostream &os, const Separator &separator); + + std::stringstream o_; + Indent indent_writer_; + OutputType last_type_; + Mode mode_; + IndentWrapper indent_; + Separator separator_; +}; + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED +#endif // DRACO_IO_GLTF_UTILS_H_ diff --git a/contrib/draco/src/draco/io/gltf_utils_test.cc b/contrib/draco/src/draco/io/gltf_utils_test.cc new file mode 100644 index 000000000..01a2d144c --- /dev/null +++ b/contrib/draco/src/draco/io/gltf_utils_test.cc @@ -0,0 +1,366 @@ +// Copyright 2018 The Draco Authors. +// +// 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 "draco/io/gltf_utils.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED +#include +#include + +#include "draco/core/draco_test_base.h" +#include "draco/core/draco_test_utils.h" + +namespace draco { + +class GltfUtilsTest : public ::testing::Test { + protected: + void CompareGolden(JsonWriter *json_writer, const std::string &golden_str) { + const std::string json = json_writer->MoveData(); + ASSERT_EQ(golden_str, json); + } +}; + +TEST_F(GltfUtilsTest, TestNoData) { + const std::string golden = ""; + JsonWriter json_writer; + CompareGolden(&json_writer, golden); +} + +TEST_F(GltfUtilsTest, TestValues) { + JsonWriter json_writer; + json_writer.OutputValue(0); + CompareGolden(&json_writer, "0"); + + json_writer.Reset(); + json_writer.OutputValue(1); + CompareGolden(&json_writer, "1"); + + json_writer.Reset(); + json_writer.OutputValue(-1); + CompareGolden(&json_writer, "-1"); + + json_writer.Reset(); + json_writer.OutputValue(0.0); + CompareGolden(&json_writer, "0"); + + json_writer.Reset(); + json_writer.OutputValue(1.0); + CompareGolden(&json_writer, "1"); + + json_writer.Reset(); + json_writer.OutputValue(0.25); + CompareGolden(&json_writer, "0.25"); + + json_writer.Reset(); + json_writer.OutputValue(-0.25); + CompareGolden(&json_writer, "-0.25"); + + json_writer.Reset(); + json_writer.OutputValue(false); + CompareGolden(&json_writer, "false"); + + json_writer.Reset(); + json_writer.OutputValue(true); + CompareGolden(&json_writer, "true"); + + json_writer.Reset(); + json_writer.OutputValue("test int", -1); + CompareGolden(&json_writer, "\"test int\": -1"); + + json_writer.Reset(); + json_writer.OutputValue("test float", -10.25); + CompareGolden(&json_writer, "\"test float\": -10.25"); + + json_writer.Reset(); + json_writer.OutputValue("test char*", "I am the string!"); + CompareGolden(&json_writer, "\"test char*\": \"I am the string!\""); + + json_writer.Reset(); + const std::string value = "I am the string!"; + json_writer.OutputValue("test string", value); + CompareGolden(&json_writer, "\"test string\": \"I am the string!\""); + + json_writer.Reset(); + json_writer.OutputValue("test bool", false); + CompareGolden(&json_writer, "\"test bool\": false"); + + json_writer.Reset(); + json_writer.OutputValue("test bool", true); + CompareGolden(&json_writer, "\"test bool\": true"); +} + +TEST_F(GltfUtilsTest, TestSpecialCharacters) { + JsonWriter json_writer; + const std::string test_double_quote = "I am double quote\""; + json_writer.OutputValue("test double quote", test_double_quote); + CompareGolden(&json_writer, + "\"test double quote\": \"I am double quote\\\"\""); + + json_writer.Reset(); + const std::string test_backspace = "I am backspace\b"; + json_writer.OutputValue("test backspace", test_backspace); + CompareGolden(&json_writer, "\"test backspace\": \"I am backspace\\\b\""); + + json_writer.Reset(); + const std::string test_form_feed = "I am form feed\f"; + json_writer.OutputValue("test form feed", test_form_feed); + CompareGolden(&json_writer, "\"test form feed\": \"I am form feed\\\f\""); + + json_writer.Reset(); + const std::string test_newline = "I am newline\n"; + json_writer.OutputValue("test newline", test_newline); + CompareGolden(&json_writer, "\"test newline\": \"I am newline\\\n\""); + + json_writer.Reset(); + const std::string test_tab = "I am tab\t"; + json_writer.OutputValue("test tab", test_tab); + CompareGolden(&json_writer, "\"test tab\": \"I am tab\\\t\""); + + json_writer.Reset(); + const std::string test_backslash = "I am backslash\\"; + json_writer.OutputValue("test backslash", test_backslash); + CompareGolden(&json_writer, "\"test backslash\": \"I am backslash\\\\\""); + + json_writer.Reset(); + const std::string test_multiple_special_characters = "\"break\"and\\more\"\\"; + json_writer.OutputValue("test multiple_special_characters", + test_multiple_special_characters); + CompareGolden(&json_writer, + "\"test multiple_special_characters\": " + "\"\\\"break\\\"and\\\\more\\\"\\\\\""); +} + +TEST_F(GltfUtilsTest, TestObjects) { + JsonWriter json_writer; + json_writer.BeginObject(); + json_writer.EndObject(); + CompareGolden(&json_writer, "{\n}"); + + json_writer.Reset(); + json_writer.BeginObject("object"); + json_writer.EndObject(); + CompareGolden(&json_writer, "\"object\": {\n}"); + + json_writer.Reset(); + json_writer.BeginObject("object"); + json_writer.OutputValue(0); + json_writer.EndObject(); + CompareGolden(&json_writer, "\"object\": {\n 0\n}"); + + json_writer.Reset(); + json_writer.BeginObject("object"); + json_writer.OutputValue(0); + json_writer.OutputValue(1); + json_writer.OutputValue(2); + json_writer.OutputValue(3); + json_writer.EndObject(); + CompareGolden(&json_writer, "\"object\": {\n 0,\n 1,\n 2,\n 3\n}"); + + json_writer.Reset(); + json_writer.BeginObject("object1"); + json_writer.EndObject(); + json_writer.BeginObject("object2"); + json_writer.EndObject(); + CompareGolden(&json_writer, "\"object1\": {\n},\n\"object2\": {\n}"); + + json_writer.Reset(); + json_writer.BeginObject("object1"); + json_writer.BeginObject("object2"); + json_writer.EndObject(); + json_writer.EndObject(); + CompareGolden(&json_writer, "\"object1\": {\n \"object2\": {\n }\n}"); +} + +TEST_F(GltfUtilsTest, TestArrays) { + JsonWriter json_writer; + json_writer.BeginArray("array"); + json_writer.EndArray(); + CompareGolden(&json_writer, "\"array\": [\n]"); + + json_writer.Reset(); + json_writer.BeginArray("array"); + json_writer.OutputValue(0); + json_writer.EndArray(); + CompareGolden(&json_writer, "\"array\": [\n 0\n]"); + + json_writer.Reset(); + json_writer.BeginArray("array"); + json_writer.OutputValue(0); + json_writer.OutputValue(1); + json_writer.OutputValue(2); + json_writer.OutputValue(3); + json_writer.EndArray(); + CompareGolden(&json_writer, "\"array\": [\n 0,\n 1,\n 2,\n 3\n]"); + + json_writer.Reset(); + json_writer.BeginArray("array1"); + json_writer.EndArray(); + json_writer.BeginArray("array2"); + json_writer.EndArray(); + CompareGolden(&json_writer, "\"array1\": [\n],\n\"array2\": [\n]"); + + json_writer.Reset(); + json_writer.BeginArray("array1"); + json_writer.BeginArray("array2"); + json_writer.EndArray(); + json_writer.EndArray(); + CompareGolden(&json_writer, "\"array1\": [\n \"array2\": [\n ]\n]"); +} + +TEST_F(GltfUtilsTest, TestGltfValues) { + JsonWriter json_writer; + const int8_t int8_value_min = std::numeric_limits::min(); + const int8_t int8_value_max = std::numeric_limits::max(); + const GltfValue int8_value_low(int8_value_min); + const GltfValue int8_value_high(int8_value_max); + json_writer.OutputValue(int8_value_low); + json_writer.OutputValue(int8_value_high); + CompareGolden(&json_writer, "-128,\n127"); + + json_writer.Reset(); + const uint8_t uint8_value_min = std::numeric_limits::min(); + const uint8_t uint8_value_max = std::numeric_limits::max(); + const GltfValue uint8_value_low(uint8_value_min); + const GltfValue uint8_value_high(uint8_value_max); + json_writer.OutputValue(uint8_value_low); + json_writer.OutputValue(uint8_value_high); + CompareGolden(&json_writer, "0,\n255"); + + json_writer.Reset(); + const int16_t int16_value_min = std::numeric_limits::min(); + const int16_t int16_value_max = std::numeric_limits::max(); + const GltfValue int16_value_low(int16_value_min); + const GltfValue int16_value_high(int16_value_max); + json_writer.OutputValue(int16_value_low); + json_writer.OutputValue(int16_value_high); + CompareGolden(&json_writer, "-32768,\n32767"); + + json_writer.Reset(); + const uint16_t uint16_value_min = std::numeric_limits::min(); + const uint16_t uint16_value_max = std::numeric_limits::max(); + const GltfValue uint16_value_low(uint16_value_min); + const GltfValue uint16_value_high(uint16_value_max); + json_writer.OutputValue(uint16_value_low); + json_writer.OutputValue(uint16_value_high); + CompareGolden(&json_writer, "0,\n65535"); + + json_writer.Reset(); + const uint32_t uint32_value_min = std::numeric_limits::min(); + const uint32_t uint32_value_max = std::numeric_limits::max(); + const GltfValue uint32_value_low(uint32_value_min); + const GltfValue uint32_value_high(uint32_value_max); + json_writer.OutputValue(uint32_value_low); + json_writer.OutputValue(uint32_value_high); + CompareGolden(&json_writer, "0,\n4294967295"); + + json_writer.Reset(); + const float float_value_min = std::numeric_limits::min(); + const float float_value_max = std::numeric_limits::max(); + const GltfValue float_value_low(float_value_min); + const GltfValue float_value_high(float_value_max); + json_writer.OutputValue(float_value_low); + json_writer.OutputValue(float_value_high); + CompareGolden(&json_writer, + "1.1754943508222875e-38,\n3.4028234663852886e+38"); + + json_writer.Reset(); + const GltfValue float_value_0(0.1f); + const GltfValue float_value_1(1.f); + json_writer.OutputValue(float_value_0); + json_writer.OutputValue(float_value_1); + CompareGolden(&json_writer, "0.10000000149011612,\n1"); +} + +TEST_F(GltfUtilsTest, TestObjectsCompact) { + JsonWriter json_writer; + json_writer.SetMode(JsonWriter::COMPACT); + json_writer.BeginObject(); + json_writer.EndObject(); + CompareGolden(&json_writer, "{}"); + + json_writer.Reset(); + json_writer.BeginObject("object"); + json_writer.EndObject(); + CompareGolden(&json_writer, "\"object\":{}"); + + json_writer.Reset(); + json_writer.BeginObject("object"); + json_writer.OutputValue(0); + json_writer.EndObject(); + CompareGolden(&json_writer, "\"object\":{0}"); + + json_writer.Reset(); + json_writer.BeginObject("object"); + json_writer.OutputValue(0); + json_writer.OutputValue(1); + json_writer.OutputValue(2); + json_writer.OutputValue(3); + json_writer.EndObject(); + CompareGolden(&json_writer, "\"object\":{0,1,2,3}"); + + json_writer.Reset(); + json_writer.BeginObject("object1"); + json_writer.EndObject(); + json_writer.BeginObject("object2"); + json_writer.EndObject(); + CompareGolden(&json_writer, "\"object1\":{},\"object2\":{}"); + + json_writer.Reset(); + json_writer.BeginObject("object1"); + json_writer.BeginObject("object2"); + json_writer.EndObject(); + json_writer.EndObject(); + CompareGolden(&json_writer, "\"object1\":{\"object2\":{}}"); +} + +TEST_F(GltfUtilsTest, TestArraysCompact) { + JsonWriter json_writer; + json_writer.SetMode(JsonWriter::COMPACT); + json_writer.BeginArray("array"); + json_writer.EndArray(); + CompareGolden(&json_writer, "\"array\":[]"); + + json_writer.Reset(); + json_writer.BeginArray("array"); + json_writer.OutputValue(0); + json_writer.EndArray(); + CompareGolden(&json_writer, "\"array\":[0]"); + + json_writer.Reset(); + json_writer.BeginArray("array"); + json_writer.OutputValue(0); + json_writer.OutputValue(1); + json_writer.OutputValue(2); + json_writer.OutputValue(3); + json_writer.EndArray(); + CompareGolden(&json_writer, "\"array\":[0,1,2,3]"); + + json_writer.Reset(); + json_writer.BeginArray("array1"); + json_writer.EndArray(); + json_writer.BeginArray("array2"); + json_writer.EndArray(); + CompareGolden(&json_writer, "\"array1\":[],\"array2\":[]"); + + json_writer.Reset(); + json_writer.BeginArray("array1"); + json_writer.BeginArray("array2"); + json_writer.EndArray(); + json_writer.EndArray(); + CompareGolden(&json_writer, "\"array1\":[\"array2\":[]]"); +} + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED diff --git a/contrib/draco/src/draco/io/image_compression_options.h b/contrib/draco/src/draco/io/image_compression_options.h new file mode 100644 index 000000000..722bdbd64 --- /dev/null +++ b/contrib/draco/src/draco/io/image_compression_options.h @@ -0,0 +1,31 @@ +// Copyright 2020 The Draco Authors. +// +// 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 DRACO_IO_IMAGE_COMPRESSION_OPTIONS_H_ +#define DRACO_IO_IMAGE_COMPRESSION_OPTIONS_H_ + +#include "draco/draco_features.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED +#include + +namespace draco { + +// Enum defining image compression formats. +enum class ImageFormat { NONE, PNG, JPEG, BASIS, WEBP }; + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED +#endif // DRACO_IO_IMAGE_COMPRESSION_OPTIONS_H_ diff --git a/contrib/draco/src/draco/io/mesh_io.cc b/contrib/draco/src/draco/io/mesh_io.cc index e0dc69c6f..4975d9236 100644 --- a/contrib/draco/src/draco/io/mesh_io.cc +++ b/contrib/draco/src/draco/io/mesh_io.cc @@ -18,8 +18,18 @@ #include #include "draco/io/file_utils.h" +#include "draco/io/file_writer_interface.h" #include "draco/io/obj_decoder.h" #include "draco/io/ply_decoder.h" +#include "draco/io/stl_decoder.h" +#ifdef DRACO_TRANSCODER_SUPPORTED +#include "draco/compression/draco_compression_options.h" +#include "draco/compression/encode.h" +#include "draco/io/gltf_decoder.h" +#include "draco/io/gltf_encoder.h" +#include "draco/io/obj_encoder.h" +#include "draco/io/ply_encoder.h" +#endif namespace draco { @@ -46,27 +56,40 @@ StatusOr> ReadMeshFromFile( std::unique_ptr mesh(new Mesh()); // Analyze file extension. const std::string extension = LowercaseFileExtension(file_name); - if (extension != "gltf" && mesh_files) { - // The GLTF decoder will fill |mesh_files|, but for other file types we set - // the root file here to avoid duplicating code. + if (extension != "gltf" && extension != "obj" && mesh_files) { + // The GLTF/OBJ decoder will fill |mesh_files|, but for other file types we + // set the root file here to avoid duplicating code. mesh_files->push_back(file_name); } if (extension == "obj") { // Wavefront OBJ file format. ObjDecoder obj_decoder; obj_decoder.set_use_metadata(options.GetBool("use_metadata", false)); - const Status obj_status = obj_decoder.DecodeFromFile(file_name, mesh.get()); + obj_decoder.set_preserve_polygons(options.GetBool("preserve_polygons")); + const Status obj_status = + obj_decoder.DecodeFromFile(file_name, mesh.get(), mesh_files); if (!obj_status.ok()) { return obj_status; } return std::move(mesh); } if (extension == "ply") { - // Wavefront PLY file format. + // Stanford PLY file format. PlyDecoder ply_decoder; DRACO_RETURN_IF_ERROR(ply_decoder.DecodeFromFile(file_name, mesh.get())); return std::move(mesh); } + if (extension == "stl") { + // STL file format. + StlDecoder stl_decoder; + return stl_decoder.DecodeFromFile(file_name); + } +#ifdef DRACO_TRANSCODER_SUPPORTED + if (extension == "gltf" || extension == "glb") { + GltfDecoder gltf_decoder; + return gltf_decoder.DecodeFromFile(file_name, mesh_files); + } +#endif // Otherwise not an obj file. Assume the file was encoded with one of the // draco encoding methods. diff --git a/contrib/draco/src/draco/io/obj_decoder.cc b/contrib/draco/src/draco/io/obj_decoder.cc index 9b4eab626..c233c2b56 100644 --- a/contrib/draco/src/draco/io/obj_decoder.cc +++ b/contrib/draco/src/draco/io/obj_decoder.cc @@ -14,8 +14,10 @@ // #include "draco/io/obj_decoder.h" +#include #include #include +#include #include "draco/io/file_utils.h" #include "draco/io/parser_utils.h" @@ -36,15 +38,25 @@ ObjDecoder::ObjDecoder() norm_att_id_(-1), material_att_id_(-1), sub_obj_att_id_(-1), + added_edge_att_id_(-1), deduplicate_input_values_(true), last_material_id_(0), use_metadata_(false), + preserve_polygons_(false), + has_polygons_(false), + mesh_files_(nullptr), out_mesh_(nullptr), out_point_cloud_(nullptr) {} Status ObjDecoder::DecodeFromFile(const std::string &file_name, Mesh *out_mesh) { + return DecodeFromFile(file_name, out_mesh, nullptr); +} + +Status ObjDecoder::DecodeFromFile(const std::string &file_name, Mesh *out_mesh, + std::vector *mesh_files) { out_mesh_ = out_mesh; + mesh_files_ = mesh_files; return DecodeFromFile(file_name, static_cast(out_mesh)); } @@ -90,6 +102,10 @@ Status ObjDecoder::DecodeInternal() { return status; } + if (mesh_files_ && !input_file_name_.empty()) { + mesh_files_->push_back(input_file_name_); + } + bool use_identity_mapping = false; if (num_obj_faces_ == 0) { // Mesh has no faces. In this case we try to read the geometry as a point @@ -146,6 +162,24 @@ Status ObjDecoder::DecodeInternal() { norm_att_id_ = out_point_cloud_->AddAttribute(va, use_identity_mapping, num_normals_); } + if (preserve_polygons_ && has_polygons_) { + // Create attribute for polygon reconstruction. + GeometryAttribute va; + va.Init(GeometryAttribute::GENERIC, nullptr, 1, DT_UINT8, false, 1, 0); + PointCloud *const pc = out_point_cloud_; + added_edge_att_id_ = pc->AddAttribute(va, false, 2); + + // Set attribute values to zero and one representing old edge and new edge. + for (const uint8_t i : {0, 1}) { + const AttributeValueIndex avi(i); + pc->attribute(added_edge_att_id_)->SetAttributeValue(avi, &i); + } + + // Add attribute metadata with name. + std::unique_ptr metadata(new draco::AttributeMetadata()); + metadata->AddEntryString("name", "added_edges"); + pc->AddAttributeMetadata(added_edge_att_id_, std::move(metadata)); + } if (num_materials_ > 0 && num_obj_faces_ > 0) { GeometryAttribute va; const auto geometry_attribute_type = GeometryAttribute::GENERIC; @@ -381,6 +415,7 @@ bool ObjDecoder::ParseTexCoord(Status *status) { } bool ObjDecoder::ParseFace(Status *status) { + constexpr int kMaxCorners = 8; char c; if (!buffer()->Peek(&c)) { return false; @@ -391,37 +426,35 @@ bool ObjDecoder::ParseFace(Status *status) { // Face definition found! buffer()->Advance(1); if (!counting_mode_) { - std::array indices[4]; - // Parse face indices (we try to look for up to four to support quads). + std::array indices[kMaxCorners]; + // Parse face indices. int num_valid_indices = 0; - for (int i = 0; i < 4; ++i) { + for (int i = 0; i < kMaxCorners; ++i) { if (!ParseVertexIndices(&indices[i])) { - if (i == 3) { - break; // It's OK if there is no fourth vertex index. + if (i >= 3) { + break; // It's OK if there is no fourth or higher vertex index. } *status = Status(Status::DRACO_ERROR, "Failed to parse vertex indices"); return true; } ++num_valid_indices; } - // Process the first face. - for (int i = 0; i < 3; ++i) { - const PointIndex vert_id(3 * num_obj_faces_ + i); - MapPointToVertexIndices(vert_id, indices[i]); - } - ++num_obj_faces_; - if (num_valid_indices == 4) { - // Add an additional triangle for the quad. - // - // 3----2 - // | / | - // | / | - // 0----1 - // - const PointIndex vert_id(3 * num_obj_faces_); - MapPointToVertexIndices(vert_id, indices[0]); - MapPointToVertexIndices(vert_id + 1, indices[2]); - MapPointToVertexIndices(vert_id + 2, indices[3]); + // Split quads and other n-gons into n - 2 triangles. + const int nt = num_valid_indices - 2; + // Iterate over triangles. + for (int t = 0; t < nt; t++) { + // Iterate over corners. + for (int c = 0; c < 3; c++) { + const PointIndex vert_id(3 * num_obj_faces_ + c); + const int triangulated_index = Triangulate(t, c); + MapPointToVertexIndices(vert_id, indices[triangulated_index]); + // Save info about new edges that will allow us to reconstruct polygons. + if (added_edge_att_id_ >= 0) { + const AttributeValueIndex avi(IsNewEdge(nt, t, c)); + out_point_cloud_->attribute(added_edge_att_id_) + ->SetPointMapEntry(vert_id, avi); + } + } ++num_obj_faces_; } } else { @@ -443,12 +476,14 @@ bool ObjDecoder::ParseFace(Status *status) { } } } - if (num_indices < 3 || num_indices > 4) { - *status = - Status(Status::DRACO_ERROR, "Invalid number of indices on a face"); + if (num_indices > 3) { + has_polygons_ = true; + } + if (num_indices < 3 || num_indices > kMaxCorners) { + *status = ErrorStatus("Invalid number of indices on a face"); return false; } - // Either one or two new triangles. + // Either one or more new triangles. num_obj_faces_ += num_indices - 2; } parser::SkipLine(buffer()); @@ -478,6 +513,9 @@ bool ObjDecoder::ParseMaterialLib(Status *status) { parser::SkipLine(&line_buffer); if (!material_file_name_.empty()) { + if (mesh_files_) { + mesh_files_->push_back(material_file_name_); + } if (!ParseMaterialFile(material_file_name_, status)) { // Silently ignore problems with material files for now. return true; @@ -705,4 +743,44 @@ bool ObjDecoder::ParseMaterialFileDefinition(Status * /* status */) { return true; } +// Methods Triangulate() and IsNewEdge() are used for polygon triangulation and +// representation as an attribute for reconstruction in the decoder. +// +// Polygon reconstruction attribute is associated with every triangle corner and +// has values zero or one. Zero indicates that an edge opposite to the corner is +// present in the original mesh (dashed lines), and one indicates that the +// opposite edge has been added during polygon triangulation (dotted lines). +// +// Polygon triangulation is illustrated below. Pentagon ABCDE is split into +// three triangles ABC, ACD, ADE. It is sufficient to set polygon reconstruction +// attribute at corners ABC and ACD. The attribute at the second corner of all +// triangles except for the last is set to one. +// +// C D +// * --------- * +// /. 1 0 .| +// / . . | +// / . . | +// / 0 . . 0 | +// / . . | +// B * 1 . . | +// \ . . | +// \ 0 . 0 . | +// \ . . | +// \ . . | +// \.. 0 0 | +// *-----------* +// A E +// +inline int ObjDecoder::Triangulate(int tri_index, int tri_corner) { + return tri_corner == 0 ? 0 : tri_index + tri_corner; +} + +inline bool ObjDecoder::IsNewEdge(int tri_count, int tri_index, + int tri_corner) { + // All but the last triangle of the triangulated polygon have an added edge + // opposite of corner 1. + return tri_index != tri_count - 1 && tri_corner == 1; +} + } // namespace draco diff --git a/contrib/draco/src/draco/io/obj_decoder.h b/contrib/draco/src/draco/io/obj_decoder.h index baeab5b0c..18dc9aadd 100644 --- a/contrib/draco/src/draco/io/obj_decoder.h +++ b/contrib/draco/src/draco/io/obj_decoder.h @@ -34,8 +34,12 @@ class ObjDecoder { ObjDecoder(); // Decodes an obj file stored in the input file. - // Returns nullptr if the decoding failed. + // Optional argument |mesh_files| will be populated with all paths to files + // relevant to the loaded mesh. Status DecodeFromFile(const std::string &file_name, Mesh *out_mesh); + Status DecodeFromFile(const std::string &file_name, Mesh *out_mesh, + std::vector *mesh_files); + Status DecodeFromFile(const std::string &file_name, PointCloud *out_point_cloud); @@ -50,6 +54,8 @@ class ObjDecoder { // Flag for whether using metadata to record other information in the obj // file, e.g. material names, object names. void set_use_metadata(bool flag) { use_metadata_ = flag; } + // Enables preservation of polygons. + void set_preserve_polygons(bool flag) { preserve_polygons_ = flag; } protected: Status DecodeInternal(); @@ -88,6 +94,11 @@ class ObjDecoder { bool ParseMaterialFile(const std::string &file_name, Status *status); bool ParseMaterialFileDefinition(Status *status); + // Methods related to polygon triangulation and preservation. + static int Triangulate(int tri_index, int tri_corner); + static bool IsNewEdge(int tri_count, int tri_index, int tri_corner); + + private: // If set to true, the parser will count the number of various definitions // but it will not parse the actual data or add any new entries to the mesh. bool counting_mode_; @@ -102,7 +113,8 @@ class ObjDecoder { int tex_att_id_; int norm_att_id_; int material_att_id_; - int sub_obj_att_id_; // Attribute id for storing sub-objects. + int sub_obj_att_id_; // Attribute id for storing sub-objects. + int added_edge_att_id_; // Attribute id for polygon reconstruction. bool deduplicate_input_values_; @@ -116,6 +128,12 @@ class ObjDecoder { bool use_metadata_; + // Polygon preservation flags. + bool preserve_polygons_; + bool has_polygons_; + + std::vector *mesh_files_; + DecoderBuffer buffer_; // Data structure that stores the decoded data. |out_point_cloud_| must be diff --git a/contrib/draco/src/draco/io/obj_decoder_test.cc b/contrib/draco/src/draco/io/obj_decoder_test.cc index b19fe6e2c..a46a15a8b 100644 --- a/contrib/draco/src/draco/io/obj_decoder_test.cc +++ b/contrib/draco/src/draco/io/obj_decoder_test.cc @@ -54,6 +54,20 @@ class ObjDecoderTest : public ::testing::Test { return geometry; } + template + std::unique_ptr DecodeObjWithPolygons( + const std::string &file_name, bool regularize_quads, + bool store_added_edges_per_vertex) const { + const std::string path = GetTestFileFullPath(file_name); + ObjDecoder decoder; + decoder.set_preserve_polygons(true); + std::unique_ptr geometry(new Geometry()); + if (!decoder.DecodeFromFile(path, geometry.get()).ok()) { + return nullptr; + } + return geometry; + } + void test_decoding(const std::string &file_name) { const std::unique_ptr mesh(DecodeObj(file_name)); ASSERT_NE(mesh, nullptr) << "Failed to load test model " << file_name; @@ -113,7 +127,7 @@ TEST_F(ObjDecoderTest, SubObjectsWithMetadata) { ASSERT_EQ(sub_obj_id, 2); } -TEST_F(ObjDecoderTest, QuadOBJ) { +TEST_F(ObjDecoderTest, QuadTriangulateOBJ) { // Tests loading an Obj with quad faces. const std::string file_name = "cube_quads.obj"; const std::unique_ptr mesh(DecodeObj(file_name)); @@ -124,11 +138,114 @@ TEST_F(ObjDecoderTest, QuadOBJ) { ASSERT_EQ(mesh->num_points(), 4 * 6); // Four points per quad face. } -TEST_F(ObjDecoderTest, ComplexPolyOBJ) { - // Tests that we fail to load an obj with complex polygon (expected failure). - const std::string file_name = "invalid/complex_poly.obj"; +TEST_F(ObjDecoderTest, QuadPreserveOBJ) { + // Tests loading an Obj with quad faces preserved as an attribute. + const std::string file_name = "cube_quads.obj"; + constexpr bool kRegularizeQuads = false; + constexpr bool kStoreAddedEdgesPerVertex = false; + const std::unique_ptr mesh(DecodeObjWithPolygons( + file_name, kRegularizeQuads, kStoreAddedEdgesPerVertex)); + ASSERT_NE(mesh, nullptr) << "Failed to load test model " << file_name; + ASSERT_EQ(mesh->num_faces(), 12); + + ASSERT_EQ(mesh->num_attributes(), 4); + ASSERT_EQ(mesh->num_points(), 4 * 6); // Four points per quad face. + + // Expect a new generic attribute. + ASSERT_EQ(mesh->attribute(3)->attribute_type(), GeometryAttribute::GENERIC); + + // Expect the new attribute to have two values to describe old and new edge. + ASSERT_EQ(mesh->attribute(3)->size(), 2); + const auto new_edge_value = + mesh->attribute(3)->GetValue(AttributeValueIndex(0))[0]; + const auto old_edge_value = + mesh->attribute(3)->GetValue(AttributeValueIndex(1))[0]; + ASSERT_EQ(new_edge_value, 0); + ASSERT_EQ(old_edge_value, 1); + + // Expect one new edge on each of the six cube quads. + for (int i = 0; i < 6; i++) { + ASSERT_EQ(mesh->attribute(3)->mapped_index(PointIndex(4 * i + 0)), 0); + // New edge. + ASSERT_EQ(mesh->attribute(3)->mapped_index(PointIndex(4 * i + 1)), 1); + ASSERT_EQ(mesh->attribute(3)->mapped_index(PointIndex(4 * i + 2)), 0); + ASSERT_EQ(mesh->attribute(3)->mapped_index(PointIndex(4 * i + 3)), 0); + } + + // Expect metadata entry on the new attribute. + const AttributeMetadata *const metadata = + mesh->GetAttributeMetadataByAttributeId(3); + ASSERT_NE(metadata, nullptr); + ASSERT_TRUE(metadata->sub_metadatas().empty()); + ASSERT_EQ(metadata->entries().size(), 1); + std::string name; + metadata->GetEntryString("name", &name); + ASSERT_EQ(name, "added_edges"); +} + +TEST_F(ObjDecoderTest, OctagonTriangulatedOBJ) { + // Tests that we can load an obj with an octagon triangulated. + const std::string file_name = "octagon.obj"; const std::unique_ptr mesh(DecodeObj(file_name)); - ASSERT_EQ(mesh, nullptr); + ASSERT_NE(mesh, nullptr) << "Failed to load test model " << file_name; + + ASSERT_EQ(mesh->num_attributes(), 1); + ASSERT_EQ(mesh->num_points(), 8); + ASSERT_EQ(mesh->attribute(0)->attribute_type(), GeometryAttribute::POSITION); + ASSERT_EQ(mesh->attribute(0)->size(), 8); +} + +TEST_F(ObjDecoderTest, OctagonPreservedOBJ) { + // Tests that we can load an obj with an octagon preserved as an attribute. + const std::string file_name = "octagon.obj"; + constexpr bool kRegularizeQuads = false; + constexpr bool kStoreAddedEdgesPerVertex = false; + const std::unique_ptr mesh(DecodeObjWithPolygons( + file_name, kRegularizeQuads, kStoreAddedEdgesPerVertex)); + ASSERT_NE(mesh, nullptr) << "Failed to load test model " << file_name; + + ASSERT_EQ(mesh->num_attributes(), 2); + ASSERT_EQ(mesh->attribute(0)->attribute_type(), GeometryAttribute::POSITION); + ASSERT_EQ(mesh->attribute(0)->size(), 8); + + // Expect a new generic attribute. + ASSERT_EQ(mesh->attribute(1)->attribute_type(), GeometryAttribute::GENERIC); + + // There are four vertices with both old and new edges in their ring. + ASSERT_EQ(mesh->num_points(), 8 + 4); + + // Expect the new attribute to have two values to describe old and new edge. + ASSERT_EQ(mesh->attribute(1)->size(), 2); + const auto new_edge_value = + mesh->attribute(1)->GetValue(AttributeValueIndex(0))[0]; + const auto old_edge_value = + mesh->attribute(1)->GetValue(AttributeValueIndex(1))[0]; + ASSERT_EQ(new_edge_value, 0); + ASSERT_EQ(old_edge_value, 1); + + // Five new edges are introduced while triangulating as octagon. + ASSERT_EQ(mesh->attribute(1)->mapped_index(PointIndex(0)), 0); + ASSERT_EQ(mesh->attribute(1)->mapped_index(PointIndex(1)), 1); // New edge. + ASSERT_EQ(mesh->attribute(1)->mapped_index(PointIndex(2)), 0); + ASSERT_EQ(mesh->attribute(1)->mapped_index(PointIndex(3)), 1); // New edge. + ASSERT_EQ(mesh->attribute(1)->mapped_index(PointIndex(4)), 0); + ASSERT_EQ(mesh->attribute(1)->mapped_index(PointIndex(5)), 1); // New edge. + ASSERT_EQ(mesh->attribute(1)->mapped_index(PointIndex(6)), 0); + ASSERT_EQ(mesh->attribute(1)->mapped_index(PointIndex(7)), 1); // New edge. + ASSERT_EQ(mesh->attribute(1)->mapped_index(PointIndex(8)), 0); + ASSERT_EQ(mesh->attribute(1)->mapped_index(PointIndex(9)), 1); // New edge. + ASSERT_EQ(mesh->attribute(1)->mapped_index(PointIndex(10)), 0); + ASSERT_EQ(mesh->attribute(1)->mapped_index(PointIndex(11)), 0); + + // Expect metadata entry on the new attribute. + const AttributeMetadata *const metadata = + mesh->GetAttributeMetadataByAttributeId(1); + ASSERT_NE(metadata, nullptr); + ASSERT_TRUE(metadata->sub_metadatas().empty()); + ASSERT_EQ(metadata->entries().size(), 1); + std::string name; + metadata->GetEntryString("name", &name); + ASSERT_EQ(name, "added_edges"); } TEST_F(ObjDecoderTest, EmptyNameOBJ) { @@ -167,7 +284,6 @@ TEST_F(ObjDecoderTest, WrongAttributeMapping) { TEST_F(ObjDecoderTest, TestObjDecodingAll) { // test if we can read all obj that are currently in test folder. test_decoding("bunny_norm.obj"); - // test_decoding("complex_poly.obj"); // not supported see test above test_decoding("cube_att.obj"); test_decoding("cube_att_partial.obj"); test_decoding("cube_att_sub_o.obj"); diff --git a/contrib/draco/src/draco/io/obj_encoder.cc b/contrib/draco/src/draco/io/obj_encoder.cc index 29c6ca8f0..1ddfd92bd 100644 --- a/contrib/draco/src/draco/io/obj_encoder.cc +++ b/contrib/draco/src/draco/io/obj_encoder.cc @@ -16,8 +16,10 @@ #include +#include "draco/attributes/geometry_attribute.h" #include "draco/io/file_writer_factory.h" #include "draco/io/file_writer_interface.h" +#include "draco/mesh/mesh_misc_functions.h" #include "draco/metadata/geometry_metadata.h" namespace draco { @@ -28,6 +30,7 @@ ObjEncoder::ObjEncoder() normal_att_(nullptr), material_att_(nullptr), sub_obj_att_(nullptr), + added_edges_att_(nullptr), out_buffer_(nullptr), in_point_cloud_(nullptr), in_mesh_(nullptr), @@ -78,11 +81,15 @@ bool ObjEncoder::EncodeInternal() { normal_att_ = nullptr; material_att_ = nullptr; sub_obj_att_ = nullptr; + added_edges_att_ = nullptr; current_sub_obj_id_ = -1; current_material_id_ = -1; if (!GetSubObjects()) { return false; } + if (in_mesh_ && !GetAddedEdges()) { + return false; + } if (!EncodeMaterialFileName()) { return false; } @@ -110,12 +117,38 @@ bool ObjEncoder::ExitAndCleanup(bool return_value) { normal_att_ = nullptr; material_att_ = nullptr; sub_obj_att_ = nullptr; + added_edges_att_ = nullptr; current_sub_obj_id_ = -1; current_material_id_ = -1; file_name_.clear(); return return_value; } +bool ObjEncoder::GetAddedEdges() { + const GeometryMetadata *mesh_metadata = in_mesh_->GetMetadata(); + if (!mesh_metadata) { + return true; + } + + // Try to get a per-corner attribute describing added edges. + { + const AttributeMetadata *att_metadata = + mesh_metadata->GetAttributeMetadataByStringEntry("name", "added_edges"); + if (att_metadata) { + const auto att = + in_mesh_->GetAttributeByUniqueId(att_metadata->att_unique_id()); + if (att->size() == 0 || att->num_components() != 1 || + att->data_type() != DataType::DT_UINT8) { + return false; + } + added_edges_att_ = att; + return true; + } + } + + return true; +} + bool ObjEncoder::GetSubObjects() { const GeometryMetadata *pc_metadata = in_point_cloud_->GetMetadata(); if (!pc_metadata) { @@ -137,7 +170,8 @@ bool ObjEncoder::GetSubObjects() { } sub_obj_att_ = in_point_cloud_->GetAttributeByUniqueId( sub_obj_metadata->att_unique_id()); - if (sub_obj_att_ == nullptr || sub_obj_att_->size() == 0) { + if (sub_obj_att_ == nullptr || sub_obj_att_->size() == 0 || + sub_obj_att_->num_components() != 1) { return false; } return true; @@ -236,17 +270,11 @@ bool ObjEncoder::EncodeNormals() { } bool ObjEncoder::EncodeFaces() { + if (added_edges_att_ != nullptr) { + return EncodePolygonalFaces(); + } for (FaceIndex i(0); i < in_mesh_->num_faces(); ++i) { - if (sub_obj_att_) { - if (!EncodeSubObject(i)) { - return false; - } - } - if (material_att_) { - if (!EncodeMaterial(i)) { - return false; - } - } + EncodeFaceAttributes(i); buffer()->Encode('f'); for (int j = 0; j < 3; ++j) { if (!EncodeFaceCorner(i, j)) { @@ -258,6 +286,56 @@ bool ObjEncoder::EncodeFaces() { return true; } +bool ObjEncoder::EncodePolygonalFaces() { + // TODO(vytyaz): This could be a much smaller set of visited face indices. + std::vector triangle_visited(in_mesh_->num_faces(), false); + PolygonEdges polygon_edges; + std::unique_ptr corner_table = + CreateCornerTableFromPositionAttribute(in_mesh_); + for (FaceIndex fi(0); fi < in_mesh_->num_faces(); ++fi) { + EncodeFaceAttributes(fi); + // Reconstruct polygon from the added edges attribute if available. + polygon_edges.clear(); + FindOriginalFaceEdges(fi, *corner_table, &triangle_visited, &polygon_edges); + + // Polygon edges could be empty if this triangle has been visited as part + // of a polygon discovery that started from an earler face. + if (polygon_edges.empty()) { + continue; + } + + // Traverse a polygon by following its edges. The starting point is not + // guaranteed to be the same as in the original polygon. It is + // deterministic, however, and defined by std::map behavior. + const AttributeValueIndex first_position_index = + polygon_edges.begin()->first; + AttributeValueIndex position_index = first_position_index; + buffer()->Encode('f'); + do { + // Get the next polygon point index by following polygon edge. + const PointIndex pi = polygon_edges[position_index]; + EncodeFaceCorner(pi); + position_index = pos_att_->mapped_index(pi).value(); + } while (position_index != first_position_index); + buffer()->Encode("\n", 1); + } + return true; +} + +bool ObjEncoder::EncodeFaceAttributes(FaceIndex face_id) { + if (sub_obj_att_) { + if (!EncodeSubObject(face_id)) { + return false; + } + } + if (material_att_) { + if (!EncodeMaterial(face_id)) { + return false; + } + } + return true; +} + bool ObjEncoder::EncodeMaterial(FaceIndex face_id) { int material_id = 0; // Pick the first corner, all corners of a face should have same id. @@ -304,8 +382,12 @@ bool ObjEncoder::EncodeSubObject(FaceIndex face_id) { } bool ObjEncoder::EncodeFaceCorner(FaceIndex face_id, int local_corner_id) { - buffer()->Encode(' '); const PointIndex vert_index = in_mesh_->face(face_id)[local_corner_id]; + return EncodeFaceCorner(vert_index); +} + +bool ObjEncoder::EncodeFaceCorner(PointIndex vert_index) { + buffer()->Encode(' '); // Note that in the OBJ format, all indices are encoded starting from index 1. // Encode position index. EncodeInt(pos_att_->mapped_index(vert_index).value() + 1); @@ -343,4 +425,67 @@ void ObjEncoder::EncodeInt(int32_t val) { buffer()->Encode(num_buffer_, strlen(num_buffer_)); } +bool ObjEncoder::IsNewEdge(const CornerTable &ct, CornerIndex ci) const { + const PointIndex pi = in_mesh_->CornerToPointId(ci); + if (added_edges_att_ != nullptr) { + uint8_t value; + added_edges_att_->GetMappedValue(pi, &value); + return value == 1; + } + return false; +} + +void ObjEncoder::FindOriginalFaceEdges(FaceIndex face_index, + const CornerTable &corner_table, + std::vector *triangle_visited, + PolygonEdges *polygon_edges) { + // Do not add any edges if this triangular face has already been visited. + if ((*triangle_visited)[face_index.value()]) { + return; + } + (*triangle_visited)[face_index.value()] = true; + const Mesh::Face &face = in_mesh_->face(face_index); + for (size_t c = 0; c < 3; c++) { + // Check for added edge using this corner. + const CornerIndex ci = corner_table.FirstCorner(face_index) + c; + const CornerIndex co = corner_table.Opposite(ci); + bool is_new_edge = IsNewEdge(corner_table, ci); + + // Check for the new edge using the opposite corner. + if (!is_new_edge && co != kInvalidCornerIndex) { + is_new_edge = IsNewEdge(corner_table, co); + } + // The new edge may become a boundary edge when a degenerate triangle + // created by polygon triangulation is removed by Draco encoder, hence |co| + // is checked below. This can happen when an isolated (boundary) quad only + // has three distinct vertex positions. + // + // TODO(vytyaz): Fix polygon reconstruction with other possible cases of + // degenerate triangles. There are two known sources of degenerate triangles + // that affect polygon reconstruction: + // + // 1. Degenerate triangles created during polygon triangulation are removed + // by Draco encoder, which invalidates the "added_edges" attribute. + // Solution is to discard those triangles before creating the attribute. + // + // 2. Degenerate triangles created by position quantization are encoded and + // decoded by Draco, but not captured into the |corner_table|, causing a + // mismatch between the corner table and the "added_edges" attribute. + // Solution is to use corner table from draco::MeshDecoder here. + // + if (is_new_edge && co != kInvalidCornerIndex) { + // Visit triangle across the new edge. + const FaceIndex opposite_face_index = corner_table.Face(co); + FindOriginalFaceEdges(opposite_face_index, corner_table, triangle_visited, + polygon_edges); + } else { + // Insert the original edge to the map. + const PointIndex point_from = face[(c + 1) % 3]; + const PointIndex point_to = face[(c + 2) % 3]; + polygon_edges->insert( + {PositionIndex(pos_att_->mapped_index(point_from)), point_to}); + } + } +} + } // namespace draco diff --git a/contrib/draco/src/draco/io/obj_encoder.h b/contrib/draco/src/draco/io/obj_encoder.h index 509d39baf..1d67b5306 100644 --- a/contrib/draco/src/draco/io/obj_encoder.h +++ b/contrib/draco/src/draco/io/obj_encoder.h @@ -18,6 +18,7 @@ #include #include "draco/core/encoder_buffer.h" +#include "draco/mesh/corner_table.h" #include "draco/mesh/mesh.h" namespace draco { @@ -44,19 +45,30 @@ class ObjEncoder { bool ExitAndCleanup(bool return_value); private: + typedef AttributeValueIndex PositionIndex; + typedef std::map PolygonEdges; + bool GetAddedEdges(); bool GetSubObjects(); bool EncodeMaterialFileName(); bool EncodePositions(); bool EncodeTextureCoordinates(); bool EncodeNormals(); bool EncodeFaces(); + bool EncodePolygonalFaces(); + bool EncodeFaceAttributes(FaceIndex face_id); bool EncodeSubObject(FaceIndex face_id); bool EncodeMaterial(FaceIndex face_id); bool EncodeFaceCorner(FaceIndex face_id, int local_corner_id); + bool EncodeFaceCorner(PointIndex vert_index); void EncodeFloat(float val); void EncodeFloatList(float *vals, int num_vals); void EncodeInt(int32_t val); + bool IsNewEdge(const CornerTable &ct, CornerIndex ci) const; + void FindOriginalFaceEdges(FaceIndex face_index, + const CornerTable &corner_table, + std::vector *triangle_visited, + PolygonEdges *polygon_edges); // Various attributes used by the encoder. If an attribute is not used, it is // set to nullptr. @@ -66,6 +78,9 @@ class ObjEncoder { const PointAttribute *material_att_; const PointAttribute *sub_obj_att_; + // Stores per-corner triangulation information for polygon reconstruction. + const PointAttribute *added_edges_att_; + // Buffer used for encoding float/int numbers. char num_buffer_[20]; diff --git a/contrib/draco/src/draco/io/obj_encoder_test.cc b/contrib/draco/src/draco/io/obj_encoder_test.cc index 4838e56ca..782983fad 100644 --- a/contrib/draco/src/draco/io/obj_encoder_test.cc +++ b/contrib/draco/src/draco/io/obj_encoder_test.cc @@ -16,10 +16,12 @@ #include +#include "draco/attributes/geometry_attribute.h" #include "draco/core/draco_test_base.h" #include "draco/core/draco_test_utils.h" #include "draco/io/file_reader_factory.h" #include "draco/io/file_reader_interface.h" +#include "draco/io/file_utils.h" #include "draco/io/obj_decoder.h" namespace draco { @@ -27,6 +29,8 @@ namespace draco { class ObjEncoderTest : public ::testing::Test { protected: void CompareMeshes(const Mesh *mesh0, const Mesh *mesh1) { + ASSERT_NE(mesh0, nullptr); + ASSERT_NE(mesh1, nullptr); ASSERT_EQ(mesh0->num_faces(), mesh1->num_faces()); ASSERT_EQ(mesh0->num_attributes(), mesh1->num_attributes()); for (size_t att_id = 0; att_id < mesh0->num_attributes(); ++att_id) { @@ -107,4 +111,34 @@ TEST_F(ObjEncoderTest, TestObjEncodingAll) { test_encoding("two_faces_312.obj"); } +TEST_F(ObjEncoderTest, TestObjOctagonPreserved) { + // Test verifies that OBJ encoder can reconstruct and encode an octagon. + // Decode triangulated octagon and an extra attribute for reconstruction. + std::unique_ptr mesh = + ReadMeshFromTestFile("octagon_preserved.drc"); + ASSERT_NE(mesh, nullptr); + ASSERT_EQ(mesh->num_faces(), 6); + ASSERT_EQ(mesh->NumNamedAttributes(GeometryAttribute::GENERIC), 1); + ASSERT_NE(mesh->GetMetadata()->GetAttributeMetadataByStringEntry( + "name", "added_edges"), + nullptr); + + // Reconstruct octagon and encode it into an OBJ file. + draco::ObjEncoder obj_encoder; + ASSERT_TRUE(obj_encoder.EncodeToFile( + *mesh, draco::GetTestTempFileFullPath("encoded.obj"))); + + // Read encoded OBJ file and golden OBJ file contents into buffers. + std::vector data_encoded; + std::vector data_golden; + ASSERT_TRUE( + ReadFileToBuffer(GetTestTempFileFullPath("encoded.obj"), &data_encoded)); + ASSERT_TRUE(ReadFileToBuffer(GetTestFileFullPath("octagon_preserved.obj"), + &data_golden)); + + // Check that encoded OBJ file contents are correct. + ASSERT_EQ(data_encoded.size(), data_golden.size()); + ASSERT_EQ(data_encoded, data_golden); +} + } // namespace draco diff --git a/contrib/draco/src/draco/io/parser_utils.cc b/contrib/draco/src/draco/io/parser_utils.cc index 12afacff6..378de7378 100644 --- a/contrib/draco/src/draco/io/parser_utils.cc +++ b/contrib/draco/src/draco/io/parser_utils.cc @@ -203,31 +203,40 @@ void ParseLine(DecoderBuffer *buffer, std::string *out_string) { out_string->clear(); } char c; - bool delim_reached = false; + int num_delims = 0; + char last_delim; while (buffer->Peek(&c)) { - // Check if |c| is a delimeter. We want to parse all delimeters until we - // reach a non-delimeter symbol. (E.g. we want to ignore '\r\n' at the end - // of the line). + // Check if |c| is a delimiter symbol. We want to identify all possible + // delimiters that can occur on different platforms (i.e. we want to detect + // '\r\n', '\r', '\n'). const bool is_delim = (c == '\r' || c == '\n'); - // If |c| is a delimeter or it is a non-delimeter symbol before any - // delimeter was found, we advance the buffer to the next character. - if (is_delim || !delim_reached) { - buffer->Advance(1); + if (is_delim) { + if (num_delims == 0) { + last_delim = c; + } else if (num_delims == 1) { + // We already parsed either '\r' or '\n'. Ensure the new delim symbol is + // '\n' and different from the previous symbol. + if (c == last_delim || c != '\n') { + return; // Same delimiter symbol already processed. + } + } else { + // Too many delimiter symbols. + return; + } + num_delims++; } - if (is_delim) { - // Mark that we found a delimeter symbol. - delim_reached = true; - continue; - } - if (delim_reached) { - // We reached a non-delimeter symbol after a delimeter was already found. + if (!is_delim && num_delims > 0) { + // We reached a non-delimiter symbol after a delimiter was already found. // Stop the parsing. return; } - // Otherwise we put the non-delimeter symbol into the output string. - if (out_string) { + + buffer->Advance(1); + + // We put the non-delimiter symbol into the output string. + if (!is_delim && out_string) { out_string->push_back(c); } } diff --git a/contrib/draco/src/draco/io/ply_decoder_test.cc b/contrib/draco/src/draco/io/ply_decoder_test.cc index 97977c8cc..1dd70d5cb 100644 --- a/contrib/draco/src/draco/io/ply_decoder_test.cc +++ b/contrib/draco/src/draco/io/ply_decoder_test.cc @@ -88,6 +88,7 @@ TEST_F(PlyDecoderTest, TestPlyDecodingAll) { // test_decoding("test_pos_color.ply"); // tested test_decoding("cube_quads.ply"); test_decoding("Box.ply"); + test_decoding("delim_test.ply"); } } // namespace draco diff --git a/contrib/draco/src/draco/io/ply_encoder.cc b/contrib/draco/src/draco/io/ply_encoder.cc index 2f6a1a2a8..0fe611f1c 100644 --- a/contrib/draco/src/draco/io/ply_encoder.cc +++ b/contrib/draco/src/draco/io/ply_encoder.cc @@ -143,7 +143,8 @@ bool PlyEncoder::EncodeInternal() { buffer()->Encode(header_str.data(), header_str.length()); // Store point attributes. - for (PointIndex v(0); v < in_point_cloud_->num_points(); ++v) { + const int num_points = in_point_cloud_->num_points(); + for (PointIndex v(0); v < num_points; ++v) { const auto *const pos_att = in_point_cloud_->attribute(pos_att_id); buffer()->Encode(pos_att->GetAddress(pos_att->mapped_index(v)), pos_att->byte_stride()); @@ -166,9 +167,13 @@ bool PlyEncoder::EncodeInternal() { buffer()->Encode(static_cast(3)); const auto &f = in_mesh_->face(i); - buffer()->Encode(f[0]); - buffer()->Encode(f[1]); - buffer()->Encode(f[2]); + for (int c = 0; c < 3; ++c) { + if (f[c] >= num_points) { + // Invalid point stored on the |in_mesh_| face. + return false; + } + buffer()->Encode(f[c]); + } if (tex_coord_att_id >= 0) { // Two coordinates for every corner -> 6. diff --git a/contrib/draco/src/draco/io/ply_reader_test.cc b/contrib/draco/src/draco/io/ply_reader_test.cc index 05ff63dd4..9612f6377 100644 --- a/contrib/draco/src/draco/io/ply_reader_test.cc +++ b/contrib/draco/src/draco/io/ply_reader_test.cc @@ -39,7 +39,7 @@ TEST_F(PlyReaderTest, TestReader) { buf.Init(data.data(), data.size()); PlyReader reader; Status status = reader.Read(&buf); - ASSERT_TRUE(status.ok()) << status; + DRACO_ASSERT_OK(status); ASSERT_EQ(reader.num_elements(), 2); ASSERT_EQ(reader.element(0).num_properties(), 7); ASSERT_EQ(reader.element(1).num_properties(), 1); @@ -64,14 +64,14 @@ TEST_F(PlyReaderTest, TestReaderAscii) { buf.Init(data.data(), data.size()); PlyReader reader; Status status = reader.Read(&buf); - ASSERT_TRUE(status.ok()) << status; + DRACO_ASSERT_OK(status); const std::string file_name_ascii = "test_pos_color_ascii.ply"; const std::vector data_ascii = ReadPlyFile(file_name_ascii); buf.Init(data_ascii.data(), data_ascii.size()); PlyReader reader_ascii; status = reader_ascii.Read(&buf); - ASSERT_TRUE(status.ok()) << status; + DRACO_ASSERT_OK(status); ASSERT_EQ(reader.num_elements(), reader_ascii.num_elements()); ASSERT_EQ(reader.element(0).num_properties(), reader_ascii.element(0).num_properties()); @@ -96,7 +96,7 @@ TEST_F(PlyReaderTest, TestReaderExtraWhitespace) { buf.Init(data.data(), data.size()); PlyReader reader; Status status = reader.Read(&buf); - ASSERT_TRUE(status.ok()) << status; + DRACO_ASSERT_OK(status); ASSERT_EQ(reader.num_elements(), 2); ASSERT_EQ(reader.element(0).num_properties(), 7); @@ -122,7 +122,7 @@ TEST_F(PlyReaderTest, TestReaderMoreDataTypes) { buf.Init(data.data(), data.size()); PlyReader reader; Status status = reader.Read(&buf); - ASSERT_TRUE(status.ok()) << status; + DRACO_ASSERT_OK(status); ASSERT_EQ(reader.num_elements(), 2); ASSERT_EQ(reader.element(0).num_properties(), 7); diff --git a/contrib/draco/src/draco/io/scene_io.cc b/contrib/draco/src/draco/io/scene_io.cc new file mode 100644 index 000000000..e41d2e1fa --- /dev/null +++ b/contrib/draco/src/draco/io/scene_io.cc @@ -0,0 +1,127 @@ +// Copyright 2019 The Draco Authors. +// +// 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 "draco/io/scene_io.h" + +#include + +#ifdef DRACO_TRANSCODER_SUPPORTED +#include "draco/io/file_utils.h" +#include "draco/io/gltf_decoder.h" +#include "draco/io/gltf_encoder.h" +#include "draco/io/obj_encoder.h" +#include "draco/io/ply_encoder.h" + +namespace draco { + +enum SceneFileFormat { UNKNOWN, GLTF, USD, PLY, OBJ }; + +SceneFileFormat GetSceneFileFormat(const std::string &file_name) { + const std::string extension = LowercaseFileExtension(file_name); + if (extension == "gltf" || extension == "glb") { + return GLTF; + } + if (extension == "usd" || extension == "usda" || extension == "usdc" || + extension == "usdz") { + return USD; + } + if (extension == "obj") { + return OBJ; + } + if (extension == "ply") { + return PLY; + } + return UNKNOWN; +} + +StatusOr> ReadSceneFromFile( + const std::string &file_name) { + return ReadSceneFromFile(file_name, nullptr); +} + +StatusOr> ReadSceneFromFile( + const std::string &file_name, std::vector *scene_files) { + std::unique_ptr scene(new Scene()); + switch (GetSceneFileFormat(file_name)) { + case GLTF: { + GltfDecoder decoder; + return decoder.DecodeFromFileToScene(file_name, scene_files); + } + case USD: { + return Status(Status::DRACO_ERROR, "USD is not supported yet."); + } + default: { + return Status(Status::DRACO_ERROR, "Unknown input file format."); + } + } +} + +Status WriteSceneToFile(const std::string &file_name, const Scene &scene) { + Options options; + return WriteSceneToFile(file_name, scene, options); +} + +Status WriteSceneToFile(const std::string &file_name, const Scene &scene, + const Options &options) { + const std::string extension = LowercaseFileExtension(file_name); + std::string folder_path; + std::string out_file_name; + draco::SplitPath(file_name, &folder_path, &out_file_name); + const auto format = GetSceneFileFormat(file_name); + switch (format) { + case GLTF: { + GltfEncoder encoder; + if (!encoder.EncodeToFile(scene, file_name, folder_path)) { + return Status(Status::DRACO_ERROR, "Failed to encode the scene."); + } + return OkStatus(); + } + case USD: { + return Status(Status::DRACO_ERROR, "USD is not supported yet."); + } + case PLY: + case OBJ: { + // Convert the scene to mesh and save the scene as a mesh. For now we do + // that by converting the scene to GLB and decoding the GLB into a mesh. + GltfEncoder gltf_encoder; + EncoderBuffer buffer; + DRACO_RETURN_IF_ERROR(gltf_encoder.EncodeToBuffer(scene, &buffer)); + GltfDecoder gltf_decoder; + DecoderBuffer dec_buffer; + dec_buffer.Init(buffer.data(), buffer.size()); + DRACO_ASSIGN_OR_RETURN(auto mesh, + gltf_decoder.DecodeFromBuffer(&dec_buffer)); + if (format == PLY) { + PlyEncoder ply_encoder; + if (!ply_encoder.EncodeToFile(*mesh, file_name)) { + return ErrorStatus("Failed to encode the scene as PLY."); + } + } + if (format == OBJ) { + ObjEncoder obj_encoder; + if (!obj_encoder.EncodeToFile(*mesh, file_name)) { + return ErrorStatus("Failed to encode the scene as OBJ."); + } + } + return OkStatus(); + } + default: { + return Status(Status::DRACO_ERROR, "Unknown output file format."); + } + } +} + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED diff --git a/contrib/draco/src/draco/io/scene_io.h b/contrib/draco/src/draco/io/scene_io.h new file mode 100644 index 000000000..964faac3c --- /dev/null +++ b/contrib/draco/src/draco/io/scene_io.h @@ -0,0 +1,55 @@ +// Copyright 2019 The Draco Authors. +// +// 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 DRACO_IO_SCENE_IO_H_ +#define DRACO_IO_SCENE_IO_H_ + +#include "draco/draco_features.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED +#include +#include + +#include "draco/core/options.h" +#include "draco/core/status_or.h" +#include "draco/scene/scene.h" + +namespace draco { + +// Reads a scene from a file. Currently only GLTF 2.0 scene files are supported. +// The second form returns the files associated with the scene via the +// |scene_files| argument. +StatusOr> ReadSceneFromFile( + const std::string &file_name); +StatusOr> ReadSceneFromFile( + const std::string &file_name, std::vector *scene_files); + +// Writes a scene into a file. +Status WriteSceneToFile(const std::string &file_name, const Scene &scene); + +// Writes a scene into a file, configurable with |options|. +// +// Supported options: +// +// force_usd_vertex_interpolation= - forces implicit vertex +// interpolation while exporting to USD +// (default = false) +// +Status WriteSceneToFile(const std::string &file_name, const Scene &scene, + const Options &options); + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED +#endif // DRACO_IO_SCENE_IO_H_ diff --git a/contrib/draco/src/draco/io/scene_io_test.cc b/contrib/draco/src/draco/io/scene_io_test.cc new file mode 100644 index 000000000..828065693 --- /dev/null +++ b/contrib/draco/src/draco/io/scene_io_test.cc @@ -0,0 +1,86 @@ +// Copyright 2019 The Draco Authors. +// +// 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 "draco/io/scene_io.h" + +#include +#include + +#ifdef DRACO_TRANSCODER_SUPPORTED +#include "draco/core/draco_test_utils.h" +#include "draco/io/file_utils.h" +#include "draco/io/mesh_io.h" + +namespace { + +TEST(SceneTest, TestSceneIO) { + // A simple test that verifies that the scene is loaded and saved using the + // scene_io.h API. + const std::string file_name = + draco::GetTestFileFullPath("CesiumMilkTruck/glTF/CesiumMilkTruck.gltf"); + draco::StatusOr> maybe_scene = + draco::ReadSceneFromFile(file_name); + ASSERT_TRUE(maybe_scene.status().ok()); + std::unique_ptr scene = std::move(maybe_scene).value(); + ASSERT_NE(scene, nullptr); + + const std::string out_file_name = + draco::GetTestTempFileFullPath("out_scene.gltf"); + ASSERT_TRUE(draco::WriteSceneToFile(out_file_name, *scene).ok()); + + // Ensure all files related to the scene are saved. + ASSERT_GT(draco::GetFileSize(out_file_name), 0); + ASSERT_GT( + draco::GetFileSize(draco::GetTestTempFileFullPath("CesiumMilkTruck.png")), + 0); + ASSERT_GT(draco::GetFileSize(draco::GetTestTempFileFullPath("buffer0.bin")), + 0); +} + +TEST(SceneTest, TestSaveToPly) { + // A simple test that verifies that a loaded scene can be stored in a PLY file + // format. + const std::string file_name = + draco::GetTestFileFullPath("CesiumMilkTruck/glTF/CesiumMilkTruck.gltf"); + DRACO_ASSIGN_OR_ASSERT(std::unique_ptr scene, + draco::ReadSceneFromFile(file_name)); + + const std::string out_file_name = + draco::GetTestTempFileFullPath("out_scene.ply"); + DRACO_ASSERT_OK(draco::WriteSceneToFile(out_file_name, *scene)); + + // Verify that we can read the saved mesh. + DRACO_ASSIGN_OR_ASSERT(auto mesh, draco::ReadMeshFromFile(out_file_name)); + ASSERT_NE(mesh, nullptr); +} + +TEST(SceneTest, TestSaveToObj) { + // A simple test that verifies that a loaded scene can be stored in an OBJ + // file format. + const std::string file_name = + draco::GetTestFileFullPath("CesiumMilkTruck/glTF/CesiumMilkTruck.gltf"); + DRACO_ASSIGN_OR_ASSERT(std::unique_ptr scene, + draco::ReadSceneFromFile(file_name)); + + const std::string out_file_name = + draco::GetTestTempFileFullPath("out_scene.obj"); + DRACO_ASSERT_OK(draco::WriteSceneToFile(out_file_name, *scene)); + + // Verify that we can read the saved mesh. + DRACO_ASSIGN_OR_ASSERT(auto mesh, draco::ReadMeshFromFile(out_file_name)); + ASSERT_NE(mesh, nullptr); +} + +} // namespace +#endif // DRACO_TRANSCODER_SUPPORTED diff --git a/contrib/draco/src/draco/io/stdio_file_reader_test.cc b/contrib/draco/src/draco/io/stdio_file_reader_test.cc index 487819a02..212945f90 100644 --- a/contrib/draco/src/draco/io/stdio_file_reader_test.cc +++ b/contrib/draco/src/draco/io/stdio_file_reader_test.cc @@ -9,7 +9,7 @@ namespace { TEST(StdioFileReaderTest, FailOpen) { EXPECT_EQ(StdioFileReader::Open(""), nullptr); - EXPECT_EQ(StdioFileReader::Open("fake file"), nullptr); + EXPECT_EQ(StdioFileReader::Open("stdio reader fake file"), nullptr); } TEST(StdioFileReaderTest, Open) { diff --git a/contrib/draco/src/draco/io/stl_decoder.cc b/contrib/draco/src/draco/io/stl_decoder.cc new file mode 100644 index 000000000..1e5d3a938 --- /dev/null +++ b/contrib/draco/src/draco/io/stl_decoder.cc @@ -0,0 +1,77 @@ +// Copyright 2022 The Draco Authors. +// +// 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 "draco/io/stl_decoder.h" + +#include + +#include "draco/core/macros.h" +#include "draco/core/status.h" +#include "draco/core/status_or.h" +#include "draco/io/file_utils.h" +#include "draco/mesh/triangle_soup_mesh_builder.h" + +namespace draco { + +StatusOr> StlDecoder::DecodeFromFile( + const std::string &file_name) { + std::vector data; + if (!ReadFileToBuffer(file_name, &data)) { + return Status(Status::IO_ERROR, "Unable to read input file."); + } + DecoderBuffer buffer; + buffer.Init(data.data(), data.size()); + return DecodeFromBuffer(&buffer); +} + +StatusOr> StlDecoder::DecodeFromBuffer( + DecoderBuffer *buffer) { + if (!strncmp(buffer->data_head(), "solid ", 6)) { + return Status(Status::IO_ERROR, + "Currently only binary STL files are supported."); + } + buffer->Advance(80); + uint32_t face_count; + buffer->Decode(&face_count, 4); + + TriangleSoupMeshBuilder builder; + builder.Start(face_count); + + const int32_t pos_att_id = + builder.AddAttribute(GeometryAttribute::POSITION, 3, DT_FLOAT32); + const int32_t norm_att_id = + builder.AddAttribute(GeometryAttribute::NORMAL, 3, DT_FLOAT32); + + for (uint32_t i = 0; i < face_count; i++) { + float data[48]; + buffer->Decode(data, 48); + uint16_t unused; + buffer->Decode(&unused, 2); + + builder.SetPerFaceAttributeValueForFace( + norm_att_id, draco::FaceIndex(i), + draco::Vector3f(data[0], data[1], data[2]).data()); + + builder.SetAttributeValuesForFace( + pos_att_id, draco::FaceIndex(i), + draco::Vector3f(data[3], data[4], data[5]).data(), + draco::Vector3f(data[6], data[7], data[8]).data(), + draco::Vector3f(data[9], data[10], data[11]).data()); + } + + std::unique_ptr mesh = builder.Finalize(); + return mesh; +} + +} // namespace draco diff --git a/contrib/draco/src/draco/io/stl_decoder.h b/contrib/draco/src/draco/io/stl_decoder.h new file mode 100644 index 000000000..44b35d849 --- /dev/null +++ b/contrib/draco/src/draco/io/stl_decoder.h @@ -0,0 +1,38 @@ +// Copyright 2022 The Draco Authors. +// +// 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 DRACO_IO_STL_DECODER_H_ +#define DRACO_IO_STL_DECODER_H_ + +#include + +#include "draco/core/decoder_buffer.h" +#include "draco/core/status.h" +#include "draco/core/status_or.h" +#include "draco/draco_features.h" +#include "draco/mesh/mesh.h" + +namespace draco { + +// Decodes an STL file into draco::Mesh (or draco::PointCloud if the +// connectivity data is not needed). +class StlDecoder { + public: + StatusOr> DecodeFromFile(const std::string &file_name); + StatusOr> DecodeFromBuffer(DecoderBuffer *buffer); +}; + +} // namespace draco + +#endif // DRACO_IO_STL_DECODER_H_ diff --git a/contrib/draco/src/draco/io/stl_decoder_test.cc b/contrib/draco/src/draco/io/stl_decoder_test.cc new file mode 100644 index 000000000..886881925 --- /dev/null +++ b/contrib/draco/src/draco/io/stl_decoder_test.cc @@ -0,0 +1,49 @@ +// Copyright 2022 The Draco Authors. +// +// 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 "draco/io/stl_decoder.h" + +#include + +#include "draco/core/draco_test_base.h" +#include "draco/core/draco_test_utils.h" + +namespace draco { + +class StlDecoderTest : public ::testing::Test { + protected: + void test_decoding(const std::string &file_name) { + const std::string path = GetTestFileFullPath(file_name); + StlDecoder decoder; + DRACO_ASSIGN_OR_ASSERT(std::unique_ptr mesh, + decoder.DecodeFromFile(path)); + ASSERT_GT(mesh->num_faces(), 0); + ASSERT_GT(mesh->num_points(), 0); + } + + void test_decoding_should_fail(const std::string &file_name) { + StlDecoder decoder; + StatusOr> statusOrMesh = + decoder.DecodeFromFile(GetTestFileFullPath(file_name)); + ASSERT_FALSE(statusOrMesh.ok()); + } +}; + +TEST_F(StlDecoderTest, TestStlDecoding) { + test_decoding("STL/bunny.stl"); + test_decoding("STL/test_sphere.stl"); + test_decoding_should_fail("STL/test_sphere_ascii.stl"); +} + +} // namespace draco diff --git a/contrib/draco/src/draco/io/stl_encoder.cc b/contrib/draco/src/draco/io/stl_encoder.cc new file mode 100644 index 000000000..5aa4a0a97 --- /dev/null +++ b/contrib/draco/src/draco/io/stl_encoder.cc @@ -0,0 +1,111 @@ +// Copyright 2022 The Draco Authors. +// +// 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 "draco/io/stl_encoder.h" + +#include +#include +#include +#include + +#include "draco/io/file_writer_factory.h" +#include "draco/io/file_writer_interface.h" + +namespace draco { + +StlEncoder::StlEncoder() + : out_buffer_(nullptr), in_point_cloud_(nullptr), in_mesh_(nullptr) {} + +Status StlEncoder::EncodeToFile(const Mesh &mesh, + const std::string &file_name) { + in_mesh_ = &mesh; + std::unique_ptr file = + FileWriterFactory::OpenWriter(file_name); + if (!file) { + return Status(Status::IO_ERROR, "File couldn't be opened"); + } + // Encode the mesh into a buffer. + EncoderBuffer buffer; + DRACO_RETURN_IF_ERROR(EncodeToBuffer(mesh, &buffer)); + // Write the buffer into the file. + file->Write(buffer.data(), buffer.size()); + return OkStatus(); +} + +Status StlEncoder::EncodeToBuffer(const Mesh &mesh, EncoderBuffer *out_buffer) { + in_mesh_ = &mesh; + out_buffer_ = out_buffer; + Status s = EncodeInternal(); + in_mesh_ = nullptr; // cleanup + in_point_cloud_ = nullptr; + out_buffer_ = nullptr; + return s; +} + +Status StlEncoder::EncodeInternal() { + // Write STL header. + std::stringstream out; + out << std::left << std::setw(80) + << "generated using Draco"; // header is 80 bytes fixed size. + const std::string header_str = out.str(); + buffer()->Encode(header_str.data(), header_str.length()); + + uint32_t num_faces = in_mesh_->num_faces(); + buffer()->Encode(&num_faces, 4); + + std::vector stl_face; + + const int pos_att_id = + in_mesh_->GetNamedAttributeId(GeometryAttribute::POSITION); + + if (pos_att_id < 0) { + return ErrorStatus("Mesh is missing the position attribute."); + } + + if (in_mesh_->attribute(pos_att_id)->data_type() != DT_FLOAT32) { + return ErrorStatus("Mesh position attribute is not of type float32."); + } + + uint16_t unused = 0; + + if (in_mesh_) { + for (FaceIndex i(0); i < in_mesh_->num_faces(); ++i) { + const auto &f = in_mesh_->face(i); + const auto *const pos_att = in_mesh_->attribute(pos_att_id); + + // The normal attribute can contain arbitrary normals that may not + // correspond to the winding of the face. + // Therefor we simply always calculate them + // using the points of the triangle face: norm(cross(p2-p1, p3-p1)) + + Vector3f pos[3]; + pos_att->GetMappedValue(f[0], &pos[0][0]); + pos_att->GetMappedValue(f[1], &pos[1][0]); + pos_att->GetMappedValue(f[2], &pos[2][0]); + Vector3f norm = CrossProduct(pos[1] - pos[0], pos[2] - pos[0]); + norm.Normalize(); + buffer()->Encode(norm.data(), sizeof(float) * 3); + + for (int c = 0; c < 3; ++c) { + buffer()->Encode(pos_att->GetAddress(pos_att->mapped_index(f[c])), + pos_att->byte_stride()); + } + + buffer()->Encode(&unused, 2); + } + } + return OkStatus(); +} + +} // namespace draco diff --git a/contrib/draco/src/draco/io/stl_encoder.h b/contrib/draco/src/draco/io/stl_encoder.h new file mode 100644 index 000000000..8b185b738 --- /dev/null +++ b/contrib/draco/src/draco/io/stl_encoder.h @@ -0,0 +1,52 @@ +// Copyright 2022 The Draco Authors. +// +// 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 DRACO_IO_STL_ENCODER_H_ +#define DRACO_IO_STL_ENCODER_H_ + +#include + +#include "draco/core/encoder_buffer.h" +#include "draco/draco_features.h" +#include "draco/mesh/mesh.h" + +namespace draco { + +// Class for encoding draco::Mesh into the STL file format. +class StlEncoder { + public: + StlEncoder(); + + // Encodes the mesh and saves it into a file. + // Returns false when either the encoding failed or when the file couldn't be + // opened. + Status EncodeToFile(const Mesh &mesh, const std::string &file_name); + + // Encodes the mesh into a buffer. + Status EncodeToBuffer(const Mesh &mesh, EncoderBuffer *out_buffer); + + protected: + Status EncodeInternal(); + EncoderBuffer *buffer() const { return out_buffer_; } + + private: + EncoderBuffer *out_buffer_; + + const PointCloud *in_point_cloud_; + const Mesh *in_mesh_; +}; + +} // namespace draco + +#endif // DRACO_IO_STL_ENCODER_H_ diff --git a/contrib/draco/src/draco/io/stl_encoder_test.cc b/contrib/draco/src/draco/io/stl_encoder_test.cc new file mode 100644 index 000000000..da6298d64 --- /dev/null +++ b/contrib/draco/src/draco/io/stl_encoder_test.cc @@ -0,0 +1,78 @@ +// Copyright 2022 The Draco Authors. +// +// 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 "draco/io/stl_encoder.h" + +#include +#include +#include + +#include "draco/core/draco_test_base.h" +#include "draco/core/draco_test_utils.h" +#include "draco/io/file_reader_factory.h" +#include "draco/io/file_reader_interface.h" +#include "draco/io/stl_decoder.h" + +namespace draco { + +class StlEncoderTest : public ::testing::Test { + protected: + void CompareMeshes(const Mesh *mesh0, const Mesh *mesh1) { + ASSERT_EQ(mesh0->num_faces(), mesh1->num_faces()); + ASSERT_EQ(mesh0->num_attributes(), mesh1->num_attributes()); + for (size_t att_id = 0; att_id < mesh0->num_attributes(); ++att_id) { + ASSERT_EQ(mesh0->attribute(att_id)->size(), + mesh1->attribute(att_id)->size()); + } + } + + // Encode a mesh using the StlEncoder and then decode to verify the encoding. + std::unique_ptr EncodeAndDecodeMesh(const Mesh *mesh) { + EncoderBuffer encoder_buffer; + StlEncoder encoder; + Status status = encoder.EncodeToBuffer(*mesh, &encoder_buffer); + if (!status.ok()) { + return nullptr; + } + + DecoderBuffer decoder_buffer; + decoder_buffer.Init(encoder_buffer.data(), encoder_buffer.size()); + StlDecoder decoder; + StatusOr> status_or_mesh = + decoder.DecodeFromBuffer(&decoder_buffer); + if (!status_or_mesh.ok()) { + return nullptr; + } + std::unique_ptr decoded_mesh = std::move(status_or_mesh).value(); + return decoded_mesh; + } + + void test_encoding(const std::string &file_name) { + const std::unique_ptr mesh(ReadMeshFromTestFile(file_name, true)); + + ASSERT_NE(mesh, nullptr) << "Failed to load test model " << file_name; + ASSERT_GT(mesh->num_faces(), 0); + + const std::unique_ptr decoded_mesh = EncodeAndDecodeMesh(mesh.get()); + CompareMeshes(mesh.get(), decoded_mesh.get()); + } +}; + +TEST_F(StlEncoderTest, TestStlEncoding) { + // Test decoded mesh from encoded stl file stays the same. + test_encoding("STL/bunny.stl"); + test_encoding("STL/test_sphere.stl"); +} + +} // namespace draco diff --git a/contrib/draco/src/draco/io/texture_io.cc b/contrib/draco/src/draco/io/texture_io.cc new file mode 100644 index 000000000..cbe2915b0 --- /dev/null +++ b/contrib/draco/src/draco/io/texture_io.cc @@ -0,0 +1,94 @@ +// Copyright 2018 The Draco Authors. +// +// 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 "draco/io/texture_io.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED + +#include "draco/io/file_utils.h" + +namespace draco { + +namespace { + +StatusOr> CreateDracoTextureInternal( + const std::vector &image_data, SourceImage *out_source_image) { + std::unique_ptr draco_texture(new Texture()); + out_source_image->MutableEncodedData() = image_data; + return std::move(draco_texture); +} + +} // namespace + +StatusOr> ReadTextureFromFile( + const std::string &file_name) { + std::vector image_data; + if (!ReadFileToBuffer(file_name, &image_data)) { + return Status(Status::IO_ERROR, "Unable to read input texture file."); + } + + SourceImage source_image; + DRACO_ASSIGN_OR_RETURN(auto texture, + CreateDracoTextureInternal(image_data, &source_image)); + source_image.set_filename(file_name); + const std::string extension = LowercaseFileExtension(file_name); + const std::string mime_type = + "image/" + (extension == "jpg" ? "jpeg" : extension); + source_image.set_mime_type(mime_type); + texture->set_source_image(source_image); + return texture; +} + +StatusOr> ReadTextureFromBuffer( + const uint8_t *buffer, size_t buffer_size, const std::string &mime_type) { + SourceImage source_image; + std::vector image_data(buffer, buffer + buffer_size); + DRACO_ASSIGN_OR_RETURN(auto texture, + CreateDracoTextureInternal(image_data, &source_image)); + source_image.set_mime_type(mime_type); + texture->set_source_image(source_image); + return texture; +} + +Status WriteTextureToFile(const std::string &file_name, + const Texture &texture) { + std::vector buffer; + DRACO_RETURN_IF_ERROR(WriteTextureToBuffer(texture, &buffer)); + + if (!WriteBufferToFile(buffer.data(), buffer.size(), file_name)) { + return Status(Status::DRACO_ERROR, "Failed to write image."); + } + + return OkStatus(); +} + +Status WriteTextureToBuffer(const Texture &texture, + std::vector *buffer) { + // Copy data from the encoded source image if possible, otherwise load the + // data from the source file. + if (!texture.source_image().encoded_data().empty()) { + *buffer = texture.source_image().encoded_data(); + } else if (!texture.source_image().filename().empty()) { + if (!ReadFileToBuffer(texture.source_image().filename(), buffer)) { + return Status(Status::IO_ERROR, "Unable to read input texture file."); + } + } else { + return Status(Status::DRACO_ERROR, "Invalid source data for the texture."); + } + return OkStatus(); +} + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED diff --git a/contrib/draco/src/draco/io/texture_io.h b/contrib/draco/src/draco/io/texture_io.h new file mode 100644 index 000000000..4dbea7554 --- /dev/null +++ b/contrib/draco/src/draco/io/texture_io.h @@ -0,0 +1,56 @@ +// Copyright 2018 The Draco Authors. +// +// 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 DRACO_IO_TEXTURE_IO_H_ +#define DRACO_IO_TEXTURE_IO_H_ + +#include "draco/draco_features.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED +#include + +#include "draco/core/draco_types.h" +#include "draco/core/status_or.h" +#include "draco/texture/texture.h" + +namespace draco { + +// Reads a texture from a file. Reads PNG, JPEG and WEBP texture files. +// Returns nullptr with an error status if the decoding failed. +StatusOr> ReadTextureFromFile( + const std::string &file_name); + +// Same as ReadTextureFromFile() but the texture data is parsed from a |buffer|. +// |mime_type| should be set to a type of the texture encoded in |buffer|. +// Supported mime types are "image/jpeg", "image/png" and "image/webp". +// TODO(ostava): We should be able to get the mime type directly from the +// |buffer| but our image decoding library doesn't support this at this time. +StatusOr> ReadTextureFromBuffer( + const uint8_t *buffer, size_t buffer_size, const std::string &mime_type); + +// Writes a texture into a file. Can write PNG, JPEG, WEBP, and KTX2 (with Basis +// compression) texture files depending on the extension specified in +// |file_name| and image format specified in |texture|. Note that images with +// Basis compression can only be saved to files in KTX2 format and not to files +// with "basis" extension. Returns an error status if the writing failed. +Status WriteTextureToFile(const std::string &file_name, const Texture &texture); + +// Writes a |texture| into |buffer|. +Status WriteTextureToBuffer(const Texture &texture, + std::vector *buffer); + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED +#endif // DRACO_IO_TEXTURE_IO_H_ diff --git a/contrib/draco/src/draco/io/texture_io_test.cc b/contrib/draco/src/draco/io/texture_io_test.cc new file mode 100644 index 000000000..13f36e44a --- /dev/null +++ b/contrib/draco/src/draco/io/texture_io_test.cc @@ -0,0 +1,55 @@ +// Copyright 2018 The Draco Authors. +// +// 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 "draco/io/texture_io.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED +#include +#include +#include +#include +#include + +#include "draco/core/draco_test_utils.h" +#include "draco/io/file_utils.h" + +namespace { + +// Tests loading of textures from a buffer. +TEST(TextureIoTest, TestLoadFromBuffer) { + const std::string file_name = draco::GetTestFileFullPath("test.png"); + std::vector image_data; + ASSERT_TRUE(draco::ReadFileToBuffer(file_name, &image_data)); + + DRACO_ASSIGN_OR_ASSERT( + std::unique_ptr texture, + draco::ReadTextureFromBuffer(image_data.data(), image_data.size(), + "image/png")); + ASSERT_NE(texture, nullptr); + + ASSERT_EQ(texture->source_image().mime_type(), "image/png"); + + // Re-encode the texture again to ensure the content hasn't changed. + std::vector encoded_buffer; + DRACO_ASSERT_OK(draco::WriteTextureToBuffer(*texture, &encoded_buffer)); + + ASSERT_EQ(image_data.size(), encoded_buffer.size()); + for (int i = 0; i < encoded_buffer.size(); ++i) { + ASSERT_EQ(image_data[i], encoded_buffer[i]); + } +} + +} // namespace + +#endif // DRACO_TRANSCODER_SUPPORTED diff --git a/contrib/draco/src/draco/io/tiny_gltf_utils.cc b/contrib/draco/src/draco/io/tiny_gltf_utils.cc new file mode 100644 index 000000000..d57e1093c --- /dev/null +++ b/contrib/draco/src/draco/io/tiny_gltf_utils.cc @@ -0,0 +1,230 @@ +// Copyright 2021 The Draco Authors. +// +// 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 "draco/io/tiny_gltf_utils.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED +#include "draco/animation/animation.h" +#include "draco/animation/node_animation_data.h" +#include "draco/core/status.h" +#include "draco/core/vector_d.h" +#include "tiny_gltf.h" + +namespace draco { + +int TinyGltfUtils::GetNumComponentsForType(int type) { + switch (type) { + case TINYGLTF_TYPE_SCALAR: + return 1; + case TINYGLTF_TYPE_VEC2: + return 2; + case TINYGLTF_TYPE_VEC3: + return 3; + case TINYGLTF_TYPE_VEC4: + case TINYGLTF_TYPE_MAT2: + return 4; + case TINYGLTF_TYPE_MAT3: + return 9; + case TINYGLTF_TYPE_MAT4: + return 16; + } + return 0; +} + +Material::TransparencyMode TinyGltfUtils::TextToMaterialMode( + const std::string &mode) { + if (mode == "MASK") { + return Material::TRANSPARENCY_MASK; + } else if (mode == "BLEND") { + return Material::TRANSPARENCY_BLEND; + } else { + return Material::TRANSPARENCY_OPAQUE; + } +} + +AnimationSampler::SamplerInterpolation +TinyGltfUtils::TextToSamplerInterpolation(const std::string &interpolation) { + if (interpolation == "STEP") { + return AnimationSampler::SamplerInterpolation::STEP; + } else if (interpolation == "CUBICSPLINE") { + return AnimationSampler::SamplerInterpolation::CUBICSPLINE; + } else { + return AnimationSampler::SamplerInterpolation::LINEAR; + } +} + +AnimationChannel::ChannelTransformation +TinyGltfUtils::TextToChannelTransformation(const std::string &path) { + if (path == "rotation") { + return AnimationChannel::ChannelTransformation::ROTATION; + } else if (path == "scale") { + return AnimationChannel::ChannelTransformation::SCALE; + } else if (path == "weights") { + return AnimationChannel::ChannelTransformation::WEIGHTS; + } else { + return AnimationChannel::ChannelTransformation::TRANSLATION; + } +} + +Status TinyGltfUtils::AddChannelToAnimation( + const tinygltf::Model &model, const tinygltf::Animation &input_animation, + const tinygltf::AnimationChannel &channel, int node_index, + Animation *animation) { + std::unique_ptr new_channel(new AnimationChannel()); + + const tinygltf::AnimationSampler &sampler = + input_animation.samplers[channel.sampler]; + // Add the sampler associated with the channel. + DRACO_RETURN_IF_ERROR( + TinyGltfUtils::AddSamplerToAnimation(model, sampler, animation)); + new_channel->sampler_index = animation->NumSamplers() - 1; + new_channel->target_index = node_index; + new_channel->transformation_type = + TinyGltfUtils::TextToChannelTransformation(channel.target_path); + + animation->AddChannel(std::move(new_channel)); + return OkStatus(); +} + +Status TinyGltfUtils::AddSamplerToAnimation( + const tinygltf::Model &model, const tinygltf::AnimationSampler &sampler, + Animation *animation) { + std::unique_ptr node_animation_data( + new NodeAnimationData()); + // TODO(fgalligan): Add support to not copy the accessor data if it is + // referenced more than once. Currently we duplicate all animation data so + // that it is referenced only once in the glTF file. + const tinygltf::Accessor &input_accessor = model.accessors[sampler.input]; + DRACO_RETURN_IF_ERROR(AddAccessorToAnimationData(model, input_accessor, + node_animation_data.get())); + animation->AddNodeAnimationData(std::move(node_animation_data)); + std::unique_ptr new_sampler(new AnimationSampler()); + new_sampler->input_index = animation->NumNodeAnimationData() - 1; + + node_animation_data.reset(new NodeAnimationData()); + const tinygltf::Accessor &output_accessor = model.accessors[sampler.output]; + DRACO_RETURN_IF_ERROR(AddAccessorToAnimationData(model, output_accessor, + node_animation_data.get())); + animation->AddNodeAnimationData(std::move(node_animation_data)); + new_sampler->output_index = animation->NumNodeAnimationData() - 1; + + new_sampler->interpolation_type = + TinyGltfUtils::TextToSamplerInterpolation(sampler.interpolation); + animation->AddSampler(std::move(new_sampler)); + return OkStatus(); +} + +// Specialization for returning the data from |accessor| as a vector of float. +template <> +StatusOr> TinyGltfUtils::CopyDataAsFloat( + const tinygltf::Model &model, const tinygltf::Accessor &accessor) { + const int num_components = GetNumComponentsForType(accessor.type); + if (num_components != 1) { + return Status(Status::DRACO_ERROR, + "Dimension does not equal num components."); + } + return CopyDataAsFloatImpl(model, accessor); +} + +// Specialization for returing the data from |accessor| as a vector of +// Matrix4x4. +template <> +StatusOr> TinyGltfUtils::CopyDataAsFloat( + const tinygltf::Model &model, const tinygltf::Accessor &accessor) { + const int num_components = GetNumComponentsForType(accessor.type); + if (num_components != 16) { + return Status(Status::DRACO_ERROR, + "Dimension does not equal num components."); + } + return CopyDataAsFloatImpl(model, accessor); +} + +Status TinyGltfUtils::AddAccessorToAnimationData( + const tinygltf::Model &model, const tinygltf::Accessor &accessor, + NodeAnimationData *node_animation_data) { + if (accessor.componentType != TINYGLTF_COMPONENT_TYPE_FLOAT) { + return Status(Status::DRACO_ERROR, + "Unsupported ComponentType for NodeAnimationData."); + } + + std::vector *dest_data = node_animation_data->GetMutableData(); + if (accessor.type == TINYGLTF_TYPE_SCALAR) { + DRACO_ASSIGN_OR_RETURN(std::vector data, + CopyDataAsFloat(model, accessor)); + + for (int i = 0; i < data.size(); ++i) { + dest_data->push_back(data[i]); + } + node_animation_data->SetType(NodeAnimationData::Type::SCALAR); + } else if (accessor.type == TINYGLTF_TYPE_VEC3) { + DRACO_ASSIGN_OR_RETURN(std::vector data, + CopyDataAsFloat(model, accessor)); + + for (int i = 0; i < data.size(); ++i) { + for (int j = 0; j < 3; ++j) { + dest_data->push_back(data[i][j]); + } + } + node_animation_data->SetType(NodeAnimationData::Type::VEC3); + } else if (accessor.type == TINYGLTF_TYPE_VEC4) { + DRACO_ASSIGN_OR_RETURN(std::vector data, + CopyDataAsFloat(model, accessor)); + + for (int i = 0; i < data.size(); ++i) { + for (int j = 0; j < 4; ++j) { + dest_data->push_back(data[i][j]); + } + } + node_animation_data->SetType(NodeAnimationData::Type::VEC4); + } else if (accessor.type == TINYGLTF_TYPE_MAT4) { + DRACO_ASSIGN_OR_RETURN(std::vector data, + CopyDataAsFloat(model, accessor)); + + for (int i = 0; i < data.size(); ++i) { + for (int j = 0; j < 16; ++j) { + dest_data->push_back(data[i](j)); + } + } + node_animation_data->SetType(NodeAnimationData::Type::MAT4); + } else { + return Status(Status::DRACO_ERROR, + "Unsupported Type for GltfNodeAnimationData."); + } + node_animation_data->SetCount(accessor.count); + node_animation_data->SetNormalized(accessor.normalized); + return OkStatus(); +} + +template <> +void TinyGltfUtils::SetDataImpl(float value, int index, float *values) { + *values = value; +} + +template <> +void TinyGltfUtils::SetDataImpl(float value, int index, + Eigen::Matrix4f *values) { + (*values)(index) = value; +} + +} // namespace draco + +// Actual definitions needed by the tinygltf library using our configuration. +#define STB_IMAGE_IMPLEMENTATION +#define STB_IMAGE_WRITE_IMPLEMENTATION +#define TINYGLTF_ENABLE_DRACO +#define TINYGLTF_IMPLEMENTATION + +#include "tiny_gltf.h" + +#endif // DRACO_TRANSCODER_SUPPORTED diff --git a/contrib/draco/src/draco/io/tiny_gltf_utils.h b/contrib/draco/src/draco/io/tiny_gltf_utils.h new file mode 100644 index 000000000..a536a70fb --- /dev/null +++ b/contrib/draco/src/draco/io/tiny_gltf_utils.h @@ -0,0 +1,140 @@ +// Copyright 2019 The Draco Authors. +// +// 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 DRACO_IO_TINY_GLTF_UTILS_H_ +#define DRACO_IO_TINY_GLTF_UTILS_H_ + +#include "draco/draco_features.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED +#include "Eigen/Geometry" +#include "draco/animation/animation.h" +#include "draco/core/status.h" +#include "draco/core/status_or.h" +#include "draco/material/material.h" + +#define TINYGLTF_ENCLOSING_NAMESPACE draco +#include "tiny_gltf.h" + +namespace draco { + +class TinyGltfUtils { + public: + TinyGltfUtils() {} + + // Returns the number of components for the attribute type. + static int GetNumComponentsForType(int type); + + // Returns the material transparency mode in |mode|. + static Material::TransparencyMode TextToMaterialMode(const std::string &mode); + + // Returns the animation sampler interpolation in |interpolation|. + static AnimationSampler::SamplerInterpolation TextToSamplerInterpolation( + const std::string &interpolation); + + // Returns the animation channel transformation in |path|. + static AnimationChannel::ChannelTransformation TextToChannelTransformation( + const std::string &path); + + // Adds all of the animation data associated with a channel. + // The channel references a sampler, whose data will be added to the + // |animation|. The sampler references input and output accessors, + // whose data will be added to the |animation|. + static Status AddChannelToAnimation( + const tinygltf::Model &model, const tinygltf::Animation &input_animation, + const tinygltf::AnimationChannel &channel, int node_index, + Animation *animation); + + // Adds all of the sampler data. The sampler references + // input and output accessors, whose data will be added to the |animation|. + static Status AddSamplerToAnimation(const tinygltf::Model &model, + const tinygltf::AnimationSampler &sampler, + Animation *animation); + + // Converts the gltf2 animation accessor and adds it to + // |node_animation_data|. + static Status AddAccessorToAnimationData( + const tinygltf::Model &model, const tinygltf::Accessor &accessor, + NodeAnimationData *node_animation_data); + + // Returns the data from |accessor| as a vector of |T|. + template + static StatusOr> CopyDataAsFloat( + const tinygltf::Model &model, const tinygltf::Accessor &accessor) { + const int num_components = GetNumComponentsForType(accessor.type); + if (num_components != T::dimension) { + return Status(Status::DRACO_ERROR, + "Dimension does not equal num components."); + } + return CopyDataAsFloatImpl(model, accessor); + } + + private: + template + static StatusOr> CopyDataAsFloatImpl( + const tinygltf::Model &model, const tinygltf::Accessor &accessor) { + if (accessor.componentType != TINYGLTF_COMPONENT_TYPE_FLOAT) { + return Status(Status::DRACO_ERROR, + "Non-float data is not supported by CopyDataAsFloat()."); + } + if (accessor.bufferView < 0) { + return Status(Status::DRACO_ERROR, + "Error CopyDataAsFloat() bufferView < 0."); + } + + const tinygltf::BufferView &buffer_view = + model.bufferViews[accessor.bufferView]; + if (buffer_view.buffer < 0) { + return Status(Status::DRACO_ERROR, "Error CopyDataAsFloat() buffer < 0."); + } + + const tinygltf::Buffer &buffer = model.buffers[buffer_view.buffer]; + + const unsigned char *const data_start = + buffer.data.data() + buffer_view.byteOffset + accessor.byteOffset; + const int byte_stride = accessor.ByteStride(buffer_view); + const int component_size = + tinygltf::GetComponentSizeInBytes(accessor.componentType); + + std::vector output; + output.resize(accessor.count); + + const int num_components = GetNumComponentsForType(accessor.type); + const unsigned char *data = data_start; + for (int i = 0; i < accessor.count; ++i) { + T values; + + for (int c = 0; c < num_components; ++c) { + float value = 0.0f; + memcpy(&value, data + (c * component_size), component_size); + SetDataImpl(value, c, &values); + } + + output[i] = values; + data += byte_stride; + } + + return output; + } + + template + static void SetDataImpl(float value, int index, T *values) { + (*values)[index] = value; + } +}; + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED +#endif // DRACO_IO_TINY_GLTF_UTILS_H_ diff --git a/contrib/draco/src/draco/javascript/emscripten/animation_decoder_webidl_wrapper.cc b/contrib/draco/src/draco/javascript/emscripten/animation_decoder_webidl_wrapper.cc deleted file mode 100644 index 7e9e6d15d..000000000 --- a/contrib/draco/src/draco/javascript/emscripten/animation_decoder_webidl_wrapper.cc +++ /dev/null @@ -1,101 +0,0 @@ -// Copyright 2017 The Draco Authors. -// -// 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 "draco/javascript/emscripten/animation_decoder_webidl_wrapper.h" - -#include - -#include "draco/compression/decode.h" -#include "draco/mesh/mesh.h" -#include "draco/mesh/mesh_stripifier.h" - -using draco::DecoderBuffer; -using draco::PointAttribute; -using draco::Status; - -DracoFloat32Array::DracoFloat32Array() {} - -float DracoFloat32Array::GetValue(int index) const { return values_[index]; } - -bool DracoFloat32Array::SetValues(const float *values, int count) { - if (values) { - values_.assign(values, values + count); - } else { - values_.resize(count); - } - return true; -} - -AnimationDecoder::AnimationDecoder() {} - -// Decodes animation data from the provided buffer. -const draco::Status *AnimationDecoder::DecodeBufferToKeyframeAnimation( - draco::DecoderBuffer *in_buffer, draco::KeyframeAnimation *animation) { - draco::DecoderOptions dec_options; - last_status_ = decoder_.Decode(dec_options, in_buffer, animation); - return &last_status_; -} - -bool AnimationDecoder::GetTimestamps(const draco::KeyframeAnimation &animation, - DracoFloat32Array *timestamp) { - if (!timestamp) { - return false; - } - const int num_frames = animation.num_frames(); - const draco::PointAttribute *timestamp_att = animation.timestamps(); - // Timestamp attribute has only 1 component, so the number of components is - // equal to the number of frames. - timestamp->SetValues(nullptr, num_frames); - int entry_id = 0; - float timestamp_value = -1.0; - for (draco::PointIndex i(0); i < num_frames; ++i) { - const draco::AttributeValueIndex val_index = timestamp_att->mapped_index(i); - if (!timestamp_att->ConvertValue(val_index, ×tamp_value)) { - return false; - } - timestamp->SetValue(entry_id++, timestamp_value); - } - return true; -} - -bool AnimationDecoder::GetKeyframes(const draco::KeyframeAnimation &animation, - int keyframes_id, - DracoFloat32Array *animation_data) { - const int num_frames = animation.num_frames(); - // Get animation data. - const draco::PointAttribute *animation_data_att = - animation.keyframes(keyframes_id); - if (!animation_data_att) { - return false; - } - - const int components = animation_data_att->num_components(); - const int num_entries = num_frames * components; - const int kMaxAttributeFloatValues = 4; - - std::vector values(components, -1.0); - int entry_id = 0; - animation_data->SetValues(nullptr, num_entries); - for (draco::PointIndex i(0); i < num_frames; ++i) { - const draco::AttributeValueIndex val_index = - animation_data_att->mapped_index(i); - if (!animation_data_att->ConvertValue(val_index, &values[0])) { - return false; - } - for (int j = 0; j < components; ++j) { - animation_data->SetValue(entry_id++, values[j]); - } - } - return true; -} diff --git a/contrib/draco/src/draco/javascript/emscripten/animation_decoder_webidl_wrapper.h b/contrib/draco/src/draco/javascript/emscripten/animation_decoder_webidl_wrapper.h deleted file mode 100644 index 7486d1503..000000000 --- a/contrib/draco/src/draco/javascript/emscripten/animation_decoder_webidl_wrapper.h +++ /dev/null @@ -1,73 +0,0 @@ -// Copyright 2017 The Draco Authors. -// -// 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 DRACO_JAVASCRIPT_EMSCRIPTEN_ANIMATION_DECODER_WEBIDL_WRAPPER_H_ -#define DRACO_JAVASCRIPT_EMSCRIPTEN_ANIMATION_DECODER_WEBIDL_WRAPPER_H_ - -#include - -#include "draco/animation/keyframe_animation_decoder.h" -#include "draco/attributes/attribute_transform_type.h" -#include "draco/attributes/point_attribute.h" -#include "draco/compression/config/compression_shared.h" -#include "draco/compression/decode.h" -#include "draco/core/decoder_buffer.h" - -typedef draco::AttributeTransformType draco_AttributeTransformType; -typedef draco::GeometryAttribute draco_GeometryAttribute; -typedef draco_GeometryAttribute::Type draco_GeometryAttribute_Type; -typedef draco::EncodedGeometryType draco_EncodedGeometryType; -typedef draco::Status draco_Status; -typedef draco::Status::Code draco_StatusCode; - -class DracoFloat32Array { - public: - DracoFloat32Array(); - float GetValue(int index) const; - - // In case |values| is nullptr, the data is allocated but not initialized. - bool SetValues(const float *values, int count); - - // Directly sets a value for a specific index. The array has to be already - // allocated at this point (using SetValues() method). - void SetValue(int index, float val) { values_[index] = val; } - - int size() const { return values_.size(); } - - private: - std::vector values_; -}; - -// Class used by emscripten WebIDL Binder [1] to wrap calls to decode animation -// data. -class AnimationDecoder { - public: - AnimationDecoder(); - - // Decodes animation data from the provided buffer. - const draco::Status *DecodeBufferToKeyframeAnimation( - draco::DecoderBuffer *in_buffer, draco::KeyframeAnimation *animation); - - static bool GetTimestamps(const draco::KeyframeAnimation &animation, - DracoFloat32Array *timestamp); - - static bool GetKeyframes(const draco::KeyframeAnimation &animation, - int keyframes_id, DracoFloat32Array *animation_data); - - private: - draco::KeyframeAnimationDecoder decoder_; - draco::Status last_status_; -}; - -#endif // DRACO_JAVASCRIPT_EMSCRIPTEN_ANIMATION_DECODER_WEBIDL_WRAPPER_H_ diff --git a/contrib/draco/src/draco/javascript/emscripten/animation_encoder_webidl_wrapper.cc b/contrib/draco/src/draco/javascript/emscripten/animation_encoder_webidl_wrapper.cc deleted file mode 100644 index 53a10e5e4..000000000 --- a/contrib/draco/src/draco/javascript/emscripten/animation_encoder_webidl_wrapper.cc +++ /dev/null @@ -1,89 +0,0 @@ -// Copyright 2017 The Draco Authors. -// -// 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 "draco/javascript/emscripten/animation_encoder_webidl_wrapper.h" - -#include "draco/animation/keyframe_animation.h" -#include "draco/animation/keyframe_animation_encoder.h" - -DracoInt8Array::DracoInt8Array() {} - -int DracoInt8Array::GetValue(int index) const { return values_[index]; } - -bool DracoInt8Array::SetValues(const char *values, int count) { - values_.assign(values, values + count); - return true; -} - -AnimationBuilder::AnimationBuilder() {} - -bool AnimationBuilder::SetTimestamps(draco::KeyframeAnimation *animation, - long num_frames, const float *timestamps) { - if (!animation || !timestamps) { - return false; - } - std::vector timestamps_arr( - timestamps, timestamps + num_frames); - return animation->SetTimestamps(timestamps_arr); -} - -int AnimationBuilder::AddKeyframes(draco::KeyframeAnimation *animation, - long num_frames, long num_components, - const float *animation_data) { - if (!animation || !animation_data) { - return -1; - } - std::vector keyframes_arr( - animation_data, animation_data + num_frames * num_components); - return animation->AddKeyframes(draco::DT_FLOAT32, num_components, - keyframes_arr); -} - -AnimationEncoder::AnimationEncoder() - : timestamps_quantization_bits_(-1), - keyframes_quantization_bits_(-1), - options_(draco::EncoderOptions::CreateDefaultOptions()) {} - -void AnimationEncoder::SetTimestampsQuantization(long quantization_bits) { - timestamps_quantization_bits_ = quantization_bits; -} - -void AnimationEncoder::SetKeyframesQuantization(long quantization_bits) { - keyframes_quantization_bits_ = quantization_bits; -} - -int AnimationEncoder::EncodeAnimationToDracoBuffer( - draco::KeyframeAnimation *animation, DracoInt8Array *draco_buffer) { - if (!animation) { - return 0; - } - draco::EncoderBuffer buffer; - - if (timestamps_quantization_bits_ > 0) { - options_.SetAttributeInt(0, "quantization_bits", - timestamps_quantization_bits_); - } - if (keyframes_quantization_bits_ > 0) { - for (int i = 1; i <= animation->num_animations(); ++i) { - options_.SetAttributeInt(i, "quantization_bits", - keyframes_quantization_bits_); - } - } - if (!encoder_.EncodeKeyframeAnimation(*animation, options_, &buffer).ok()) { - return 0; - } - - draco_buffer->SetValues(buffer.data(), buffer.size()); - return buffer.size(); -} diff --git a/contrib/draco/src/draco/javascript/emscripten/animation_encoder_webidl_wrapper.h b/contrib/draco/src/draco/javascript/emscripten/animation_encoder_webidl_wrapper.h deleted file mode 100644 index f2ac733d1..000000000 --- a/contrib/draco/src/draco/javascript/emscripten/animation_encoder_webidl_wrapper.h +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright 2017 The Draco Authors. -// -// 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 DRACO_JAVASCRIPT_EMSCRIPTEN_ANIMATION_ENCODER_WEBIDL_WRAPPER_H_ -#define DRACO_JAVASCRIPT_EMSCRIPTEN_ANIMATION_ENCODER_WEBIDL_WRAPPER_H_ - -#include - -#include "draco/animation/keyframe_animation_encoder.h" -#include "draco/attributes/point_attribute.h" -#include "draco/compression/config/compression_shared.h" -#include "draco/compression/config/encoder_options.h" -#include "draco/compression/encode.h" - -class DracoInt8Array { - public: - DracoInt8Array(); - int GetValue(int index) const; - bool SetValues(const char *values, int count); - - size_t size() { return values_.size(); } - - private: - std::vector values_; -}; - -class AnimationBuilder { - public: - AnimationBuilder(); - - bool SetTimestamps(draco::KeyframeAnimation *animation, long num_frames, - const float *timestamps); - - int AddKeyframes(draco::KeyframeAnimation *animation, long num_frames, - long num_components, const float *animation_data); -}; - -class AnimationEncoder { - public: - AnimationEncoder(); - - void SetTimestampsQuantization(long quantization_bits); - // TODO: Use expert encoder to set per attribute quantization. - void SetKeyframesQuantization(long quantization_bits); - int EncodeAnimationToDracoBuffer(draco::KeyframeAnimation *animation, - DracoInt8Array *draco_buffer); - - private: - draco::KeyframeAnimationEncoder encoder_; - long timestamps_quantization_bits_; - long keyframes_quantization_bits_; - draco::EncoderOptions options_; -}; - -#endif // DRACO_JAVASCRIPT_EMSCRIPTEN_ANIMATION_ENCODER_WEBIDL_WRAPPER_H_ diff --git a/contrib/draco/src/draco/javascript/emscripten/decoder_webidl_wrapper.cc b/contrib/draco/src/draco/javascript/emscripten/decoder_webidl_wrapper.cc index 66fe77dbd..034f3c3b4 100644 --- a/contrib/draco/src/draco/javascript/emscripten/decoder_webidl_wrapper.cc +++ b/contrib/draco/src/draco/javascript/emscripten/decoder_webidl_wrapper.cc @@ -221,14 +221,13 @@ bool Decoder::GetAttributeFloatForAllPoints(const PointCloud &pc, const int components = pa.num_components(); const int num_points = pc.num_points(); const int num_entries = num_points * components; - const int kMaxAttributeFloatValues = 4; - float values[kMaxAttributeFloatValues] = {-2.0, -2.0, -2.0, -2.0}; + std::vector values(components, -2.f); int entry_id = 0; out_values->Resize(num_entries); for (draco::PointIndex i(0); i < num_points; ++i) { const draco::AttributeValueIndex val_index = pa.mapped_index(i); - if (!pa.ConvertValue(val_index, values)) { + if (!pa.ConvertValue(val_index, &values[0])) { return false; } for (int j = 0; j < components; ++j) { @@ -249,17 +248,16 @@ bool Decoder::GetAttributeFloatArrayForAllPoints(const PointCloud &pc, return false; } const bool requested_type_is_float = pa.data_type() == draco::DT_FLOAT32; - const int kMaxAttributeFloatValues = 4; - float values[kMaxAttributeFloatValues] = {-2.0, -2.0, -2.0, -2.0}; + std::vector values(components, -2.f); int entry_id = 0; float *const floats = reinterpret_cast(out_values); for (draco::PointIndex i(0); i < num_points; ++i) { const draco::AttributeValueIndex val_index = pa.mapped_index(i); if (requested_type_is_float) { - pa.GetValue(val_index, values); + pa.GetValue(val_index, &values[0]); } else { - if (!pa.ConvertValue(val_index, values)) { + if (!pa.ConvertValue(val_index, &values[0])) { return false; } } diff --git a/contrib/draco/src/draco/javascript/emscripten/draco_animation_decoder_glue_wrapper.cc b/contrib/draco/src/draco/javascript/emscripten/draco_animation_decoder_glue_wrapper.cc deleted file mode 100644 index 83ed98fdc..000000000 --- a/contrib/draco/src/draco/javascript/emscripten/draco_animation_decoder_glue_wrapper.cc +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright 2017 The Draco Authors. -// -// 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. -// -// This file is used by emscripten's WebIDL Binder. -// http://kripken.github.io/emscripten-site/docs/porting/connecting_cpp_and_javascript/WebIDL-Binder.html -#include "draco/attributes/attribute_octahedron_transform.h" -#include "draco/attributes/attribute_quantization_transform.h" -#include "draco/attributes/geometry_attribute.h" -#include "draco/attributes/point_attribute.h" -#include "draco/compression/decode.h" -#include "draco/core/decoder_buffer.h" -#include "draco/javascript/emscripten/animation_decoder_webidl_wrapper.h" -#include "draco/mesh/mesh.h" -#include "draco/point_cloud/point_cloud.h" - -// glue_animation_decoder.cpp is generated by Makefile.emcc build_glue target. -#include "glue_animation_decoder.cpp" diff --git a/contrib/draco/src/draco/javascript/emscripten/draco_animation_web_decoder.idl b/contrib/draco/src/draco/javascript/emscripten/draco_animation_web_decoder.idl deleted file mode 100644 index c9fe76b59..000000000 --- a/contrib/draco/src/draco/javascript/emscripten/draco_animation_web_decoder.idl +++ /dev/null @@ -1,52 +0,0 @@ -// Interface exposed to emscripten's WebIDL Binder. -// http://kripken.github.io/emscripten-site/docs/porting/connecting_cpp_and_javascript/WebIDL-Binder.html -[Prefix="draco::"] -interface DecoderBuffer { - void DecoderBuffer(); - void Init([Const] byte[] data, unsigned long data_size); -}; - -enum draco_StatusCode { - "draco_Status::OK", - "draco_Status::DRACO_ERROR", - "draco_Status::IO_ERROR", - "draco_Status::INVALID_PARAMETER", - "draco_Status::UNSUPPORTED_VERSION", - "draco_Status::UNKNOWN_VERSION", -}; - -[Prefix="draco::"] -interface Status { - draco_StatusCode code(); - boolean ok(); - [Const] DOMString error_msg(); -}; - -// Draco version of typed arrays. The memory of these arrays is allocated on the -// emscripten heap. -interface DracoFloat32Array { - void DracoFloat32Array(); - float GetValue(long index); - long size(); -}; - -[Prefix="draco::"] -interface KeyframeAnimation { - void KeyframeAnimation(); - long num_frames(); - long num_animations(); -}; - -interface AnimationDecoder { - void AnimationDecoder(); - - [Const] Status DecodeBufferToKeyframeAnimation(DecoderBuffer in_buffer, - KeyframeAnimation animation); - - boolean GetTimestamps([Ref, Const] KeyframeAnimation animation, - DracoFloat32Array timestamp); - - boolean GetKeyframes([Ref, Const] KeyframeAnimation animation, - long keyframes_id, - DracoFloat32Array animation_data); -}; diff --git a/contrib/draco/src/draco/javascript/emscripten/draco_animation_web_encoder.idl b/contrib/draco/src/draco/javascript/emscripten/draco_animation_web_encoder.idl deleted file mode 100644 index e74a4c9e4..000000000 --- a/contrib/draco/src/draco/javascript/emscripten/draco_animation_web_encoder.idl +++ /dev/null @@ -1,34 +0,0 @@ -// Interface exposed to emscripten's WebIDL Binder. -// http://kripken.github.io/emscripten-site/docs/porting/connecting_cpp_and_javascript/WebIDL-Binder.html -// Draco version of typed arrays. The memory of these arrays is allocated on the -// emscripten heap. -interface DracoInt8Array { - void DracoInt8Array(); - long GetValue(long index); - long size(); -}; - -[Prefix="draco::"] -interface KeyframeAnimation { - void KeyframeAnimation(); - long num_frames(); -}; - -interface AnimationBuilder { - void AnimationBuilder(); - boolean SetTimestamps(KeyframeAnimation animation, long num_frames, - [Const] float[] timestamps); - - long AddKeyframes(KeyframeAnimation animation, long num_frames, - long num_components, [Const] float[] animation_data); -}; - -interface AnimationEncoder { - void AnimationEncoder(); - - void SetTimestampsQuantization(long quantization_bits); - void SetKeyframesQuantization(long quantization_bits); - - long EncodeAnimationToDracoBuffer(KeyframeAnimation animation, - DracoInt8Array encoded_data); -}; diff --git a/contrib/draco/src/draco/javascript/emscripten/version.js b/contrib/draco/src/draco/javascript/emscripten/version.js index 46fb25271..b21f3b5e2 100644 --- a/contrib/draco/src/draco/javascript/emscripten/version.js +++ b/contrib/draco/src/draco/javascript/emscripten/version.js @@ -19,7 +19,7 @@ function isVersionSupported(versionString) { const version = versionString.split('.'); if (version.length < 2 || version.length > 3) return false; // Unexpected version string. - if (version[0] == 1 && version[1] >= 0 && version[1] <= 4) + if (version[0] == 1 && version[1] >= 0 && version[1] <= 5) return true; if (version[0] != 0 || version[1] > 10) return false; diff --git a/contrib/draco/src/draco/material/material.cc b/contrib/draco/src/draco/material/material.cc new file mode 100644 index 000000000..da854a172 --- /dev/null +++ b/contrib/draco/src/draco/material/material.cc @@ -0,0 +1,258 @@ +// Copyright 2018 The Draco Authors. +// +// 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 "draco/material/material.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED + +namespace draco { + +Material::Material() : Material(nullptr) {} + +Material::Material(TextureLibrary *texture_library) + : texture_library_(texture_library) { + Clear(); +} + +void Material::Copy(const Material &src) { + name_ = src.name_; + color_factor_ = src.color_factor_; + metallic_factor_ = src.metallic_factor_; + roughness_factor_ = src.roughness_factor_; + emissive_factor_ = src.emissive_factor_; + transparency_mode_ = src.transparency_mode_; + alpha_cutoff_ = src.alpha_cutoff_; + double_sided_ = src.double_sided_; + normal_texture_scale_ = src.normal_texture_scale_; + + // Copy properties of material extensions. + unlit_ = src.unlit_; + has_sheen_ = src.has_sheen_; + sheen_color_factor_ = src.sheen_color_factor_; + sheen_roughness_factor_ = src.sheen_roughness_factor_; + has_transmission_ = src.has_transmission_; + transmission_factor_ = src.transmission_factor_; + has_clearcoat_ = src.has_clearcoat_; + clearcoat_factor_ = src.clearcoat_factor_; + clearcoat_roughness_factor_ = src.clearcoat_roughness_factor_; + has_volume_ = src.has_volume_; + thickness_factor_ = src.thickness_factor_; + attenuation_distance_ = src.attenuation_distance_; + attenuation_color_ = src.attenuation_color_; + has_ior_ = src.has_ior_; + ior_ = src.ior_; + has_specular_ = src.has_specular_; + specular_factor_ = src.specular_factor_; + specular_color_factor_ = src.specular_color_factor_; + + // Copy texture maps. + texture_map_type_to_index_map_ = src.texture_map_type_to_index_map_; + texture_maps_.resize(src.texture_maps_.size()); + for (int i = 0; i < texture_maps_.size(); ++i) { + texture_maps_[i] = std::unique_ptr(new TextureMap()); + texture_maps_[i]->Copy(*src.texture_maps_[i]); + } +} + +void Material::Clear() { + ClearTextureMaps(); + + // Defaults correspond to the GLTF 2.0 spec. + name_.clear(); + color_factor_ = Vector4f(1.f, 1.f, 1.f, 1.f); + metallic_factor_ = 1.f; + roughness_factor_ = 1.f; + emissive_factor_ = Vector3f(0.f, 0.f, 0.f); + transparency_mode_ = TRANSPARENCY_OPAQUE; + alpha_cutoff_ = 0.5f; + double_sided_ = false; + normal_texture_scale_ = 1.0f; + + // Clear properties of material extensions to glTF 2.0 spec defaults. + unlit_ = false; + has_sheen_ = false; + sheen_color_factor_ = Vector3f(0.f, 0.f, 0.f); + sheen_roughness_factor_ = 0.f; + has_transmission_ = false; + transmission_factor_ = 0.f; + has_clearcoat_ = false; + clearcoat_factor_ = 0.f; + clearcoat_roughness_factor_ = 0.f; + has_volume_ = false; + thickness_factor_ = 0.f; + attenuation_distance_ = std::numeric_limits::max(); // Infinity. + attenuation_color_ = Vector3f(1.f, 1.f, 1.f); + has_ior_ = false; + ior_ = 1.5f; + has_specular_ = false; + specular_factor_ = 1.f; + specular_color_factor_ = Vector3f(1.f, 1.f, 1.f); +} + +void Material::ClearTextureMaps() { + texture_maps_.clear(); + texture_map_type_to_index_map_.clear(); +} + +void Material::SetTextureMap(TextureMap &&texture_map) { + std::unique_ptr new_texture_map(new TextureMap); + *new_texture_map = std::move(texture_map); + SetTextureMap(std::move(new_texture_map)); +} + +void Material::SetTextureMap(std::unique_ptr texture_map) { + const TextureMap::Type type = texture_map->type(); + const auto it = texture_map_type_to_index_map_.find(type); + // Only one texture of a given type is allowed to exist. + if (it == texture_map_type_to_index_map_.end()) { + texture_maps_.push_back(std::move(texture_map)); + texture_map_type_to_index_map_[type] = texture_maps_.size() - 1; + } else { + texture_maps_[it->second] = std::move(texture_map); + } +} + +void Material::SetTextureMap(std::unique_ptr texture, + TextureMap::Type texture_map_type, + int tex_coord_index) { + SetTextureMap(std::move(texture), texture_map_type, + TextureMap::WrappingMode(TextureMap::CLAMP_TO_EDGE), + tex_coord_index); +} + +void Material::SetTextureMap(std::unique_ptr texture, + TextureMap::Type texture_map_type, + TextureMap::WrappingMode wrapping_mode, + int tex_coord_index) { + std::unique_ptr texture_map(new TextureMap); + texture_map->SetProperties(texture_map_type, wrapping_mode, tex_coord_index); + + if (texture_library_) { + texture_map->SetTexture(texture.get()); + texture_library_->PushTexture(std::move(texture)); + } else { + texture_map->SetTexture(std::move(texture)); + } + SetTextureMap(std::move(texture_map)); +} + +Status Material::SetTextureMap(Texture *texture, + TextureMap::Type texture_map_type, + int tex_coord_index) { + return SetTextureMap(texture, texture_map_type, + TextureMap::WrappingMode(TextureMap::CLAMP_TO_EDGE), + TextureMap::UNSPECIFIED, TextureMap::UNSPECIFIED, + tex_coord_index); +} + +Status Material::SetTextureMap(Texture *texture, + TextureMap::Type texture_map_type, + TextureMap::WrappingMode wrapping_mode, + int tex_coord_index) { + std::unique_ptr texture_map(new TextureMap); + return SetTextureMap(std::move(texture_map), texture, texture_map_type, + wrapping_mode, TextureMap::UNSPECIFIED, + TextureMap::UNSPECIFIED, tex_coord_index); +} + +Status Material::SetTextureMap(Texture *texture, + TextureMap::Type texture_map_type, + TextureMap::WrappingMode wrapping_mode, + TextureMap::FilterType min_filter, + TextureMap::FilterType mag_filter, + int tex_coord_index) { + std::unique_ptr texture_map(new TextureMap); + return SetTextureMap(std::move(texture_map), texture, texture_map_type, + wrapping_mode, min_filter, mag_filter, tex_coord_index); +} + +Status Material::SetTextureMap(Texture *texture, + TextureMap::Type texture_map_type, + TextureMap::WrappingMode wrapping_mode, + TextureMap::FilterType min_filter, + TextureMap::FilterType mag_filter, + const TextureTransform &transform, + int tex_coord_index) { + std::unique_ptr texture_map(new TextureMap); + texture_map->SetTransform(transform); + return SetTextureMap(std::move(texture_map), texture, texture_map_type, + wrapping_mode, min_filter, mag_filter, tex_coord_index); +} + +Status Material::SetTextureMap(std::unique_ptr texture_map, + Texture *texture, + TextureMap::Type texture_map_type, + TextureMap::WrappingMode wrapping_mode, + TextureMap::FilterType min_filter, + TextureMap::FilterType mag_filter, + int tex_coord_index) { + if (!IsTextureOwned(*texture)) { + return Status(Status::DRACO_ERROR, + "Provided texture is not owned by the material."); + } + texture_map->SetProperties(texture_map_type, wrapping_mode, tex_coord_index, + min_filter, mag_filter); + texture_map->SetTexture(texture); + SetTextureMap(std::move(texture_map)); + return OkStatus(); +} + +bool Material::IsTextureOwned(const Texture &texture) { + if (texture_library_) { + // Ensure the texture is owned by the texture library. + for (int ti = 0; ti < texture_library_->NumTextures(); ++ti) { + if (texture_library_->GetTexture(ti) == &texture) { + return true; + } + } + return false; + } + // Else we need to check every texture map of this material. + for (int ti = 0; ti < NumTextureMaps(); ++ti) { + if (GetTextureMapByIndex(ti)->texture() == &texture) { + return true; + } + } + return false; +} + +std::unique_ptr Material::RemoveTextureMapByIndex(int index) { + if (index < 0 || index >= texture_maps_.size()) { + return nullptr; + } + std::unique_ptr ret = std::move(texture_maps_[index]); + texture_maps_.erase(texture_maps_.begin() + index); + // A texture map was removed and we need to update + // |texture_map_type_to_index_map_| to reflect the changes. + for (int i = index; i < texture_maps_.size(); ++i) { + texture_map_type_to_index_map_[texture_maps_[i]->type()] = i; + } + // Delete the removed texture map type. + texture_map_type_to_index_map_.erase( + texture_map_type_to_index_map_.find(ret->type())); + return ret; +} + +std::unique_ptr Material::RemoveTextureMapByType( + TextureMap::Type texture_type) { + const auto it = texture_map_type_to_index_map_.find(texture_type); + if (it == texture_map_type_to_index_map_.end()) { + return nullptr; + } + return RemoveTextureMapByIndex(it->second); +} + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED diff --git a/contrib/draco/src/draco/material/material.h b/contrib/draco/src/draco/material/material.h new file mode 100644 index 000000000..7c405b45c --- /dev/null +++ b/contrib/draco/src/draco/material/material.h @@ -0,0 +1,276 @@ +// Copyright 2018 The Draco Authors. +// +// 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 DRACO_MATERIAL_MATERIAL_H_ +#define DRACO_MATERIAL_MATERIAL_H_ + +#include "draco/draco_features.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED +#include +#include + +#include "draco/core/status.h" +#include "draco/core/vector_d.h" +#include "draco/texture/texture_library.h" +#include "draco/texture/texture_map.h" + +namespace draco { + +// Material specification for Draco geometry. Parameters are based on the +// metallic-roughness PBR model adopted by GLTF 2.0 standard: +// https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#materials +class Material { + public: + enum TransparencyMode { + TRANSPARENCY_OPAQUE = 0, + TRANSPARENCY_MASK, + TRANSPARENCY_BLEND + }; + + Material(); + explicit Material(TextureLibrary *texture_library); + + // Copies all material data from the |src| material to this material. + void Copy(const Material &src); + + // Deletes all texture maps and resets all material properties to default + // values. + void Clear(); + + // Deletes all texture maps from the material while keeping other material + // properties unchanged. + void ClearTextureMaps(); + + const std::string &GetName() const { return name_; } + void SetName(const std::string &name) { name_ = name; } + Vector4f GetColorFactor() const { return color_factor_; } + void SetColorFactor(const Vector4f &color_factor) { + color_factor_ = color_factor; + } + float GetMetallicFactor() const { return metallic_factor_; } + void SetMetallicFactor(float metallic_factor) { + metallic_factor_ = metallic_factor; + } + float GetRoughnessFactor() const { return roughness_factor_; } + void SetRoughnessFactor(float roughness_factor) { + roughness_factor_ = roughness_factor; + } + Vector3f GetEmissiveFactor() const { return emissive_factor_; } + void SetEmissiveFactor(const Vector3f &emissive_factor) { + emissive_factor_ = emissive_factor; + } + bool GetDoubleSided() const { return double_sided_; } + void SetDoubleSided(bool double_sided) { double_sided_ = double_sided; } + TransparencyMode GetTransparencyMode() const { return transparency_mode_; } + void SetTransparencyMode(TransparencyMode mode) { transparency_mode_ = mode; } + float GetAlphaCutoff() const { return alpha_cutoff_; } + void SetAlphaCutoff(float alpha_cutoff) { alpha_cutoff_ = alpha_cutoff; } + float GetNormalTextureScale() const { return normal_texture_scale_; } + void SetNormalTextureScale(float scale) { normal_texture_scale_ = scale; } + + // Properties of glTF material extension KHR_materials_unlit. + bool GetUnlit() const { return unlit_; } + void SetUnlit(bool unlit) { unlit_ = unlit; } + + // Properties of glTF material extension KHR_materials_sheen. + bool HasSheen() const { return has_sheen_; } + void SetHasSheen(bool value) { has_sheen_ = value; } + Vector3f GetSheenColorFactor() const { return sheen_color_factor_; } + void SetSheenColorFactor(const Vector3f &value) { + sheen_color_factor_ = value; + } + float GetSheenRoughnessFactor() const { return sheen_roughness_factor_; } + void SetSheenRoughnessFactor(float value) { sheen_roughness_factor_ = value; } + + // Properties of glTF material extension KHR_materials_transmission. + bool HasTransmission() const { return has_transmission_; } + void SetHasTransmission(bool value) { has_transmission_ = value; } + float GetTransmissionFactor() const { return transmission_factor_; } + void SetTransmissionFactor(float value) { transmission_factor_ = value; } + + // Properties of glTF material extension KHR_materials_clearcoat. + bool HasClearcoat() const { return has_clearcoat_; } + void SetHasClearcoat(bool value) { has_clearcoat_ = value; } + float GetClearcoatFactor() const { return clearcoat_factor_; } + void SetClearcoatFactor(float value) { clearcoat_factor_ = value; } + float GetClearcoatRoughnessFactor() const { + return clearcoat_roughness_factor_; + } + void SetClearcoatRoughnessFactor(float value) { + clearcoat_roughness_factor_ = value; + } + + // Properties of glTF material extension KHR_materials_volume. + bool HasVolume() const { return has_volume_; } + void SetHasVolume(bool value) { has_volume_ = value; } + float GetThicknessFactor() const { return thickness_factor_; } + void SetThicknessFactor(float value) { thickness_factor_ = value; } + float GetAttenuationDistance() const { return attenuation_distance_; } + void SetAttenuationDistance(float value) { attenuation_distance_ = value; } + Vector3f GetAttenuationColor() const { return attenuation_color_; } + void SetAttenuationColor(const Vector3f &value) { + attenuation_color_ = value; + } + + // Properties of glTF material extension KHR_materials_ior. + bool HasIor() const { return has_ior_; } + void SetHasIor(bool value) { has_ior_ = value; } + float GetIor() const { return ior_; } + void SetIor(float value) { ior_ = value; } + + // Properties of glTF material extension KHR_materials_specular. + bool HasSpecular() const { return has_specular_; } + void SetHasSpecular(bool value) { has_specular_ = value; } + float GetSpecularFactor() const { return specular_factor_; } + void SetSpecularFactor(float value) { specular_factor_ = value; } + Vector3f GetSpecularColorFactor() const { return specular_color_factor_; } + void SetSpecularColorFactor(const Vector3f &value) { + specular_color_factor_ = value; + } + + // Methods for working with texture maps. + size_t NumTextureMaps() const { return texture_maps_.size(); } + const TextureMap *GetTextureMapByIndex(int index) const { + return texture_maps_[index].get(); + } + TextureMap *GetTextureMapByIndex(int index) { + return texture_maps_[index].get(); + } + const TextureMap *GetTextureMapByType(TextureMap::Type texture_type) const { + const auto it = texture_map_type_to_index_map_.find(texture_type); + if (it == texture_map_type_to_index_map_.end()) { + return nullptr; + } + return GetTextureMapByIndex(it->second); + } + TextureMap *GetTextureMapByType(TextureMap::Type texture_type) { + const auto it = texture_map_type_to_index_map_.find(texture_type); + if (it == texture_map_type_to_index_map_.end()) { + return nullptr; + } + return GetTextureMapByIndex(it->second); + } + + // TODO(b/146061359): Refactor the set texture map code. + // Specifies a new texture map using a texture with a given type. + // |tex_coord_index| defines which texture coordinate attribute should be used + // to map the texture on the underlying geometry (e.g. tex_coord_index 0 would + // use the first texture coordinate attribute). + void SetTextureMap(std::unique_ptr texture, + TextureMap::Type texture_map_type, int tex_coord_index); + void SetTextureMap(std::unique_ptr texture, + TextureMap::Type texture_map_type, + TextureMap::WrappingMode wrapping_mode, + int tex_coord_index); + + // Sets a new texture map using a |texture| that is already owned by this + // material (that is by one of its texture maps or by the unerlying + // |texture_library_|). |transform| is the texture map's transform if set. + // |min_filter| and |mag_filter| are the texture filter types. Returns error + // status if provided |texture| is not owned by the material. + Status SetTextureMap(Texture *texture, TextureMap::Type texture_map_type, + int tex_coord_index); + Status SetTextureMap(Texture *texture, TextureMap::Type texture_map_type, + TextureMap::WrappingMode wrapping_mode, + int tex_coord_index); + Status SetTextureMap(Texture *texture, TextureMap::Type texture_map_type, + TextureMap::WrappingMode wrapping_mode, + TextureMap::FilterType min_filter, + TextureMap::FilterType mag_filter, int tex_coord_index); + Status SetTextureMap(Texture *texture, TextureMap::Type texture_map_type, + TextureMap::WrappingMode wrapping_mode, + TextureMap::FilterType min_filter, + TextureMap::FilterType mag_filter, + const TextureTransform &transform, int tex_coord_index); + + // Removes a texture map from the material based on its index or texture type. + // The material releases the ownership of the texture map and returns it as + // a unique_ptr to allow the caller to use the texture map for other purposes. + std::unique_ptr RemoveTextureMapByIndex(int index); + std::unique_ptr RemoveTextureMapByType( + TextureMap::Type texture_type); + + private: + void SetTextureMap(TextureMap &&texture_map); + void SetTextureMap(std::unique_ptr texture_map); + Status SetTextureMap(std::unique_ptr texture_map, + Texture *texture, TextureMap::Type texture_map_type, + TextureMap::WrappingMode wrapping_mode, + TextureMap::FilterType min_filter, + TextureMap::FilterType mag_filter, int tex_coord_index); + + // Returns true if the |texture| is owned by the material. + bool IsTextureOwned(const Texture &texture); + + private: + std::string name_; + Vector4f color_factor_; + float metallic_factor_; + float roughness_factor_; + Vector3f emissive_factor_; + bool double_sided_; + TransparencyMode transparency_mode_; + float alpha_cutoff_; + float normal_texture_scale_; + + // Properties of glTF material extension KHR_materials_unlit. + bool unlit_; + + // Properties of glTF material extension KHR_materials_sheen. + bool has_sheen_; + Vector3f sheen_color_factor_; + float sheen_roughness_factor_; + + // Properties of glTF material extension KHR_materials_transmission. + bool has_transmission_; + float transmission_factor_; + + // Properties of glTF material extension KHR_materials_clearcoat. + bool has_clearcoat_; + float clearcoat_factor_; + float clearcoat_roughness_factor_; + + // Properties of glTF material extension KHR_materials_volume. + bool has_volume_; + float thickness_factor_; + float attenuation_distance_; + Vector3f attenuation_color_; + + // Properties of glTF material extension KHR_materials_ior. + bool has_ior_; + float ior_; + + // Properties of glTF material extension KHR_materials_specular. + bool has_specular_; + float specular_factor_; + Vector3f specular_color_factor_; + + // Texture maps. + std::vector> texture_maps_; + + // Map between a texture type to texture index in |texture_maps_|. Allows fast + // retrieval of texture maps based on their type. + std::unordered_map texture_map_type_to_index_map_; + + // Optional pointer to a library that holds ownership of textures used for + // this material. If set to nullptr, the texture ownership will be assigned + // to the newly created TextureMaps directly. + TextureLibrary *texture_library_; +}; + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED +#endif // DRACO_MATERIAL_MATERIAL_H_ diff --git a/contrib/draco/src/draco/material/material_library.cc b/contrib/draco/src/draco/material/material_library.cc new file mode 100644 index 000000000..f2165295f --- /dev/null +++ b/contrib/draco/src/draco/material/material_library.cc @@ -0,0 +1,125 @@ +// Copyright 2018 The Draco Authors. +// +// 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 "draco/material/material_library.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED +namespace draco { + +void MaterialLibrary::Copy(const MaterialLibrary &src) { + Clear(); + Append(src); +} + +void MaterialLibrary::Append(const MaterialLibrary &src) { + const size_t old_num_materials = materials_.size(); + materials_.resize(old_num_materials + src.materials_.size()); + for (int i = 0; i < src.materials_.size(); ++i) { + materials_[old_num_materials + i] = + std::unique_ptr(new Material(&texture_library_)); + materials_[old_num_materials + i]->Copy(*src.materials_[i]); + } + + const size_t old_num_textures = texture_library_.NumTextures(); + texture_library_.Append(src.texture_library_); + for (int i = 0; i < src.materials_variants_names_.size(); i++) { + materials_variants_names_.push_back(src.materials_variants_names_[i]); + } + + // Remap all texture maps to the textures in the new texture library. + + // First gather mapping between texture maps and textures in the old material + // library. + const auto texture_map_to_index = + ComputeTextureMapToTextureIndexMapping(src.texture_library_); + + // Remap all texture maps to textures stored in the new texture library. + for (auto it = texture_map_to_index.begin(); it != texture_map_to_index.end(); + ++it) { + TextureMap *const texture_map = it->first; + const int texture_index = old_num_textures + it->second; + texture_map->SetTexture(texture_library_.GetTexture(texture_index)); + } +} + +std::unique_ptr MaterialLibrary::RemoveMaterial(int index) { + std::unique_ptr ret = std::move(materials_[index]); + materials_.erase(materials_.begin() + index); + return ret; +} + +void MaterialLibrary::RemoveUnusedTextures() { + const auto texture_map_to_index = + ComputeTextureMapToTextureIndexMapping(texture_library_); + + // Mark which textures are used. + std::vector is_texture_used(texture_library_.NumTextures(), false); + for (auto it = texture_map_to_index.begin(); it != texture_map_to_index.end(); + ++it) { + is_texture_used[it->second] = true; + } + + // Remove all textures that are not used (from backwards to avoid updating + // entries in the |is_texture_used| vector). + for (int i = texture_library_.NumTextures() - 1; i >= 0; --i) { + if (!is_texture_used[i]) { + texture_library_.RemoveTexture(i); + } + } +} + +std::map +MaterialLibrary::ComputeTextureMapToTextureIndexMapping( + const TextureLibrary &library) const { + std::map map_to_index; + for (int mi = 0; mi < materials_.size(); ++mi) { + for (int ti = 0; ti < materials_[mi]->NumTextureMaps(); ++ti) { + TextureMap *const texture_map = materials_[mi]->GetTextureMapByIndex(ti); + for (int tli = 0; tli < library.NumTextures(); ++tli) { + if (library.GetTexture(tli) != texture_map->texture()) { + continue; + } + map_to_index[texture_map] = tli; + break; + } + } + } + return map_to_index; +} + +void MaterialLibrary::Clear() { + materials_.clear(); + texture_library_.Clear(); + materials_variants_names_.clear(); +} + +Material *MaterialLibrary::MutableMaterial(int index) { + if (index < 0) { + return nullptr; + } + if (materials_.size() <= index) { + const int old_size = materials_.size(); + materials_.resize(index + 1); + // Ensure all newly created materials are valid. + for (int i = old_size; i < index + 1; ++i) { + materials_[i] = + std::unique_ptr(new Material(&texture_library_)); + } + } + return materials_[index].get(); +} + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED diff --git a/contrib/draco/src/draco/material/material_library.h b/contrib/draco/src/draco/material/material_library.h new file mode 100644 index 000000000..574d86b23 --- /dev/null +++ b/contrib/draco/src/draco/material/material_library.h @@ -0,0 +1,104 @@ +// Copyright 2018 The Draco Authors. +// +// 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 DRACO_MATERIAL_MATERIAL_LIBRARY_H_ +#define DRACO_MATERIAL_MATERIAL_LIBRARY_H_ + +#include "draco/draco_features.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED +#include +#include +#include + +#include "draco/material/material.h" +#include "draco/texture/texture_library.h" + +namespace draco { + +// MaterialLibrary holds an array of materials that are applied to a single +// model. +class MaterialLibrary { + public: + MaterialLibrary() = default; + + // Copies the |src| into this instance. + void Copy(const MaterialLibrary &src); + + // Appends materials from the |src| library to this library. All materials + // and textures are copied over. + void Append(const MaterialLibrary &src); + + // Deletes all materials from the material library. + void Clear(); + + // The number of materials stored in the library. All materials are stored + // with indices <0, num_materials() - 1>. + size_t NumMaterials() const { return materials_.size(); } + + // Returns a material with a given index or nullptr if the index is not valid. + const Material *GetMaterial(int index) const { + if (index < 0 || index >= materials_.size()) { + return nullptr; + } + return materials_[index].get(); + } + + // Returns a mutable pointer to a given material. If the material with the + // specified |index| does not exist, it is automatically created. + Material *MutableMaterial(int index); + + // Removes a material with a given index and returns it. Caller can ignore the + // returned value, in which case the material will be automatically deleted. + // Index of all subsequent materials will be decremented by one. + std::unique_ptr RemoveMaterial(int index); + + const TextureLibrary &GetTextureLibrary() const { return texture_library_; } + TextureLibrary &MutableTextureLibrary() { return texture_library_; } + + // Removes all textures that are not referenced by a TextureMap from the + // texture library. + void RemoveUnusedTextures(); + + // Returns a map between each TextureMap object and associated texture index + // in the texture |library|. + std::map ComputeTextureMapToTextureIndexMapping( + const TextureLibrary &library) const; + + // Creates a named materials variant and returns its index. + int AddMaterialsVariant(const std::string &name) { + materials_variants_names_.push_back(name); + return materials_variants_names_.size() - 1; + } + + // Returns the number of materials variants. + int NumMaterialsVariants() const { return materials_variants_names_.size(); } + + // Returns the name of a materials variant. + const std::string &GetMaterialsVariantName(int index) const { + return materials_variants_names_[index]; + } + + private: + std::vector> materials_; + std::vector materials_variants_names_; + + // Container for storing all textures used by materials of this library. + TextureLibrary texture_library_; +}; + +} // namespace draco + +#endif // DRACO_MATERIAL_MATERIAL_LIBRARY_H_ +#endif // DRACO_TRANSCODER_SUPPORTED diff --git a/contrib/draco/src/draco/material/material_library_test.cc b/contrib/draco/src/draco/material/material_library_test.cc new file mode 100644 index 000000000..a110fa4db --- /dev/null +++ b/contrib/draco/src/draco/material/material_library_test.cc @@ -0,0 +1,155 @@ +// Copyright 2018 The Draco Authors. +// +// 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 "draco/material/material_library.h" + +#include "draco/core/draco_test_base.h" +#include "draco/core/draco_test_utils.h" + +namespace { + +#ifdef DRACO_TRANSCODER_SUPPORTED +TEST(MaterialLibraryTest, TestMaterials) { + // Test verifies that we can modify materials in a library. + draco::MaterialLibrary library; + ASSERT_EQ(library.NumMaterials(), 0); + + // Add a new material to the library. + const draco::Material *const new_mat = library.MutableMaterial(0); + ASSERT_NE(new_mat, nullptr); + ASSERT_EQ(library.NumMaterials(), 1); + + const draco::Material *const new_mat2 = library.MutableMaterial(2); + ASSERT_NE(new_mat2, nullptr); + ASSERT_EQ(library.NumMaterials(), 3); + ASSERT_EQ(library.GetMaterial(2), new_mat2); + + // Ensure that even though we call mutable_material multiple times, it does + // not increase the number of materials associated to the library. + for (int i = 0; i < library.NumMaterials(); ++i) { + ASSERT_NE(library.MutableMaterial(i), nullptr); + } + ASSERT_EQ(library.NumMaterials(), 3); + + // Check that material variants can be added and cleared. + library.AddMaterialsVariant("Milk Truck"); + library.AddMaterialsVariant("Ice Cream Truck"); + ASSERT_EQ(library.NumMaterialsVariants(), 2); + ASSERT_EQ(library.GetMaterialsVariantName(0), "Milk Truck"); + ASSERT_EQ(library.GetMaterialsVariantName(1), "Ice Cream Truck"); + + library.Clear(); + ASSERT_EQ(library.NumMaterials(), 0); + ASSERT_EQ(library.NumMaterialsVariants(), 0); +} + +TEST(MaterialLibraryTest, TestMaterialsCopy) { + // Test verifies that we can copy a material library. + draco::MaterialLibrary library; + library.MutableMaterial(0)->SetMetallicFactor(2.4f); + library.MutableMaterial(3)->SetRoughnessFactor(1.2f); + library.AddMaterialsVariant("Milk Truck"); + library.AddMaterialsVariant("Ice Cream Truck"); + + draco::MaterialLibrary new_library; + new_library.Copy(library); + ASSERT_EQ(library.NumMaterials(), new_library.NumMaterials()); + ASSERT_EQ(library.GetMaterial(0)->GetMetallicFactor(), + new_library.GetMaterial(0)->GetMetallicFactor()); + ASSERT_EQ(library.GetMaterial(3)->GetRoughnessFactor(), + new_library.GetMaterial(3)->GetRoughnessFactor()); + ASSERT_EQ(new_library.NumMaterialsVariants(), 2); + ASSERT_EQ(new_library.GetMaterialsVariantName(0), "Milk Truck"); + ASSERT_EQ(new_library.GetMaterialsVariantName(1), "Ice Cream Truck"); +} + +TEST(MaterialLibraryTest, TestTextureLibrary) { + // Tests that texture library is properly updated when we add new textures + // to a material belonging to the material library. + std::unique_ptr texture_0(new draco::Texture()); + std::unique_ptr texture_1(new draco::Texture()); + + draco::MaterialLibrary library; + library.MutableMaterial(0)->SetTextureMap(std::move(texture_0), + draco::TextureMap::COLOR, 0); + ASSERT_EQ(library.GetTextureLibrary().NumTextures(), 1); + library.MutableMaterial(3)->SetTextureMap(std::move(texture_1), + draco::TextureMap::COLOR, 0); + ASSERT_EQ(library.GetTextureLibrary().NumTextures(), 2); +} + +TEST(MaterialLibraryTest, RemoveUnusedTextures) { + // Test verifies that we can remove unusued textures from the material + // library. + draco::MaterialLibrary library; + + // Create dummy textures. + std::unique_ptr texture_0(new draco::Texture()); + std::unique_ptr texture_1(new draco::Texture()); + std::unique_ptr texture_2(new draco::Texture()); + + // Add them to the materials of the library. + library.MutableMaterial(0)->SetTextureMap(std::move(texture_0), + draco::TextureMap::COLOR, 0); + library.MutableMaterial(0)->SetTextureMap( + std::move(texture_1), draco::TextureMap::METALLIC_ROUGHNESS, 0); + library.MutableMaterial(1)->SetTextureMap(std::move(texture_2), + draco::TextureMap::COLOR, 0); + + ASSERT_EQ(library.GetTextureLibrary().NumTextures(), 3); + + library.RemoveUnusedTextures(); + ASSERT_EQ(library.GetTextureLibrary().NumTextures(), 3); + + // Remove texture map from a material. + library.MutableMaterial(0)->RemoveTextureMapByType( + draco::TextureMap::METALLIC_ROUGHNESS); + library.RemoveUnusedTextures(); + ASSERT_EQ(library.GetTextureLibrary().NumTextures(), 2); + + library.MutableMaterial(1)->RemoveTextureMapByType(draco::TextureMap::COLOR); + library.RemoveUnusedTextures(); + ASSERT_EQ(library.GetTextureLibrary().NumTextures(), 1); + + library.MutableMaterial(0)->RemoveTextureMapByType(draco::TextureMap::COLOR); + library.RemoveUnusedTextures(); + ASSERT_EQ(library.GetTextureLibrary().NumTextures(), 0); +} + +TEST(MaterialLibraryTest, RemoveMaterial) { + // Tests that we can safely remove materials from the material library. + draco::MaterialLibrary library; + library.MutableMaterial(0)->SetMetallicFactor(0.f); + library.MutableMaterial(1)->SetMetallicFactor(1.f); + library.MutableMaterial(2)->SetMetallicFactor(2.f); + library.MutableMaterial(3)->SetMetallicFactor(3.f); + + ASSERT_EQ(library.NumMaterials(), 4); + + ASSERT_EQ(library.RemoveMaterial(0)->GetMetallicFactor(), 0.f); + ASSERT_EQ(library.NumMaterials(), 3); + + ASSERT_EQ(library.RemoveMaterial(1)->GetMetallicFactor(), 2.f); + ASSERT_EQ(library.NumMaterials(), 2); + + ASSERT_EQ(library.RemoveMaterial(1)->GetMetallicFactor(), 3.f); + ASSERT_EQ(library.NumMaterials(), 1); + + ASSERT_EQ(library.RemoveMaterial(0)->GetMetallicFactor(), 1.f); + ASSERT_EQ(library.NumMaterials(), 0); +} + +#endif // DRACO_TRANSCODER_SUPPORTED + +} // namespace diff --git a/contrib/draco/src/draco/material/material_test.cc b/contrib/draco/src/draco/material/material_test.cc new file mode 100644 index 000000000..8c999a532 --- /dev/null +++ b/contrib/draco/src/draco/material/material_test.cc @@ -0,0 +1,320 @@ +// Copyright 2018 The Draco Authors. +// +// 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 "draco/material/material.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED +#include "draco/core/draco_test_utils.h" +#include "draco/io/texture_io.h" + +namespace { + +TEST(MaterialTest, TestMaterialAccess) { + // Tests that we can set and get material properties. + draco::Material material; + + material.SetName("Superalloy"); + ASSERT_EQ(material.GetName(), "Superalloy"); + material.SetColorFactor(draco::Vector4f(1.f, 0.2f, 0.1f, 0.9f)); + ASSERT_EQ(material.GetColorFactor(), draco::Vector4f(1.f, 0.2f, 0.1f, 0.9f)); + material.SetMetallicFactor(0.3f); + ASSERT_EQ(material.GetMetallicFactor(), 0.3f); + material.SetRoughnessFactor(0.2f); + ASSERT_EQ(material.GetRoughnessFactor(), 0.2f); + material.SetEmissiveFactor(draco::Vector3f(0.2f, 0.f, 0.1f)); + ASSERT_EQ(material.GetEmissiveFactor(), draco::Vector3f(0.2f, 0.f, 0.1f)); + + // Set and check the properties of material extensions. + material.SetUnlit(true); + ASSERT_TRUE(material.GetUnlit()); + material.SetHasSheen(true); + ASSERT_TRUE(material.HasSheen()); + material.SetSheenColorFactor(draco::Vector3f(0.4f, 0.2f, 0.8f)); + ASSERT_EQ(material.GetSheenColorFactor(), draco::Vector3f(0.4f, 0.2f, 0.8f)); + material.SetSheenRoughnessFactor(0.428f); + ASSERT_EQ(material.GetSheenRoughnessFactor(), 0.428f); + material.SetHasTransmission(true); + ASSERT_TRUE(material.HasTransmission()); + material.SetTransmissionFactor(0.5f); + ASSERT_EQ(material.GetTransmissionFactor(), 0.5f); + material.SetHasClearcoat(true); + ASSERT_TRUE(material.HasClearcoat()); + material.SetClearcoatFactor(0.6f); + ASSERT_EQ(material.GetClearcoatFactor(), 0.6f); + material.SetClearcoatRoughnessFactor(0.7f); + ASSERT_EQ(material.GetClearcoatRoughnessFactor(), 0.7f); + material.SetHasVolume(true); + ASSERT_TRUE(material.HasVolume()); + material.SetThicknessFactor(0.8f); + ASSERT_EQ(material.GetThicknessFactor(), 0.8f); + material.SetAttenuationDistance(0.9f); + ASSERT_EQ(material.GetAttenuationDistance(), 0.9f); + material.SetAttenuationColor(draco::Vector3f(0.2f, 0.5f, 0.8f)); + ASSERT_EQ(material.GetAttenuationColor(), draco::Vector3f(0.2f, 0.5f, 0.8f)); + material.SetHasIor(true); + ASSERT_TRUE(material.HasIor()); + material.SetIor(1.1f); + ASSERT_EQ(material.GetIor(), 1.1f); + material.SetHasSpecular(true); + ASSERT_TRUE(material.HasSpecular()); + material.SetSpecularFactor(0.01f); + ASSERT_EQ(material.GetSpecularFactor(), 0.01f); + material.SetSpecularColorFactor(draco::Vector3f(0.4f, 1.f, 1.f)); + ASSERT_EQ(material.GetSpecularColorFactor(), draco::Vector3f(0.4f, 1.f, 1.f)); + + ASSERT_EQ(material.GetTextureMapByType(draco::TextureMap::COLOR), nullptr); + ASSERT_EQ(material.NumTextureMaps(), 0); + + std::unique_ptr texture = + draco::ReadTextureFromFile(draco::GetTestFileFullPath("test.png")) + .value(); + ASSERT_NE(texture, nullptr); + + material.SetTextureMap(std::move(texture), draco::TextureMap::COLOR, 0); + + ASSERT_NE(material.GetTextureMapByType(draco::TextureMap::COLOR), nullptr); + ASSERT_EQ(material.NumTextureMaps(), 1); + ASSERT_EQ(material.GetTextureMapByIndex(0), + material.GetTextureMapByType(draco::TextureMap::COLOR)); + + std::unique_ptr texture2 = + draco::ReadTextureFromFile(draco::GetTestFileFullPath("test.png")) + .value(); + ASSERT_NE(texture2, nullptr); + material.SetTextureMap(std::move(texture2), draco::TextureMap::EMISSIVE, 1); + + ASSERT_NE(material.GetTextureMapByType(draco::TextureMap::EMISSIVE), nullptr); + ASSERT_EQ(material.GetTextureMapByType(draco::TextureMap::EMISSIVE) + ->tex_coord_index(), + 1); + ASSERT_EQ(material.NumTextureMaps(), 2); + + // Try to add the emissive texture one more time. This should replace the + // previous instance of the emissive texture on the material. + std::unique_ptr texture3 = + draco::ReadTextureFromFile(draco::GetTestFileFullPath("test.png")) + .value(); + ASSERT_NE(texture3, nullptr); + material.SetTextureMap(std::move(texture2), draco::TextureMap::EMISSIVE, 2); + ASSERT_EQ(material.NumTextureMaps(), 2); + ASSERT_EQ(material.GetTextureMapByType(draco::TextureMap::EMISSIVE) + ->tex_coord_index(), + 2); + + std::unique_ptr texture_map4(new draco::TextureMap()); + std::unique_ptr texture4 = + draco::ReadTextureFromFile(draco::GetTestFileFullPath("test.png")) + .value(); + material.SetTextureMap(std::move(texture4), draco::TextureMap::ROUGHNESS, 0); + ASSERT_EQ(material.NumTextureMaps(), 3); + ASSERT_NE(material.GetTextureMapByType(draco::TextureMap::ROUGHNESS), + nullptr); + + material.SetTransparencyMode(draco::Material::TRANSPARENCY_BLEND); + ASSERT_EQ(material.GetTransparencyMode(), + draco::Material::TRANSPARENCY_BLEND); + material.SetAlphaCutoff(0.2f); + ASSERT_EQ(material.GetAlphaCutoff(), 0.2f); + material.SetNormalTextureScale(0.75f); + ASSERT_EQ(material.GetNormalTextureScale(), 0.75f); + + material.ClearTextureMaps(); + ASSERT_EQ(material.NumTextureMaps(), 0); + ASSERT_EQ(material.GetTextureMapByType(draco::TextureMap::COLOR), nullptr); + + // Metallic factor should be unchanged. + ASSERT_EQ(material.GetMetallicFactor(), 0.3f); + + material.Clear(); + // Metallic factor should be reset to default. + ASSERT_NE(material.GetMetallicFactor(), 0.3f); + + ASSERT_EQ(material.GetDoubleSided(), false); + material.SetDoubleSided(true); + ASSERT_EQ(material.GetDoubleSided(), true); +} + +TEST(MaterialTest, TestMaterialCopy) { + draco::Material material; + material.SetName("Antimatter"); + material.SetColorFactor(draco::Vector4f(0.3f, 0.2f, 0.4f, 0.9f)); + material.SetMetallicFactor(0.2f); + material.SetRoughnessFactor(0.4f); + material.SetEmissiveFactor(draco::Vector3f(0.3f, 0.1f, 0.2f)); + material.SetTransparencyMode(draco::Material::TRANSPARENCY_MASK); + material.SetAlphaCutoff(0.25f); + material.SetDoubleSided(true); + material.SetNormalTextureScale(0.75f); + + // Set the properties of material extensions. + material.SetUnlit(true); + material.SetHasSheen(true); + material.SetSheenColorFactor(draco::Vector3f(0.4f, 0.2f, 0.8f)); + material.SetSheenRoughnessFactor(0.428f); + material.SetHasTransmission(true); + material.SetTransmissionFactor(0.5f); + material.SetHasClearcoat(true); + material.SetClearcoatFactor(0.6f); + material.SetClearcoatRoughnessFactor(0.7f); + material.SetHasVolume(true); + material.SetThicknessFactor(0.8f); + material.SetAttenuationDistance(0.9f); + material.SetAttenuationColor(draco::Vector3f(0.2f, 0.5f, 0.8f)); + material.SetHasIor(true); + material.SetIor(1.1f); + material.SetHasSpecular(true); + material.SetSpecularFactor(0.01f); + material.SetSpecularColorFactor(draco::Vector3f(0.4f, 1.f, 1.f)); + + std::unique_ptr texture = + draco::ReadTextureFromFile(draco::GetTestFileFullPath("test.png")) + .value(); + ASSERT_NE(texture, nullptr); + material.SetTextureMap(std::move(texture), draco::TextureMap::EMISSIVE, 2); + + draco::Material new_material; + new_material.Copy(material); + + ASSERT_EQ(material.GetName(), new_material.GetName()); + ASSERT_EQ(material.GetColorFactor(), new_material.GetColorFactor()); + ASSERT_EQ(material.GetMetallicFactor(), new_material.GetMetallicFactor()); + ASSERT_EQ(material.GetRoughnessFactor(), new_material.GetRoughnessFactor()); + ASSERT_EQ(material.GetEmissiveFactor(), new_material.GetEmissiveFactor()); + ASSERT_EQ(material.GetTransparencyMode(), new_material.GetTransparencyMode()); + ASSERT_EQ(material.GetAlphaCutoff(), new_material.GetAlphaCutoff()); + ASSERT_EQ(material.GetDoubleSided(), new_material.GetDoubleSided()); + ASSERT_EQ(material.GetNormalTextureScale(), + new_material.GetNormalTextureScale()); + + // Check that the properties of material extensions have been copied. + ASSERT_EQ(material.GetUnlit(), new_material.GetUnlit()); + ASSERT_EQ(material.HasSheen(), new_material.HasSheen()); + ASSERT_EQ(material.GetSheenColorFactor(), new_material.GetSheenColorFactor()); + ASSERT_EQ(material.GetSheenRoughnessFactor(), + new_material.GetSheenRoughnessFactor()); + ASSERT_TRUE(material.HasTransmission()); + ASSERT_EQ(material.GetTransmissionFactor(), + new_material.GetTransmissionFactor()); + ASSERT_TRUE(material.HasClearcoat()); + ASSERT_EQ(material.GetClearcoatFactor(), new_material.GetClearcoatFactor()); + ASSERT_EQ(material.GetClearcoatRoughnessFactor(), + new_material.GetClearcoatRoughnessFactor()); + ASSERT_TRUE(material.HasVolume()); + ASSERT_EQ(material.GetThicknessFactor(), new_material.GetThicknessFactor()); + ASSERT_EQ(material.GetAttenuationDistance(), + new_material.GetAttenuationDistance()); + ASSERT_EQ(material.GetAttenuationColor(), new_material.GetAttenuationColor()); + ASSERT_TRUE(material.HasIor()); + ASSERT_EQ(material.GetIor(), new_material.GetIor()); + ASSERT_TRUE(material.HasSpecular()); + ASSERT_EQ(material.GetSpecularFactor(), new_material.GetSpecularFactor()); + ASSERT_EQ(material.GetSpecularColorFactor(), + new_material.GetSpecularColorFactor()); + + for (int i = 0; i < draco::TextureMap::TEXTURE_TYPES_COUNT; ++i) { + const draco::TextureMap::Type texture_map_type = + static_cast(i); + if (material.GetTextureMapByType(texture_map_type) == nullptr) { + ASSERT_EQ(new_material.GetTextureMapByType(texture_map_type), nullptr); + continue; + } + if (material.GetTextureMapByType(texture_map_type)->texture() == nullptr) { + ASSERT_EQ(new_material.GetTextureMapByType(texture_map_type)->texture(), + nullptr); + } else { + ASSERT_NE(new_material.GetTextureMapByType(texture_map_type)->texture(), + nullptr); + ASSERT_EQ( + material.GetTextureMapByType(texture_map_type)->tex_coord_index(), + new_material.GetTextureMapByType(texture_map_type) + ->tex_coord_index()); + } + } + + ASSERT_EQ(material.NumTextureMaps(), new_material.NumTextureMaps()); + for (int i = 0; i < material.NumTextureMaps(); ++i) { + const draco::TextureMap *const tm0 = material.GetTextureMapByIndex(i); + const draco::TextureMap *const tm1 = new_material.GetTextureMapByIndex(i); + ASSERT_NE(tm0, nullptr); + ASSERT_NE(tm1, nullptr); + ASSERT_EQ(tm0->type(), tm1->type()); + } +} + +TEST(MaterialTest, RemoveTextureMap) { + // Tests that we can remove existing texture maps from a material. + draco::Material material; + + // Add some dummy textures to the material. + std::unique_ptr texture = + draco::ReadTextureFromFile(draco::GetTestFileFullPath("test.png")) + .value(); + ASSERT_NE(texture, nullptr); + material.SetTextureMap(std::move(texture), draco::TextureMap::COLOR, 0); + + std::unique_ptr texture_2 = + draco::ReadTextureFromFile(draco::GetTestFileFullPath("test.png")) + .value(); + + material.SetTextureMap(std::move(texture), draco::TextureMap::EMISSIVE, 0); + + ASSERT_EQ(material.NumTextureMaps(), 2); + + // Try to delete the color texture. + std::unique_ptr removed_texture = + material.RemoveTextureMapByType(draco::TextureMap::COLOR); + ASSERT_NE(removed_texture, nullptr); + ASSERT_EQ(removed_texture->type(), draco::TextureMap::COLOR); + ASSERT_EQ(material.NumTextureMaps(), 1); + ASSERT_NE(material.GetTextureMapByType(draco::TextureMap::EMISSIVE), nullptr); + ASSERT_EQ(material.GetTextureMapByIndex(0)->type(), + draco::TextureMap::EMISSIVE); + ASSERT_EQ(material.GetTextureMapByType(draco::TextureMap::COLOR), nullptr); + + removed_texture = material.RemoveTextureMapByIndex(0); + ASSERT_NE(removed_texture, nullptr); + ASSERT_EQ(removed_texture->type(), draco::TextureMap::EMISSIVE); + ASSERT_EQ(material.NumTextureMaps(), 0); + ASSERT_EQ(material.GetTextureMapByType(draco::TextureMap::EMISSIVE), nullptr); +} + +TEST(MaterialTest, SharedTexture) { + // Tests adding shared textures. + draco::Material material; + + // Add some dummy textures to the material. + std::unique_ptr texture = + draco::ReadTextureFromFile(draco::GetTestFileFullPath("test.png")) + .value(); + ASSERT_NE(texture, nullptr); + draco::Texture *texture_raw = texture.get(); + material.SetTextureMap(std::move(texture), draco::TextureMap::COLOR, 0); + + DRACO_ASSERT_OK( + material.SetTextureMap(texture_raw, draco::TextureMap::EMISSIVE, 0)); + + ASSERT_EQ(material.NumTextureMaps(), 2); + + // Read a new texture. + texture = draco::ReadTextureFromFile(draco::GetTestFileFullPath("test.png")) + .value(); + // Texture is not owned by the material so we expect a failure. + ASSERT_FALSE( + material + .SetTextureMap(texture.get(), draco::TextureMap::AMBIENT_OCCLUSION, 0) + .ok()); +} + +} // namespace +#endif // DRACO_TRANSCODER_SUPPORTED diff --git a/contrib/draco/src/draco/material/material_utils.cc b/contrib/draco/src/draco/material/material_utils.cc new file mode 100644 index 000000000..7f9fcb621 --- /dev/null +++ b/contrib/draco/src/draco/material/material_utils.cc @@ -0,0 +1,14 @@ +// Copyright 2020 The Draco Authors. +// +// 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. +// diff --git a/contrib/draco/src/draco/material/material_utils.h b/contrib/draco/src/draco/material/material_utils.h new file mode 100644 index 000000000..7f9fcb621 --- /dev/null +++ b/contrib/draco/src/draco/material/material_utils.h @@ -0,0 +1,14 @@ +// Copyright 2020 The Draco Authors. +// +// 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. +// diff --git a/contrib/draco/src/draco/material/material_utils_test.cc b/contrib/draco/src/draco/material/material_utils_test.cc new file mode 100644 index 000000000..82a1227a2 --- /dev/null +++ b/contrib/draco/src/draco/material/material_utils_test.cc @@ -0,0 +1,24 @@ +// Copyright 2020 The Draco Authors. +// +// 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 "draco/material/material_utils.h" + +#include +#include +#include + +#include "draco/core/draco_test_utils.h" +#include "draco/io/texture_io.h" + +namespace {} // namespace diff --git a/contrib/draco/src/draco/mesh/corner_table.cc b/contrib/draco/src/draco/mesh/corner_table.cc index 3f92f651a..6494c1572 100644 --- a/contrib/draco/src/draco/mesh/corner_table.cc +++ b/contrib/draco/src/draco/mesh/corner_table.cc @@ -15,6 +15,7 @@ #include "draco/mesh/corner_table.h" #include +#include #include "draco/attributes/geometry_indices.h" #include "draco/mesh/corner_table_iterators.h" diff --git a/contrib/draco/src/draco/mesh/corner_table.h b/contrib/draco/src/draco/mesh/corner_table.h index 3aa720fde..3088931c1 100644 --- a/contrib/draco/src/draco/mesh/corner_table.h +++ b/contrib/draco/src/draco/mesh/corner_table.h @@ -21,6 +21,7 @@ #include "draco/attributes/geometry_indices.h" #include "draco/core/draco_index_type_vector.h" #include "draco/core/macros.h" +#include "draco/draco_features.h" #include "draco/mesh/valence_cache.h" namespace draco { diff --git a/contrib/draco/src/draco/mesh/corner_table_iterators.h b/contrib/draco/src/draco/mesh/corner_table_iterators.h index 7122aa1be..72c70ac32 100644 --- a/contrib/draco/src/draco/mesh/corner_table_iterators.h +++ b/contrib/draco/src/draco/mesh/corner_table_iterators.h @@ -15,15 +15,23 @@ #ifndef DRACO_MESH_CORNER_TABLE_ITERATORS_H_ #define DRACO_MESH_CORNER_TABLE_ITERATORS_H_ +#include + #include "draco/mesh/corner_table.h" namespace draco { // Class for iterating over vertices in a 1-ring around the specified vertex. template -class VertexRingIterator - : public std::iterator { +class VertexRingIterator { public: + // Iterator traits expected by std libraries. + using iterator_category = std::forward_iterator_tag; + using value_type = VertexIndex; + using difference_type = std::ptrdiff_t; + using pointer = VertexIndex *; + using reference = VertexIndex &; + // std::iterator interface requires a default constructor. VertexRingIterator() : corner_table_(nullptr), @@ -111,9 +119,15 @@ class VertexRingIterator // Class for iterating over faces adjacent to the specified input face. template -class FaceAdjacencyIterator - : public std::iterator { +class FaceAdjacencyIterator { public: + // Iterator traits expected by std libraries. + using iterator_category = std::forward_iterator_tag; + using value_type = FaceIndex; + using difference_type = std::ptrdiff_t; + using pointer = FaceIndex *; + using reference = FaceIndex &; + // std::iterator interface requires a default constructor. FaceAdjacencyIterator() : corner_table_(nullptr), @@ -193,9 +207,15 @@ class FaceAdjacencyIterator // Class for iterating over corners attached to a specified vertex. template -class VertexCornersIterator - : public std::iterator { +class VertexCornersIterator { public: + // Iterator traits expected by std libraries. + using iterator_category = std::forward_iterator_tag; + using value_type = CornerIndex; + using difference_type = std::ptrdiff_t; + using pointer = CornerIndex *; + using reference = CornerIndex &; + // std::iterator interface requires a default constructor. VertexCornersIterator() : corner_table_(nullptr), diff --git a/contrib/draco/src/draco/mesh/corner_table_test.cc b/contrib/draco/src/draco/mesh/corner_table_test.cc new file mode 100644 index 000000000..f88d3ec96 --- /dev/null +++ b/contrib/draco/src/draco/mesh/corner_table_test.cc @@ -0,0 +1,126 @@ +#include "draco/mesh/corner_table.h" + +#include + +#include "draco/core/draco_test_utils.h" +#include "draco/io/obj_decoder.h" +#include "draco/mesh/mesh_connected_components.h" +#include "draco/mesh/mesh_misc_functions.h" + +namespace draco { + +class CornerTableTest : public ::testing::Test { + protected: + std::unique_ptr DecodeObj(const std::string &file_name) const { + const std::string path = GetTestFileFullPath(file_name); + ObjDecoder decoder; + std::unique_ptr mesh(new Mesh()); + if (!decoder.DecodeFromFile(path, mesh.get()).ok()) { + return nullptr; + } + return mesh; + } + + void TestEncodingCube() { + // Build a CornerTable looking at the mesh and verify that the caching of + // valences are reasonably correct and within range of expectations. This + // test is built specifically for working with 'cubes' and has expectations + // about the degree of each corner. + const std::string file_name = "cube_att.obj"; + std::unique_ptr in_mesh = DecodeObj(file_name); + ASSERT_NE(in_mesh, nullptr) << "Failed to load test model " << file_name; + draco::Mesh *mesh = nullptr; + mesh = in_mesh.get(); + + std::unique_ptr utable = + draco::CreateCornerTableFromPositionAttribute(mesh); + draco::CornerTable *table = utable.get(); + + table->GetValenceCache().CacheValences(); + table->GetValenceCache().CacheValencesInaccurate(); + + for (VertexIndex index = static_cast(0); + index < static_cast(table->num_vertices()); index++) { + const auto valence = table->Valence(index); + const auto valence2 = table->GetValenceCache().ValenceFromCache(index); + const auto valence3 = + table->GetValenceCache().ValenceFromCacheInaccurate(index); + ASSERT_EQ(valence, valence2); + ASSERT_GE(valence, valence3); // may be clipped. + + // No more than 6 triangles can touch a cube corner. + ASSERT_LE(valence, 6); + ASSERT_LE(valence2, 6); + ASSERT_LE(valence3, 6); + + // No less than 3 triangles can touch a cube corner. + ASSERT_GE(valence, 3); + ASSERT_GE(valence2, 3); + ASSERT_GE(valence3, 3); + } + + for (CornerIndex index = static_cast(0); + index < static_cast(table->num_corners()); index++) { + const auto valence = table->Valence(index); + const auto valence2 = table->GetValenceCache().ValenceFromCache(index); + const auto valence3 = + table->GetValenceCache().ValenceFromCacheInaccurate(index); + ASSERT_EQ(valence, valence2); + ASSERT_GE(valence, valence3); // may be clipped. + + // No more than 6 triangles can touch a cube corner, 6 edges result. + ASSERT_LE(valence, 6); + ASSERT_LE(valence2, 6); + ASSERT_LE(valence3, 6); + + // No less than 3 triangles can touch a cube corner, 3 edges result. + ASSERT_GE(valence, 3); + ASSERT_GE(valence2, 3); + ASSERT_GE(valence3, 3); + } + + table->GetValenceCache().ClearValenceCache(); + table->GetValenceCache().ClearValenceCacheInaccurate(); + } +}; + +TEST_F(CornerTableTest, NormalWithSeams) { TestEncodingCube(); } + +TEST_F(CornerTableTest, TestNonManifoldEdges) { + std::unique_ptr mesh = DecodeObj("non_manifold_wrap.obj"); + ASSERT_NE(mesh, nullptr); + std::unique_ptr ct = + draco::CreateCornerTableFromPositionAttribute(mesh.get()); + ASSERT_NE(ct, nullptr); + + MeshConnectedComponents connected_components; + connected_components.FindConnectedComponents(ct.get()); + ASSERT_EQ(connected_components.NumConnectedComponents(), 2); +} + +TEST_F(CornerTableTest, TestNewFace) { + // Tests that we can add a new face to the corner table. + const std::string file_name = "cube_att.obj"; + std::unique_ptr mesh = DecodeObj(file_name); + ASSERT_NE(mesh, nullptr); + + std::unique_ptr ct = + draco::CreateCornerTableFromPositionAttribute(mesh.get()); + ASSERT_NE(ct, nullptr); + ASSERT_EQ(ct->num_faces(), 12); + ASSERT_EQ(ct->num_corners(), 3 * 12); + ASSERT_EQ(ct->num_vertices(), 8); + + const VertexIndex new_vi = ct->AddNewVertex(); + ASSERT_EQ(ct->num_vertices(), 9); + + ASSERT_EQ(ct->AddNewFace({VertexIndex(6), VertexIndex(7), new_vi}), 12); + ASSERT_EQ(ct->num_faces(), 13); + ASSERT_EQ(ct->num_corners(), 3 * 13); + + ASSERT_EQ(ct->Vertex(CornerIndex(3 * 12 + 0)), 6); + ASSERT_EQ(ct->Vertex(CornerIndex(3 * 12) + 1), 7); + ASSERT_EQ(ct->Vertex(CornerIndex(3 * 12) + 2), new_vi); +} + +} // namespace draco diff --git a/contrib/draco/src/draco/mesh/mesh.cc b/contrib/draco/src/draco/mesh/mesh.cc index 3be4b1494..b287ecb45 100644 --- a/contrib/draco/src/draco/mesh/mesh.cc +++ b/contrib/draco/src/draco/mesh/mesh.cc @@ -15,6 +15,10 @@ #include "draco/mesh/mesh.h" #include +#include +#include +#include +#include namespace draco { @@ -22,7 +26,436 @@ namespace draco { template using conditional_t = typename std::conditional::type; +#ifdef DRACO_TRANSCODER_SUPPORTED +Mesh::Mesh() : compression_enabled_(false) {} +#else Mesh::Mesh() {} +#endif + +#ifdef DRACO_TRANSCODER_SUPPORTED +void Mesh::Copy(const Mesh &src) { + PointCloud::Copy(src); + name_ = src.name_; + faces_ = src.faces_; + attribute_data_ = src.attribute_data_; + material_library_.Copy(src.material_library_); + compression_enabled_ = src.compression_enabled_; + compression_options_ = src.compression_options_; + + // Copy mesh feature ID sets. + mesh_features_.clear(); + for (MeshFeaturesIndex i(0); i < src.NumMeshFeatures(); i++) { + std::unique_ptr mesh_features(new MeshFeatures()); + mesh_features->Copy(src.GetMeshFeatures(i)); + AddMeshFeatures(std::move(mesh_features)); + } + mesh_features_material_mask_ = src.mesh_features_material_mask_; + + // Copy non-material textures. + non_material_texture_library_.Copy(src.non_material_texture_library_); + + // Update pointers to non-material textures in mesh feature ID sets. + if (non_material_texture_library_.NumTextures() != 0) { + const auto texture_to_index_map = + src.non_material_texture_library_.ComputeTextureToIndexMap(); + for (MeshFeaturesIndex j(0); j < NumMeshFeatures(); ++j) { + Mesh::UpdateMeshFeaturesTexturePointer(texture_to_index_map, + &non_material_texture_library_, + &GetMeshFeatures(j)); + } + } + + // Copy structural metadata. + structural_metadata_.Copy(src.structural_metadata_); +} + +namespace { +// A helper struct that augments a point index with an attribute value index. +// A unique combination of |point_index| and |attribute_value_index| +// corresponds to a unique point on the mesh. Used to identify unique points +// after a new attribute is added to the mesh. +struct AugmentedPointData { + PointIndex point_index; + AttributeValueIndex attribute_value_index; + bool operator<(const AugmentedPointData &pd) const { + if (point_index < pd.point_index) { + return true; + } + if (point_index > pd.point_index) { + return false; + } + return attribute_value_index < pd.attribute_value_index; + } +}; +} // namespace + +int32_t Mesh::AddAttributeWithConnectivity( + std::unique_ptr att, + const IndexTypeVector &corner_to_value) { + // Map between augmented point and new point indices (one augmented point + // corresponds to one PointIndex). + std::map old_to_new_point_map; + + // Map between corners and the new point indices. + IndexTypeVector corner_to_point(num_faces() * 3, + kInvalidPointIndex); + + // Flag whether a given existing point index has been used. Used to ensure + // that mapping between existing and new point indices that are smaller + // than num_points() is identity. In other words, we want to keep indices of + // the existing points intact and add new points to end. + IndexTypeVector is_point_used(num_points(), false); + + int new_num_points = num_points(); + for (CornerIndex ci(0); ci < num_faces() * 3; ++ci) { + AugmentedPointData apd; + apd.point_index = CornerToPointId(ci); + apd.attribute_value_index = corner_to_value[ci]; + const auto it = old_to_new_point_map.find(apd); + if (it != old_to_new_point_map.end()) { + // Augmented point is already mapped to a point index. Reuse it. + corner_to_point[ci] = it->second; + } else { + // New combination of point index + attribute value index. Map it to a + // unique point index. + PointIndex new_point_index; + if (!is_point_used[apd.point_index]) { + // Reuse the existing (old) point index. + new_point_index = apd.point_index; + is_point_used[apd.point_index] = true; + } else { + // Add a new point index to the end. + new_point_index = PointIndex(new_num_points++); + } + old_to_new_point_map[apd] = new_point_index; + corner_to_point[ci] = new_point_index; + } + } + + // Update point to attribute value mapping for the new attribute. + att->SetExplicitMapping(new_num_points); + for (CornerIndex ci(0); ci < num_faces() * 3; ++ci) { + att->SetPointMapEntry(corner_to_point[ci], corner_to_value[ci]); + } + + // Update point to attribute value mapping on the remaining attributes if + // needed. + if (new_num_points > num_points()) { + set_num_points(new_num_points); + + // Setup attributes for the new number of points. + for (int ai = 0; ai < num_attributes(); ++ai) { + const bool mapping_was_identity = attribute(ai)->is_mapping_identity(); + attribute(ai)->SetExplicitMapping(new_num_points); + if (mapping_was_identity) { + // Convert all old points from identity to explicit mapping. + for (AttributeValueIndex avi(0); avi < attribute(ai)->size(); ++avi) { + attribute(ai)->SetPointMapEntry(PointIndex(avi.value()), avi); + } + } + } + + for (CornerIndex ci(0); ci < num_faces() * 3; ++ci) { + const PointIndex old_point_index = CornerToPointId(ci); + const PointIndex new_point_index = corner_to_point[ci]; + if (old_point_index == new_point_index) { + continue; + } + // Update point to value mapping for all existing attributes. + for (int ai = 0; ai < num_attributes(); ++ai) { + attribute(ai)->SetPointMapEntry( + new_point_index, attribute(ai)->mapped_index(old_point_index)); + } + // Update mapping between the corner and the new point index. + faces_[FaceIndex(ci.value() / 3)][ci.value() % 3] = new_point_index; + } + } + + // If any of the old points have not been used, initialize dummy mapping for + // the new attribute. + for (PointIndex pi(0); pi < is_point_used.size(); ++pi) { + if (!is_point_used[pi]) { + att->SetPointMapEntry(pi, AttributeValueIndex(0)); + } + } + + return PointCloud::AddAttribute(std::move(att)); +} + +int32_t Mesh::AddPerVertexAttribute(std::unique_ptr att) { + const PointAttribute *const pos_att = + GetNamedAttribute(GeometryAttribute::POSITION); + if (pos_att == nullptr) { + return -1; + } + if (att->size() != pos_att->size()) { + return -1; // Number of values must be same as in the position attribute. + } + + if (pos_att->is_mapping_identity()) { + att->SetIdentityMapping(); + } else { + // Copy point to attribute value mapping from the position attribute to + // |att|. + att->SetExplicitMapping(num_points()); + for (PointIndex pi(0); pi < num_points(); ++pi) { + att->SetPointMapEntry(pi, pos_att->mapped_index(pi)); + } + } + + return PointCloud::AddAttribute(std::move(att)); +} + +void Mesh::RemoveIsolatedPoints() { + // For each point, check if it is mapped to a face. + IndexTypeVector is_point_used(num_points(), false); + int num_used_points = 0; + for (FaceIndex fi(0); fi < num_faces(); ++fi) { + const auto &f = face(fi); + for (int c = 0; c < 3; ++c) { + if (!is_point_used[f[c]]) { + num_used_points++; + is_point_used[f[c]] = true; + } + } + } + if (num_used_points == num_points()) { + return; // All points are used. + } + + // Create mapping between the old and new point indices. + IndexTypeVector old_to_new_point_map( + num_points(), kInvalidPointIndex); + PointIndex new_point_index(0); + for (PointIndex pi(0); pi < num_points(); ++pi) { + if (is_point_used[pi]) { + old_to_new_point_map[pi] = new_point_index++; + } + } + + // Update point to attribute value index map for all attributes. + for (int ai = 0; ai < num_attributes(); ++ai) { + PointAttribute *att = attribute(ai); + if (att->is_mapping_identity()) { + // When the attribute uses identity mapping we need to reorder to the + // attribute values to match the new point indices. + for (PointIndex pi(0); pi < num_points(); ++pi) { + const PointIndex new_pi = old_to_new_point_map[pi]; + if (new_pi == pi || new_pi == kInvalidPointIndex) { + continue; + } + att->SetAttributeValue( + AttributeValueIndex(new_pi.value()), + att->GetAddress(AttributeValueIndex(pi.value()))); + } + att->Resize(num_used_points); + } else { + // For explicitly mapped attributes, we first update the point to + // attribute value mapping and then we remove all unused values from the + // attribute. + for (PointIndex pi(0); pi < num_points(); ++pi) { + const PointIndex new_pi = old_to_new_point_map[pi]; + if (new_pi == pi || new_pi == kInvalidPointIndex) { + continue; + } + att->SetPointMapEntry(new_pi, att->mapped_index(pi)); + } + att->SetExplicitMapping(num_used_points); + + att->RemoveUnusedValues(); + } + } + + // Update the mapping between faces and point indices. + for (FaceIndex fi(0); fi < num_faces(); ++fi) { + auto &f = faces_[fi]; + for (int c = 0; c < 3; ++c) { + f[c] = old_to_new_point_map[f[c]]; + } + } + + set_num_points(num_used_points); +} + +void Mesh::RemoveUnusedMaterials() { RemoveUnusedMaterials(true); } + +void Mesh::RemoveUnusedMaterials(bool remove_unused_material_indices) { + const int mat_att_index = GetNamedAttributeId(GeometryAttribute::MATERIAL); + if (mat_att_index == -1) { + // Remove all materials except for the first one. + while (GetMaterialLibrary().NumMaterials() > 1) { + GetMaterialLibrary().RemoveMaterial(1); + } + GetMaterialLibrary().RemoveUnusedTextures(); + return; + } + auto mat_att = attribute(mat_att_index); + + // Deduplicate attribute values in the material attribute to ensure that one + // attribute value index corresponds to one unique material index. + // Note that this does not remove unused material indices. + mat_att->DeduplicateValues(*mat_att); + + // Gather all material indices that are referenced by faces of the mesh. + const int num_materials = GetMaterialLibrary().NumMaterials(); + std::vector is_material_used(num_materials, false); + int num_used_materials = 0; + + // Helper function that updates |is_material_used| for the processed mesh. + auto update_used_materials = [&is_material_used, &num_used_materials, mat_att, + num_materials](PointIndex pi) { + uint32_t mat_index = 0; + mat_att->GetMappedValue(pi, &mat_index); + if (mat_index < num_materials) { + if (!is_material_used[mat_index]) { + is_material_used[mat_index] = true; + num_used_materials++; + } + } + }; + + if (num_faces() > 0) { + for (FaceIndex fi(0); fi < num_faces(); ++fi) { + update_used_materials(faces_[fi][0]); + } + } else { + // Handle the mesh as a point cloud and check materials used by points. + for (PointIndex pi(0); pi < num_points(); ++pi) { + update_used_materials(pi); + } + } + + // Check if any of the (unused) materials is used by mesh features. If so, + // user should remove unused mesh features first. + for (MeshFeaturesIndex mfi(0); mfi < NumMeshFeatures(); ++mfi) { + for (int mask_index = 0; mask_index < NumMeshFeaturesMaterialMasks(mfi); + ++mask_index) { + const int mat_index = GetMeshFeaturesMaterialMask(mfi, mask_index); + if (mat_index < num_materials && !is_material_used[mat_index]) { + is_material_used[mat_index] = true; + num_used_materials++; + } + } + } + + if (num_used_materials == num_materials) { + return; // All materials are used, don't do anything. + } + + // Remove unused materials from the material library or replace them with + // default materials if we do not remove unused material indices. + for (int mi = num_materials - 1; mi >= 0; --mi) { + if (!is_material_used[mi] && mi < GetMaterialLibrary().NumMaterials()) { + if (remove_unused_material_indices) { + GetMaterialLibrary().RemoveMaterial(mi); + } else { + GetMaterialLibrary().MutableMaterial(mi)->Clear(); + } + } + } + GetMaterialLibrary().RemoveUnusedTextures(); + + if (!remove_unused_material_indices) { + // All the code below handles updating of material indices. Since we do not + // want to update them, we can return early. + return; + } + + // Compute map between old and new material indices. + std::vector old_to_new_material_index_map(num_materials, -1); + for (int mi = 0, new_material_index = 0; mi < num_materials; ++mi) { + if (is_material_used[mi]) { + old_to_new_material_index_map[mi] = new_material_index; + ++new_material_index; + } + } + IndexTypeVector + old_to_new_material_attribute_value_index_map(mat_att->size(), -1); + for (AttributeValueIndex avi(0); avi < mat_att->size(); ++avi) { + uint32_t mat_index = 0; + mat_att->GetValue(avi, &mat_index); + if (mat_index < num_materials && is_material_used[mat_index]) { + old_to_new_material_attribute_value_index_map[avi] = + old_to_new_material_index_map[mat_index]; + } + } + + // Update attribute values with the new number of materials. + mat_att->Reset(num_used_materials); + + // Set identity mapping between AttributeValueIndex and material indices. + for (AttributeValueIndex avi(0); avi < mat_att->size(); ++avi) { + const uint32_t mat_index = avi.value(); + mat_att->SetAttributeValue(avi, &mat_index); + } + + // Update mapping between points and attribute values. + for (PointIndex pi(0); pi < num_points(); ++pi) { + const AttributeValueIndex old_avi = mat_att->mapped_index(pi); + mat_att->SetPointMapEntry( + pi, AttributeValueIndex( + old_to_new_material_attribute_value_index_map[old_avi])); + } + + // Update material indices on mesh features. + for (MeshFeaturesIndex mfi(0); mfi < NumMeshFeatures(); ++mfi) { + for (int mask_index = 0; mask_index < NumMeshFeaturesMaterialMasks(mfi); + ++mask_index) { + const int old_mat_index = GetMeshFeaturesMaterialMask(mfi, mask_index); + if (old_mat_index < num_materials && is_material_used[old_mat_index]) { + mesh_features_material_mask_[mfi][mask_index] = + old_to_new_material_index_map[old_mat_index]; + } + } + } +} + +void Mesh::UpdateMeshFeaturesTexturePointer( + const std::unordered_map &texture_to_index_map, + TextureLibrary *texture_library, MeshFeatures *mesh_features) { + TextureMap &texture_map = mesh_features->GetTextureMap(); + if (texture_map.texture() == nullptr) { + return; + } + const auto it = texture_to_index_map.find(texture_map.texture()); + DRACO_DCHECK(it != texture_to_index_map.end()); + const int texture_index = it->second; + DRACO_DCHECK(texture_index < texture_library->NumTextures()); + texture_map.SetTexture(texture_library->GetTexture(texture_index)); +} + +void Mesh::CopyMeshFeaturesForMaterial(const Mesh &source_mesh, + Mesh *target_mesh, int material_index) { + for (MeshFeaturesIndex mfi(0); mfi < source_mesh.NumMeshFeatures(); ++mfi) { + // Mesh features is used if it doesn't have any material mask or if one + // of the material masks matches |material_index|. + bool is_used = source_mesh.NumMeshFeaturesMaterialMasks(mfi) == 0; + for (int mask_index = 0; + !is_used && mask_index < source_mesh.NumMeshFeaturesMaterialMasks(mfi); + ++mask_index) { + if (source_mesh.GetMeshFeaturesMaterialMask(mfi, mask_index) == + material_index) { + is_used = true; + } + } + if (is_used) { + // Copy over the mesh features to the target mesh. Note that texture + // pointers are not updated at this step. + std::unique_ptr new_mf(new MeshFeatures()); + new_mf->Copy(source_mesh.GetMeshFeatures(mfi)); + target_mesh->AddMeshFeatures(std::move(new_mf)); + } + } +} + +int32_t Mesh::AddPerFaceAttribute(std::unique_ptr att) { + IndexTypeVector corner_map(num_faces() * 3); + for (CornerIndex ci(0); ci < num_faces() * 3; ++ci) { + corner_map[ci] = AttributeValueIndex(ci.value() / 3); + } + return AddAttributeWithConnectivity(std::move(att), corner_map); +} +#endif // DRACO_TRANSCODER_SUPPORTED #ifdef DRACO_ATTRIBUTE_INDICES_DEDUPLICATION_SUPPORTED void Mesh::ApplyPointIdDeduplication( diff --git a/contrib/draco/src/draco/mesh/mesh.h b/contrib/draco/src/draco/mesh/mesh.h index f4506da81..652c2c010 100644 --- a/contrib/draco/src/draco/mesh/mesh.h +++ b/contrib/draco/src/draco/mesh/mesh.h @@ -16,12 +16,20 @@ #define DRACO_MESH_MESH_H_ #include +#include #include "draco/attributes/geometry_indices.h" #include "draco/core/hash_utils.h" #include "draco/core/macros.h" #include "draco/core/status.h" #include "draco/draco_features.h" +#ifdef DRACO_TRANSCODER_SUPPORTED +#include "draco/compression/draco_compression_options.h" +#include "draco/material/material_library.h" +#include "draco/mesh/mesh_features.h" +#include "draco/mesh/mesh_indices.h" +#include "draco/metadata/structural_metadata.h" +#endif #include "draco/point_cloud/point_cloud.h" namespace draco { @@ -47,6 +55,11 @@ class Mesh : public PointCloud { Mesh(); +#ifdef DRACO_TRANSCODER_SUPPORTED + // Copies all data from the |src| mesh. + void Copy(const Mesh &src); +#endif + void AddFace(const Face &face) { faces_.push_back(face); } void SetFace(FaceIndex face_id, const Face &face) { @@ -83,6 +96,38 @@ class Mesh : public PointCloud { } } +#ifdef DRACO_TRANSCODER_SUPPORTED + // Adds a point attribute |att| to the mesh and returns the index of the + // newly inserted attribute. Attribute connectivity data is specified in + // |corner_to_value| array that contains mapping between face corners and + // attribute value indices. + // The purpose of this function is to allow users to add attributes with + // arbitrary connectivity to an existing mesh. New points will be + // automatically created if needed. + int32_t AddAttributeWithConnectivity( + std::unique_ptr att, + const IndexTypeVector &corner_to_value); + + // Adds a point attribute |att| to the mesh and returns the index of the + // newly inserted attribute. The inserted attribute must have the same + // connectivity as the position attribute of the mesh (that is, the attribute + // values are defined per-vertex). Each attribute value entry in |att| + // corresponds to the corresponding attribute value entry in the position + // attribute (AttributeValueIndex in both attributes refer to the same + // spatial vertex). + // Returns -1 in case of error. + int32_t AddPerVertexAttribute(std::unique_ptr att); + + // Removes points that are not mapped to any face of the mesh. All attribute + // values are going to be removed as well. + void RemoveIsolatedPoints(); + + // Adds a point attribute |att| to the mesh and returns the index of the + // newly inserted attribute. Attribute values are mapped 1:1 to face indices. + // Returns -1 in case of error. + int32_t AddPerFaceAttribute(std::unique_ptr att); +#endif // DRACO_TRANSCODER_SUPPORTED + MeshAttributeElementType GetAttributeElementType(int att_id) const { return attribute_data_[att_id].element_type; } @@ -109,6 +154,103 @@ class Mesh : public PointCloud { MeshAttributeElementType element_type; }; +#ifdef DRACO_TRANSCODER_SUPPORTED + void SetName(const std::string &name) { name_ = name; } + const std::string &GetName() const { return name_; } + const MaterialLibrary &GetMaterialLibrary() const { + return material_library_; + } + MaterialLibrary &GetMaterialLibrary() { return material_library_; } + + // Removes all materials that are not referenced by any face of the mesh. + // Optional argument |remove_unused_material_indices| can be used to control + // whether unusued material indices are removed as well (default = true). + // If material indices are not removed, the unused material indices will + // point to empty (default) materials. + void RemoveUnusedMaterials(); + void RemoveUnusedMaterials(bool remove_unused_material_indices); + + // Enables or disables Draco geometry compression for this mesh. + void SetCompressionEnabled(bool enabled) { compression_enabled_ = enabled; } + bool IsCompressionEnabled() const { return compression_enabled_; } + + // Sets |options| that configure Draco geometry compression. This does not + // enable or disable compression. + void SetCompressionOptions(const DracoCompressionOptions &options) { + compression_options_ = options; + } + const DracoCompressionOptions &GetCompressionOptions() const { + return compression_options_; + } + DracoCompressionOptions &GetCompressionOptions() { + return compression_options_; + } + + // Library that contains non-material textures. + const TextureLibrary &GetNonMaterialTextureLibrary() const { + return non_material_texture_library_; + } + TextureLibrary &GetNonMaterialTextureLibrary() { + return non_material_texture_library_; + } + + // Mesh feature ID sets as defined by EXT_mesh_features glTF extension. + MeshFeaturesIndex AddMeshFeatures( + std::unique_ptr mesh_features) { + mesh_features_.push_back(std::move(mesh_features)); + mesh_features_material_mask_.push_back({}); + return MeshFeaturesIndex(mesh_features_.size() - 1); + } + int NumMeshFeatures() const { return mesh_features_.size(); } + const MeshFeatures &GetMeshFeatures(MeshFeaturesIndex index) const { + return *mesh_features_[index]; + } + MeshFeatures &GetMeshFeatures(MeshFeaturesIndex index) { + return *mesh_features_[index]; + } + void RemoveMeshFeatures(MeshFeaturesIndex index) { + mesh_features_.erase(mesh_features_.begin() + index.value()); + mesh_features_material_mask_.erase(mesh_features_material_mask_.begin() + + index.value()); + } + + // Restricts given mesh features to faces mapped to a material with + // |material_index|. Note that single mesh features can be restricted to + // multiple materials. + void AddMeshFeaturesMaterialMask(MeshFeaturesIndex index, + int material_index) { + mesh_features_material_mask_[index].push_back(material_index); + } + + size_t NumMeshFeaturesMaterialMasks(MeshFeaturesIndex index) const { + return mesh_features_material_mask_[index].size(); + } + int GetMeshFeaturesMaterialMask(MeshFeaturesIndex index, + int mask_index) const { + return mesh_features_material_mask_[index][mask_index]; + } + + // Updates mesh features texture pointer to point to a new |texture_library|. + // The current texture pointer is used to determine the texture index in the + // new texture library via a given |texture_to_index_map|. + static void UpdateMeshFeaturesTexturePointer( + const std::unordered_map &texture_to_index_map, + TextureLibrary *texture_library, MeshFeatures *mesh_features); + + // Copies over mesh features from |source_mesh| and stores them in + // |target_mesh| as long as the mesh features material mask is valid for + // given |material_index|. + static void CopyMeshFeaturesForMaterial(const Mesh &source_mesh, + Mesh *target_mesh, + int material_index); + + // Structural metadata. + const StructuralMetadata &GetStructuralMetadata() const { + return structural_metadata_; + } + StructuralMetadata &GetStructuralMetadata() { return structural_metadata_; } +#endif // DRACO_TRANSCODER_SUPPORTED + protected: #ifdef DRACO_ATTRIBUTE_INDICES_DEDUPLICATION_SUPPORTED // Extends the point deduplication to face corners. This method is called from @@ -119,6 +261,10 @@ class Mesh : public PointCloud { const std::vector &unique_point_ids) override; #endif + // Exposes |faces_|. Use |faces_| at your own risk. DO NOT store the + // reference: the |faces_| object is destroyed with the mesh. + IndexTypeVector &faces() { return faces_; } + private: // Mesh specific per-attribute data. std::vector attribute_data_; @@ -127,6 +273,40 @@ class Mesh : public PointCloud { // that converts vertex indices into attribute indices. IndexTypeVector faces_; +#ifdef DRACO_TRANSCODER_SUPPORTED + // Mesh name. + std::string name_; + + // Materials applied to to this mesh. + MaterialLibrary material_library_; + + // Compression options for this mesh. + // TODO(vytyaz): Store encoded bitstream that this mesh compresses into. + bool compression_enabled_; + DracoCompressionOptions compression_options_; + + // Sets of feature IDs as defined by EXT_mesh_features glTF extension. + IndexTypeVector> + mesh_features_; + + // When the Mesh contains multiple materials, |mesh_features_material_mask_| + // can be used to limit specific MeshFeaturesIndex to a vector of material + // indices. If for a given mesh feature index, the material indices are empty, + // the corresponding mesh features are applied to the entire mesh. + IndexTypeVector> + mesh_features_material_mask_; + + // Texture library for storing non-material textures used by this mesh, e.g., + // textures containing mesh feature IDs of EXT_mesh_features glTF extension. + // If the mesh is part of the scene then the textures are stored in the scene. + // Note that mesh features contain pointers to non-material textures. It is + // responsibility of class user to update these pointers when updating the + // textures. See Mesh::Copy() for example. + TextureLibrary non_material_texture_library_; + + // Structural metadata defined by the EXT_structural_metadata glTF extension. + StructuralMetadata structural_metadata_; +#endif // DRACO_TRANSCODER_SUPPORTED friend struct MeshHasher; }; diff --git a/contrib/draco/src/draco/mesh/mesh_are_equivalent.cc b/contrib/draco/src/draco/mesh/mesh_are_equivalent.cc index b832379af..305811f10 100644 --- a/contrib/draco/src/draco/mesh/mesh_are_equivalent.cc +++ b/contrib/draco/src/draco/mesh/mesh_are_equivalent.cc @@ -15,6 +15,9 @@ #include "draco/mesh/mesh_are_equivalent.h" #include +#include + +#include "draco/texture/texture_utils.h" namespace draco { @@ -114,6 +117,55 @@ bool MeshAreEquivalent::operator()(const Mesh &mesh0, const Mesh &mesh1) { // face with respect to lex order. Init(mesh0, mesh1); +#ifdef DRACO_TRANSCODER_SUPPORTED + // Compare geometry compression settings. + if (mesh0.IsCompressionEnabled() != mesh1.IsCompressionEnabled()) { + return false; + } + if (mesh0.GetCompressionOptions() != mesh1.GetCompressionOptions()) { + return false; + } + + // Compare non-material texture library sizes. + if (mesh0.GetNonMaterialTextureLibrary().NumTextures() != + mesh1.GetNonMaterialTextureLibrary().NumTextures()) { + return false; + } + + // Compare mesh feature ID sets. + if (mesh0.NumMeshFeatures() != mesh1.NumMeshFeatures()) { + return false; + } + for (MeshFeaturesIndex i(0); i < mesh0.NumMeshFeatures(); ++i) { + const MeshFeatures &features0 = mesh0.GetMeshFeatures(i); + const MeshFeatures &features1 = mesh1.GetMeshFeatures(i); + if (features0.GetAttributeIndex() != features1.GetAttributeIndex()) { + return false; + } + if (features0.GetFeatureCount() != features1.GetFeatureCount()) { + return false; + } + if (features0.GetLabel() != features1.GetLabel()) { + return false; + } + if (features0.GetNullFeatureId() != features1.GetNullFeatureId()) { + return false; + } + if (features0.GetTextureChannels() != features1.GetTextureChannels()) { + return false; + } + if (features0.GetPropertyTableIndex() != + features1.GetPropertyTableIndex()) { + return false; + } + const TextureMap &map0 = features0.GetTextureMap(); + const TextureMap &map1 = features1.GetTextureMap(); + if (map0.tex_coord_index() != map1.tex_coord_index()) { + return false; + } + } +#endif // DRACO_TRANSCODER_SUPPORTED + // Check for every attribute that is valid that every corner is identical. typedef GeometryAttribute::Type AttributeType; const int att_max = AttributeType::NAMED_ATTRIBUTES_COUNT; diff --git a/contrib/draco/src/draco/mesh/mesh_are_equivalent_test.cc b/contrib/draco/src/draco/mesh/mesh_are_equivalent_test.cc index 74db3f7de..94d8c9c16 100644 --- a/contrib/draco/src/draco/mesh/mesh_are_equivalent_test.cc +++ b/contrib/draco/src/draco/mesh/mesh_are_equivalent_test.cc @@ -15,6 +15,7 @@ #include "draco/mesh/mesh_are_equivalent.h" #include +#include #include "draco/core/draco_test_base.h" #include "draco/core/draco_test_utils.h" @@ -30,6 +31,14 @@ TEST_F(MeshAreEquivalentTest, TestOnIndenticalMesh) { const std::string file_name = "test_nm.obj"; const std::unique_ptr mesh(ReadMeshFromTestFile(file_name)); ASSERT_NE(mesh, nullptr) << "Failed to load test model." << file_name; + +#ifdef DRACO_TRANSCODER_SUPPORTED + // Add mesh feature ID set to the mesh. + std::unique_ptr mesh_features(new MeshFeatures()); + mesh->AddMeshFeatures(std::move(mesh_features)); +#endif + + // Check that mesh is equivalent to itself. MeshAreEquivalent equiv; ASSERT_TRUE(equiv(*mesh, *mesh)); } @@ -95,4 +104,32 @@ TEST_F(MeshAreEquivalentTest, TestOnBigMesh) { ASSERT_TRUE(equiv(*mesh0, *mesh1)); } +#ifdef DRACO_TRANSCODER_SUPPORTED + +TEST_F(MeshAreEquivalentTest, TestMeshFeatures) { + const std::string file_name = "test_nm.obj"; + const std::unique_ptr mesh0(ReadMeshFromTestFile(file_name)); + const std::unique_ptr mesh1(ReadMeshFromTestFile(file_name)); + ASSERT_NE(mesh0, nullptr); + ASSERT_NE(mesh1, nullptr); + + // Add identical mesh feature ID sets to meshes. + mesh0->AddMeshFeatures(std::unique_ptr(new MeshFeatures())); + mesh1->AddMeshFeatures(std::unique_ptr(new MeshFeatures())); + + // Empty feature sets should match. + MeshAreEquivalent equiv; + ASSERT_TRUE(equiv(*mesh0, *mesh1)); + + // Make mesh features different and check that the meshes are not equivalent. + mesh0->GetMeshFeatures(MeshFeaturesIndex(0)).SetFeatureCount(5); + mesh1->GetMeshFeatures(MeshFeaturesIndex(0)).SetFeatureCount(6); + ASSERT_FALSE(equiv(*mesh0, *mesh1)); + + // Make mesh features identical and check that the meshes are equivalent. + mesh0->GetMeshFeatures(MeshFeaturesIndex(0)).SetFeatureCount(1); + mesh1->GetMeshFeatures(MeshFeaturesIndex(0)).SetFeatureCount(1); + ASSERT_TRUE(equiv(*mesh0, *mesh1)); +} +#endif // DRACO_TRANSCODER_SUPPORTED } // namespace draco diff --git a/contrib/draco/src/draco/mesh/mesh_attribute_corner_table.cc b/contrib/draco/src/draco/mesh/mesh_attribute_corner_table.cc index 28b68d5fd..54801ce5c 100644 --- a/contrib/draco/src/draco/mesh/mesh_attribute_corner_table.cc +++ b/contrib/draco/src/draco/mesh/mesh_attribute_corner_table.cc @@ -126,18 +126,18 @@ void MeshAttributeCornerTable::AddSeamEdge(CornerIndex c) { } } -void MeshAttributeCornerTable::RecomputeVertices(const Mesh *mesh, +bool MeshAttributeCornerTable::RecomputeVertices(const Mesh *mesh, const PointAttribute *att) { DRACO_DCHECK(GetValenceCache().IsCacheEmpty()); if (mesh != nullptr && att != nullptr) { - RecomputeVerticesInternal(mesh, att); + return RecomputeVerticesInternal(mesh, att); } else { - RecomputeVerticesInternal(nullptr, nullptr); + return RecomputeVerticesInternal(nullptr, nullptr); } } template -void MeshAttributeCornerTable::RecomputeVerticesInternal( +bool MeshAttributeCornerTable::RecomputeVerticesInternal( const Mesh *mesh, const PointAttribute *att) { DRACO_DCHECK(GetValenceCache().IsCacheEmpty()); vertex_to_attribute_entry_id_map_.clear(); @@ -167,6 +167,11 @@ void MeshAttributeCornerTable::RecomputeVerticesInternal( while (act_c != kInvalidCornerIndex) { first_c = act_c; act_c = SwingLeft(act_c); + if (act_c == c) { + // We reached the initial corner which shouldn't happen when we swing + // left from |c|. + return false; + } } } corner_to_vertex_map_[first_c.value()] = VertexIndex(first_vert_id.value()); @@ -189,6 +194,7 @@ void MeshAttributeCornerTable::RecomputeVerticesInternal( act_c = corner_table_->SwingRight(act_c); } } + return true; } int MeshAttributeCornerTable::Valence(VertexIndex v) const { diff --git a/contrib/draco/src/draco/mesh/mesh_attribute_corner_table.h b/contrib/draco/src/draco/mesh/mesh_attribute_corner_table.h index 7dad25cf1..c60be7c86 100644 --- a/contrib/draco/src/draco/mesh/mesh_attribute_corner_table.h +++ b/contrib/draco/src/draco/mesh/mesh_attribute_corner_table.h @@ -40,7 +40,7 @@ class MeshAttributeCornerTable { // whenever the seam edges are updated). // |mesh| and |att| can be null, in which case mapping between vertices and // attribute value ids is set to identity. - void RecomputeVertices(const Mesh *mesh, const PointAttribute *att); + bool RecomputeVertices(const Mesh *mesh, const PointAttribute *att); inline bool IsCornerOppositeToSeamEdge(CornerIndex corner) const { return is_edge_on_seam_[corner.value()]; @@ -130,6 +130,12 @@ class MeshAttributeCornerTable { return false; } + bool IsDegenerated(FaceIndex face) const { + // Introducing seams can't change the degeneracy of the individual faces, + // therefore we can delegate the check to the original |corner_table_|. + return corner_table_->IsDegenerated(face); + } + bool no_interior_seams() const { return no_interior_seams_; } const CornerTable *corner_table() const { return corner_table_; } @@ -166,7 +172,7 @@ class MeshAttributeCornerTable { private: template - void RecomputeVerticesInternal(const Mesh *mesh, const PointAttribute *att); + bool RecomputeVerticesInternal(const Mesh *mesh, const PointAttribute *att); std::vector is_edge_on_seam_; std::vector is_vertex_on_seam_; diff --git a/contrib/draco/src/draco/mesh/mesh_cleanup.cc b/contrib/draco/src/draco/mesh/mesh_cleanup.cc index 75b55f045..a6dc1823e 100644 --- a/contrib/draco/src/draco/mesh/mesh_cleanup.cc +++ b/contrib/draco/src/draco/mesh/mesh_cleanup.cc @@ -14,21 +14,25 @@ // #include "draco/mesh/mesh_cleanup.h" +#include +#include #include +#include +#include #include "draco/core/hash_utils.h" namespace draco { -bool MeshCleanup::operator()(Mesh *mesh, const MeshCleanupOptions &options) { +Status MeshCleanup::Cleanup(Mesh *mesh, const MeshCleanupOptions &options) { if (!options.remove_degenerated_faces && !options.remove_unused_attributes && !options.remove_duplicate_faces && !options.make_geometry_manifold) { - return true; // Nothing to cleanup. + return OkStatus(); // Nothing to cleanup. } const PointAttribute *const pos_att = mesh->GetNamedAttribute(GeometryAttribute::POSITION); if (pos_att == nullptr) { - return false; + return Status(Status::DRACO_ERROR, "Missing position attribute."); } if (options.remove_degenerated_faces) { @@ -43,7 +47,7 @@ bool MeshCleanup::operator()(Mesh *mesh, const MeshCleanupOptions &options) { RemoveUnusedAttributes(mesh); } - return true; + return OkStatus(); } void MeshCleanup::RemoveDegeneratedFaces(Mesh *mesh) { diff --git a/contrib/draco/src/draco/mesh/mesh_cleanup.h b/contrib/draco/src/draco/mesh/mesh_cleanup.h index 09aae2e1c..c6bdfc6c0 100644 --- a/contrib/draco/src/draco/mesh/mesh_cleanup.h +++ b/contrib/draco/src/draco/mesh/mesh_cleanup.h @@ -16,42 +16,38 @@ #define DRACO_MESH_MESH_CLEANUP_H_ #include "draco/core/status.h" +#include "draco/draco_features.h" #include "draco/mesh/mesh.h" namespace draco { // Options used by the MeshCleanup class. struct MeshCleanupOptions { - MeshCleanupOptions() - : remove_degenerated_faces(true), - remove_duplicate_faces(true), - remove_unused_attributes(true), - make_geometry_manifold(false) {} // If true, the cleanup tool removes any face where two or more vertices // share the same position index. - bool remove_degenerated_faces; + bool remove_degenerated_faces = true; // If true, the cleanup tool removes all duplicate faces. A pair of faces is // duplicate if both faces share the same position indices on all vertices // (that is, position values have to be duduplicated). Note that all // non-position properties are currently ignored. - bool remove_duplicate_faces; + bool remove_duplicate_faces = true; // If true, the cleanup tool removes any unused attribute value or unused // point id. For example, it can be used to remove isolated vertices. - bool remove_unused_attributes; + bool remove_unused_attributes = true; // If true, the cleanup tool splits vertices along non-manifold edges and // vertices. This ensures that the connectivity defined by position indices // is manifold. - bool make_geometry_manifold; + bool make_geometry_manifold = false; }; // Tool that can be used for removing bad or unused data from draco::Meshes. class MeshCleanup { public: // Performs in-place cleanup of the input mesh according to the input options. - bool operator()(Mesh *mesh, const MeshCleanupOptions &options); + static Status Cleanup(Mesh *mesh, const MeshCleanupOptions &options); private: static void RemoveDegeneratedFaces(Mesh *mesh); diff --git a/contrib/draco/src/draco/mesh/mesh_cleanup_test.cc b/contrib/draco/src/draco/mesh/mesh_cleanup_test.cc index 89c350e94..76e5206ae 100644 --- a/contrib/draco/src/draco/mesh/mesh_cleanup_test.cc +++ b/contrib/draco/src/draco/mesh/mesh_cleanup_test.cc @@ -15,6 +15,7 @@ #include "draco/mesh/mesh_cleanup.h" #include "draco/core/draco_test_base.h" +#include "draco/core/draco_test_utils.h" #include "draco/core/vector_d.h" #include "draco/mesh/triangle_soup_mesh_builder.h" @@ -43,9 +44,7 @@ TEST_F(MeshCleanupTest, TestDegneratedFaces) { ASSERT_NE(mesh, nullptr) << "Failed to build the test mesh."; ASSERT_EQ(mesh->num_faces(), 2) << "Wrong number of faces in the input mesh."; MeshCleanupOptions cleanup_options; - MeshCleanup cleanup; - ASSERT_TRUE(cleanup(mesh.get(), cleanup_options)) - << "Failed to cleanup the mesh."; + DRACO_ASSERT_OK(MeshCleanup::Cleanup(mesh.get(), cleanup_options)); ASSERT_EQ(mesh->num_faces(), 1) << "Failed to remove degenerated faces."; } @@ -89,9 +88,7 @@ TEST_F(MeshCleanupTest, TestDegneratedFacesAndIsolatedVertices) { << "Wrong number of point ids in the input mesh."; ASSERT_EQ(mesh->attribute(int_att_id)->size(), 3); const MeshCleanupOptions cleanup_options; - MeshCleanup cleanup; - ASSERT_TRUE(cleanup(mesh.get(), cleanup_options)) - << "Failed to cleanup the mesh."; + DRACO_ASSERT_OK(MeshCleanup::Cleanup(mesh.get(), cleanup_options)); ASSERT_EQ(mesh->num_faces(), 1) << "Failed to remove degenerated faces."; ASSERT_EQ(mesh->num_points(), 3) << "Failed to remove isolated attribute indices."; @@ -133,9 +130,7 @@ TEST_F(MeshCleanupTest, TestAttributes) { ASSERT_EQ(mesh->attribute(1)->size(), 2u) << "Wrong number of generic attribute entries."; const MeshCleanupOptions cleanup_options; - MeshCleanup cleanup; - ASSERT_TRUE(cleanup(mesh.get(), cleanup_options)) - << "Failed to cleanup the mesh."; + DRACO_ASSERT_OK(MeshCleanup::Cleanup(mesh.get(), cleanup_options)); ASSERT_EQ(mesh->num_faces(), 1) << "Failed to remove degenerated faces."; ASSERT_EQ(mesh->num_points(), 3) << "Failed to remove isolated attribute indices."; @@ -184,8 +179,7 @@ TEST_F(MeshCleanupTest, TestDuplicateFaces) { ASSERT_NE(mesh, nullptr); ASSERT_EQ(mesh->num_faces(), 5); const MeshCleanupOptions cleanup_options; - MeshCleanup cleanup; - ASSERT_TRUE(cleanup(mesh.get(), cleanup_options)); + DRACO_ASSERT_OK(MeshCleanup::Cleanup(mesh.get(), cleanup_options)); ASSERT_EQ(mesh->num_faces(), 2); } diff --git a/contrib/draco/src/draco/mesh/mesh_connected_components.h b/contrib/draco/src/draco/mesh/mesh_connected_components.h new file mode 100644 index 000000000..6ee30551e --- /dev/null +++ b/contrib/draco/src/draco/mesh/mesh_connected_components.h @@ -0,0 +1,161 @@ +// Copyright 2016 The Draco Authors. +// +// 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 DRACO_MESH_MESH_CONNECTED_COMPONENTS_H_ +#define DRACO_MESH_MESH_CONNECTED_COMPONENTS_H_ + +#include + +#include "draco/mesh/corner_table.h" + +namespace draco { + +// Class for detecting connected components on an input mesh defined by a +// corner table. Degenerated faces and their vertices are not assigned to any +// component. +class MeshConnectedComponents { + public: + MeshConnectedComponents() = default; + + // Initializes the class with the component data of the input mesh. No other + // method should be called before this one. + template + void FindConnectedComponents(const CornerTableT *corner_table); + int NumConnectedComponents() const { return components_.size(); } + + struct ConnectedComponent { + std::vector vertices; + std::vector faces; + std::vector boundary_edges; + }; + + const ConnectedComponent &GetConnectedComponent(int index) const { + return components_[index]; + } + + // Returns the id of an component attached to a given vertex. Returns -1 when + // the vertex was not assigned to any component. + int GetConnectedComponentIdAtVertex(int vertex_id) const { + return vertex_to_component_map_[vertex_id]; + } + + // Returns the number of vertices that belong to the input component. + int NumConnectedComponentVertices(int component_id) const { + return components_[component_id].vertices.size(); + } + + // Returns the i-th vertex of the input component. + int GetConnectedComponentVertex(int component_id, int i) const { + return components_[component_id].vertices[i]; + } + + // Returns the id of an component attached to a given face. Returns -1 when + // the face was not assigned to any component. + int GetConnectedComponentIdAtFace(int face_id) const { + return face_to_component_map_[face_id]; + } + + // Returns the number of faces that belong to the input component. + int NumConnectedComponentFaces(int component_id) const { + return components_[component_id].faces.size(); + } + + // Returns the i-th face of the input component. + int GetConnectedComponentFace(int component_id, int i) const { + return components_[component_id].faces[i]; + } + + // Returns the number of boundary edges that belong to the input component. + int NumConnectedComponentBoundaryEdges(int component_id) const { + return components_[component_id].boundary_edges.size(); + } + + // Returns the i-th boundary edge of the input component. + int GetConnectedComponentBoundaryEdge(int component_id, int i) const { + return components_[component_id].boundary_edges[i]; + } + + private: + std::vector vertex_to_component_map_; + std::vector face_to_component_map_; + std::vector boundary_corner_to_component_map_; + std::vector components_; +}; + +template +void MeshConnectedComponents::FindConnectedComponents( + const CornerTableT *corner_table) { + components_.clear(); + vertex_to_component_map_.assign(corner_table->num_vertices(), -1); + face_to_component_map_.assign(corner_table->num_faces(), -1); + boundary_corner_to_component_map_.assign(corner_table->num_corners(), -1); + std::vector is_face_visited(corner_table->num_faces(), false); + std::vector face_stack; + // Go over all faces of the mesh and for each unvisited face, recursively + // traverse its neighborhood and mark all traversed faces as visited. All + // faces visited during one traversal belong to one mesh component. + for (int face_id = 0; face_id < corner_table->num_faces(); ++face_id) { + if (is_face_visited[face_id]) { + continue; + } + if (corner_table->IsDegenerated(FaceIndex(face_id))) { + continue; + } + const int component_id = components_.size(); + components_.push_back(ConnectedComponent()); + face_stack.push_back(face_id); + is_face_visited[face_id] = true; + while (!face_stack.empty()) { + const int act_face_id = face_stack.back(); + if (face_to_component_map_[act_face_id] == -1) { + face_to_component_map_[act_face_id] = component_id; + components_[component_id].faces.push_back(act_face_id); + } + face_stack.pop_back(); + // Gather all neighboring faces. + std::array corners = + corner_table->AllCorners(FaceIndex(act_face_id)); + for (int c = 0; c < 3; ++c) { + // Update vertex to component mapping. + const int vertex_id = corner_table->Vertex(corners[c]).value(); + if (vertex_to_component_map_[vertex_id] == -1) { + vertex_to_component_map_[vertex_id] = component_id; + components_[component_id].vertices.push_back(vertex_id); + } + // Traverse component to neighboring faces (add the faces to the stack). + const CornerIndex opp_corner = corner_table->Opposite(corners[c]); + if (opp_corner == kInvalidCornerIndex) { + if (boundary_corner_to_component_map_[corners[c].value()] == -1) { + boundary_corner_to_component_map_[corners[c].value()] = + component_id; + components_[component_id].boundary_edges.push_back( + corners[c].value()); + } + continue; // Invalid corner (mesh boundary). + } + + const int opp_face_id = corner_table->Face(opp_corner).value(); + if (is_face_visited[opp_face_id]) { + continue; // Opposite face has been already reached. + } + is_face_visited[opp_face_id] = true; + face_stack.push_back(opp_face_id); + } + } + } +} + +} // namespace draco + +#endif // DRACO_MESH_MESH_CONNECTED_COMPONENTS_H_ diff --git a/contrib/draco/src/draco/mesh/mesh_features.cc b/contrib/draco/src/draco/mesh/mesh_features.cc new file mode 100644 index 000000000..f859ae411 --- /dev/null +++ b/contrib/draco/src/draco/mesh/mesh_features.cc @@ -0,0 +1,98 @@ +// Copyright 2022 The Draco Authors. +// +// 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 "draco/mesh/mesh_features.h" + +#include +#include + +#ifdef DRACO_TRANSCODER_SUPPORTED + +namespace draco { + +MeshFeatures::MeshFeatures() + : feature_count_(0), + null_feature_id_(-1), + attribute_index_(-1), + property_table_index_(-1) {} + +void MeshFeatures::Copy(const MeshFeatures &src) { + label_ = src.label_; + feature_count_ = src.feature_count_; + null_feature_id_ = src.null_feature_id_; + attribute_index_ = src.attribute_index_; + texture_map_.Copy(src.texture_map_); + texture_channels_ = src.texture_channels_; + property_table_index_ = src.property_table_index_; +} + +void MeshFeatures::SetLabel(const std::string &label) { label_ = label; } + +const std::string &MeshFeatures::GetLabel() const { return label_; } + +void MeshFeatures::SetFeatureCount(int feature_count) { + feature_count_ = feature_count; +} + +int MeshFeatures::GetFeatureCount() const { return feature_count_; } + +void MeshFeatures::SetNullFeatureId(int null_feature_id) { + null_feature_id_ = null_feature_id; +} + +int MeshFeatures::GetNullFeatureId() const { return null_feature_id_; } + +void MeshFeatures::SetAttributeIndex(int attribute_index) { + attribute_index_ = attribute_index; +} + +int MeshFeatures::GetAttributeIndex() const { return attribute_index_; } + +void MeshFeatures::SetTextureMap(const TextureMap &texture_map) { + texture_map_.Copy(texture_map); +} + +void MeshFeatures::SetTextureMap(Texture *texture, int tex_coord_index) { + texture_map_.SetProperties(TextureMap::GENERIC, tex_coord_index); + texture_map_.SetTexture(texture); +} + +const TextureMap &MeshFeatures::GetTextureMap() const { return texture_map_; } + +TextureMap &MeshFeatures::GetTextureMap() { return texture_map_; } + +void MeshFeatures::SetTextureChannels( + const std::vector &texture_channels) { + texture_channels_ = texture_channels; +} + +const std::vector &MeshFeatures::GetTextureChannels() const { + return texture_channels_; +} + +std::vector &MeshFeatures::GetTextureChannels() { + return texture_channels_; +} + +void MeshFeatures::SetPropertyTableIndex(int property_table_index) { + property_table_index_ = property_table_index; +} + +int MeshFeatures::GetPropertyTableIndex() const { + return property_table_index_; +} + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED diff --git a/contrib/draco/src/draco/mesh/mesh_features.h b/contrib/draco/src/draco/mesh/mesh_features.h new file mode 100644 index 000000000..af024013f --- /dev/null +++ b/contrib/draco/src/draco/mesh/mesh_features.h @@ -0,0 +1,93 @@ +// Copyright 2022 The Draco Authors. +// +// 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 DRACO_MESH_MESH_FEATURES_H_ +#define DRACO_MESH_MESH_FEATURES_H_ + +#include "draco/draco_features.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED +#include +#include + +#include "draco/texture/texture_library.h" +#include "draco/texture/texture_map.h" + +namespace draco { + +// Describes a mesh feature ID set according to the EXT_mesh_features glTF +// extension. Feature IDs are either associated with geometry vertices or with +// texture pixels and stored in a geometry attribute or in texture channels, +// respectively. Optionally, the feature ID set may be associated with a +// property table defined in the EXT_structural_metadata glTF extension. +class MeshFeatures { + public: + // Creates an empty feature ID set that is associated neither with vertices, + // nor with texture pixels, nor with property tables. + MeshFeatures(); + + // Copies all data from |src| mesh feature ID set. + void Copy(const MeshFeatures &src); + + // Label assigned to this feature ID set. + void SetLabel(const std::string &label); + const std::string &GetLabel() const; + + // The number of unique features in this feature ID set. + void SetFeatureCount(int feature_count); + int GetFeatureCount() const; + + // Non-negative null feature ID value indicating the absence of an associated + // feature. The value of -1 indicates that the null feature ID is not set. + void SetNullFeatureId(int null_feature_id); + int GetNullFeatureId() const; + + // Index of the feature ID vertex attribute, e.g., 5 for an attribute named + // _FEATURE_ID_5, or -1 if the feature ID is not associated with vertices. + void SetAttributeIndex(int attribute_index); + int GetAttributeIndex() const; + + // Feature ID texture map and texture channels containing feature IDs + // associated with texture pixels. Only used when |attribute_index_| is -1. + // The RGBA channels are numbered from 0 to 3. See the glTF extension + // documentation for reconstruction of feature ID from the channel values. + void SetTextureMap(const TextureMap &texture_map); + void SetTextureMap(Texture *texture, int tex_coord_index); + const TextureMap &GetTextureMap() const; + TextureMap &GetTextureMap(); + void SetTextureChannels(const std::vector &texture_channels); + const std::vector &GetTextureChannels() const; + std::vector &GetTextureChannels(); + + // Non-negative index of the property table this feature ID set is associated + // with. Property tables are defined in the EXT_structural_metadata glTF + // extension. The value of -1 indicates that this feature ID set is not + // associated with any property tables. + void SetPropertyTableIndex(int property_table_index); + int GetPropertyTableIndex() const; + + private: + std::string label_; + int feature_count_; + int null_feature_id_; + int attribute_index_; + TextureMap texture_map_; + std::vector texture_channels_; + int property_table_index_; +}; + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED +#endif // DRACO_MESH_MESH_FEATURES_H_ diff --git a/contrib/draco/src/draco/mesh/mesh_features_test.cc b/contrib/draco/src/draco/mesh/mesh_features_test.cc new file mode 100644 index 000000000..0e67af2b1 --- /dev/null +++ b/contrib/draco/src/draco/mesh/mesh_features_test.cc @@ -0,0 +1,98 @@ +// Copyright 2022 The Draco Authors. +// +// 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 "draco/mesh/mesh_features.h" + +#include +#include + +#include "draco/core/draco_test_base.h" +#include "draco/core/draco_test_utils.h" +#include "draco/texture/texture_map.h" + +namespace { + +#ifdef DRACO_TRANSCODER_SUPPORTED + +TEST(MeshFeaturesTest, TestDefaults) { + // Test construction of an empty feature ID set. + draco::MeshFeatures mesh_features; + ASSERT_TRUE(mesh_features.GetLabel().empty()); + ASSERT_EQ(mesh_features.GetFeatureCount(), 0); + ASSERT_EQ(mesh_features.GetNullFeatureId(), -1); + ASSERT_EQ(mesh_features.GetAttributeIndex(), -1); + ASSERT_EQ(mesh_features.GetPropertyTableIndex(), -1); + ASSERT_TRUE(mesh_features.GetTextureChannels().empty()); + ASSERT_EQ(mesh_features.GetTextureMap().texture(), nullptr); + ASSERT_EQ(mesh_features.GetTextureMap().type(), draco::TextureMap::GENERIC); +} + +TEST(MeshFeaturesTest, TestSettersAndGetters) { + // Test setter and getter methods of the feature ID set. + draco::MeshFeatures mesh_features; + mesh_features.SetLabel("continent"); + mesh_features.SetFeatureCount(8); + mesh_features.SetNullFeatureId(0); + mesh_features.SetAttributeIndex(2); + mesh_features.SetPropertyTableIndex(10); + std::vector channels = {2, 3}; + mesh_features.SetTextureChannels({2, 3}); + draco::TextureMap texture_map; + texture_map.SetProperties(draco::TextureMap::GENERIC, 1); + std::unique_ptr texture(new draco::Texture()); + texture_map.SetTexture(texture.get()); + mesh_features.SetTextureMap(texture_map); + + // Check that mesh feature set properties can be accessed via getters. + ASSERT_EQ(mesh_features.GetLabel(), "continent"); + ASSERT_EQ(mesh_features.GetFeatureCount(), 8); + ASSERT_EQ(mesh_features.GetNullFeatureId(), 0); + ASSERT_EQ(mesh_features.GetAttributeIndex(), 2); + ASSERT_EQ(mesh_features.GetPropertyTableIndex(), 10); + ASSERT_EQ(mesh_features.GetTextureChannels(), channels); + ASSERT_EQ(mesh_features.GetTextureMap().texture(), texture.get()); + ASSERT_EQ(mesh_features.GetTextureMap().type(), draco::TextureMap::GENERIC); +} + +TEST(MeshFeaturesTest, TestCopy) { + // Test that feature ID set can be copied. + draco::MeshFeatures mesh_features; + mesh_features.SetLabel("continent"); + mesh_features.SetFeatureCount(8); + mesh_features.SetNullFeatureId(0); + mesh_features.SetAttributeIndex(2); + mesh_features.SetPropertyTableIndex(10); + std::vector channels = {2, 3}; + mesh_features.SetTextureChannels({2, 3}); + std::unique_ptr texture(new draco::Texture()); + mesh_features.SetTextureMap(texture.get(), 1); + + // Make a copy. + draco::MeshFeatures copy; + copy.Copy(mesh_features); + + // Check the copy. + ASSERT_EQ(copy.GetLabel(), "continent"); + ASSERT_EQ(copy.GetFeatureCount(), 8); + ASSERT_EQ(copy.GetNullFeatureId(), 0); + ASSERT_EQ(copy.GetAttributeIndex(), 2); + ASSERT_EQ(copy.GetPropertyTableIndex(), 10); + ASSERT_EQ(copy.GetTextureChannels(), channels); + ASSERT_EQ(copy.GetTextureMap().texture(), texture.get()); + ASSERT_EQ(copy.GetTextureMap().type(), draco::TextureMap::GENERIC); +} + +#endif // DRACO_TRANSCODER_SUPPORTED + +} // namespace diff --git a/contrib/draco/src/draco/mesh/mesh_indices.h b/contrib/draco/src/draco/mesh/mesh_indices.h new file mode 100644 index 000000000..5df28d550 --- /dev/null +++ b/contrib/draco/src/draco/mesh/mesh_indices.h @@ -0,0 +1,37 @@ +// Copyright 2022 The Draco Authors. +// +// 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. +// +#ifdef DRACO_TRANSCODER_SUPPORTED +#ifndef DRACO_MESH_MESH_INDICES_H_ +#define DRACO_MESH_MESH_INDICES_H_ + +#include + +#include + +#include "draco/core/draco_index_type.h" + +namespace draco { + +// Index of a mesh feature ID set. +DEFINE_NEW_DRACO_INDEX_TYPE(uint32_t, MeshFeaturesIndex) + +// Constants denoting invalid indices. +static constexpr MeshFeaturesIndex kInvalidMeshFeaturesIndex( + std::numeric_limits::max()); + +} // namespace draco + +#endif // DRACO_MESH_MESH_INDICES_H_ +#endif // DRACO_TRANSCODER_SUPPORTED diff --git a/contrib/draco/src/draco/mesh/mesh_misc_functions.h b/contrib/draco/src/draco/mesh/mesh_misc_functions.h index b450bc80c..0a3bcf497 100644 --- a/contrib/draco/src/draco/mesh/mesh_misc_functions.h +++ b/contrib/draco/src/draco/mesh/mesh_misc_functions.h @@ -67,7 +67,6 @@ inline bool IsCornerOppositeToAttributeSeam(CornerIndex ci, // Interpolates an attribute value on a face using given barycentric // coordinates. InterpolatedVectorT should be a VectorD that corresponds to the // values stored in the attribute. -// TODO(ostava): Find a better place for this. template InterpolatedVectorT ComputeInterpolatedAttributeValueOnMeshFace( const Mesh &mesh, const PointAttribute &attribute, FaceIndex fi, diff --git a/contrib/draco/src/draco/mesh/mesh_splitter.cc b/contrib/draco/src/draco/mesh/mesh_splitter.cc new file mode 100644 index 000000000..ac3c4661c --- /dev/null +++ b/contrib/draco/src/draco/mesh/mesh_splitter.cc @@ -0,0 +1,451 @@ +// Copyright 2017 The Draco Authors. +// +// 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 "draco/mesh/mesh_splitter.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED +#include +#include +#include + +#include "draco/mesh/mesh_utils.h" +#include "draco/mesh/triangle_soup_mesh_builder.h" +#include "draco/point_cloud/point_cloud_builder.h" + +namespace draco { + +// Helper class that handles splitting of meshes with faces / without faces, +// i.e. point clouds. +template +class MeshSplitterInternal { + public: + struct WorkData : public MeshSplitter::WorkData { + // TriangleSoupMeshBuilder or PointCloudBuilder. + std::vector builders; + }; + + // Computes number of elements (faces or points) for each sub-mesh. + Status InitializeWorkDataNumElements(const Mesh &mesh, int split_attribute_id, + WorkData *work_data) const; + // Initializes a builder for a given sub-mesh. + void InitializeBuilder(int b_index, int num_elements, const Mesh &mesh, + int ignored_attribute_id, WorkData *work_data) const; + // Add all faces or points to the builders. + void AddElementsToBuilder(const Mesh &mesh, + const PointAttribute *split_attribute, + WorkData *work_data) const; + // Builds the meshes from the data accumulated in the builders. + StatusOr BuildMeshes(const Mesh &mesh, + WorkData *work_data) const; +}; + +namespace { + +// Helper functions for copying single element from source |mesh| to a target +// builder |b_index| stored in |work_data|. +void AddElementToBuilder( + int b_index, FaceIndex source_i, FaceIndex target_i, const Mesh &mesh, + MeshSplitterInternal::WorkData *work_data); +void AddElementToBuilder( + int b_index, PointIndex source_i, PointIndex target_i, const Mesh &mesh, + MeshSplitterInternal::WorkData *work_data); +} // namespace + +MeshSplitter::MeshSplitter() + : preserve_materials_(false), + remove_unused_material_indices_(true), + preserve_mesh_features_(false) {} + +StatusOr MeshSplitter::SplitMesh( + const Mesh &mesh, uint32_t split_attribute_id) { + if (mesh.num_attributes() <= split_attribute_id) { + return Status(Status::DRACO_ERROR, "Invalid attribute id."); + } + if (mesh.num_faces() == 0) { + return SplitMeshInternal(mesh, split_attribute_id); + } else { + return SplitMeshInternal(mesh, split_attribute_id); + } +} + +template +StatusOr MeshSplitter::SplitMeshInternal( + const Mesh &mesh, int split_attribute_id) { + const PointAttribute *const split_attribute = + mesh.attribute(split_attribute_id); + + // Preserve the split attribute only if it is the material attribute and the + // |preserve_materials_| flag is set. Othwerwise the split attribute will get + // discarded. + // TODO(ostava): We may revisit this later and add an option to always + // preserve the split attribute. + const bool preserve_split_attribute = + preserve_materials_ && + split_attribute->attribute_type() == GeometryAttribute::MATERIAL; + + const int num_out_meshes = split_attribute->size(); + MeshSplitterInternal splitter_internal; + typename MeshSplitterInternal::WorkData work_data; + work_data.num_sub_mesh_elements.resize(num_out_meshes, 0); + work_data.split_by_materials = + (split_attribute->attribute_type() == GeometryAttribute::MATERIAL); + + DRACO_RETURN_IF_ERROR(splitter_internal.InitializeWorkDataNumElements( + mesh, split_attribute_id, &work_data)); + + // Create the sub-meshes. + work_data.builders.resize(num_out_meshes); + // Map between attribute ids of the input and output meshes. + work_data.att_id_map.resize(mesh.num_attributes(), -1); + const int ignored_att_id = + (!preserve_split_attribute ? split_attribute_id : -1); + for (int mi = 0; mi < num_out_meshes; ++mi) { + if (work_data.num_sub_mesh_elements[mi] == 0) { + continue; // Empty mesh, don't initialize it. + } + + const int num_elements = work_data.num_sub_mesh_elements[mi]; + splitter_internal.InitializeBuilder(mi, num_elements, mesh, ignored_att_id, + &work_data); + + // Reset the element counter for the sub-mesh. It will be used to keep track + // of number of elements added to the sub-mesh. + work_data.num_sub_mesh_elements[mi] = 0; + } + + splitter_internal.AddElementsToBuilder(mesh, split_attribute, &work_data); + + DRACO_ASSIGN_OR_RETURN(MeshVector out_meshes, + splitter_internal.BuildMeshes(mesh, &work_data)); + return FinalizeMeshes(mesh, work_data, std::move(out_meshes)); +} + +template <> +Status +MeshSplitterInternal::InitializeWorkDataNumElements( + const Mesh &mesh, int split_attribute_id, WorkData *work_data) const { + const PointAttribute *const split_attribute = + mesh.attribute(split_attribute_id); + // Verify that the attribute values are defined "per-face", i.e., all points + // on a face are always mapped to the same attribute value. + for (FaceIndex fi(0); fi < mesh.num_faces(); ++fi) { + const auto face = mesh.face(fi); + const AttributeValueIndex avi = split_attribute->mapped_index(face[0]); + for (int c = 1; c < 3; ++c) { + if (split_attribute->mapped_index(face[c]) != avi) { + return Status(Status::DRACO_ERROR, + "Attribute values not consistent on a face."); + } + } + work_data->num_sub_mesh_elements[avi.value()] += 1; + } + return OkStatus(); +} + +template <> +Status MeshSplitterInternal::InitializeWorkDataNumElements( + const Mesh &mesh, int split_attribute_id, WorkData *work_data) const { + const PointAttribute *const split_attribute = + mesh.attribute(split_attribute_id); + // Each point can have a different value. Just accumulate the number of points + // with the same attribute value index. + for (PointIndex pi(0); pi < mesh.num_points(); ++pi) { + const AttributeValueIndex avi = split_attribute->mapped_index(pi); + work_data->num_sub_mesh_elements[avi.value()] += 1; + } + return OkStatus(); +} + +template +void MeshSplitterInternal::InitializeBuilder( + int b_index, int num_elements, const Mesh &mesh, int ignored_attribute_id, + WorkData *work_data) const { + work_data->builders[b_index].Start(num_elements); + + // Add all attributes. + for (int ai = 0; ai < mesh.num_attributes(); ++ai) { + if (ai == ignored_attribute_id) { + continue; + } + const GeometryAttribute *const src_att = mesh.attribute(ai); + work_data->att_id_map[ai] = work_data->builders[b_index].AddAttribute( + src_att->attribute_type(), src_att->num_components(), + src_att->data_type()); + } +} + +template <> +void MeshSplitterInternal::AddElementsToBuilder( + const Mesh &mesh, const PointAttribute *split_attribute, + WorkData *work_data) const { + // Go over all faces of the input mesh and add them to the appropriate + // sub-mesh. + for (FaceIndex fi(0); fi < mesh.num_faces(); ++fi) { + const auto face = mesh.face(fi); + const int sub_mesh_id = split_attribute->mapped_index(face[0]).value(); + const FaceIndex target_fi(work_data->num_sub_mesh_elements[sub_mesh_id]++); + AddElementToBuilder(sub_mesh_id, fi, target_fi, mesh, work_data); + } +} + +template <> +void MeshSplitterInternal::AddElementsToBuilder( + const Mesh &mesh, const PointAttribute *split_attribute, + WorkData *work_data) const { + // Go over all points of the input mesh and add them to the appropriate + // sub-mesh. + for (PointIndex pi(0); pi < mesh.num_points(); ++pi) { + const int sub_mesh_id = split_attribute->mapped_index(pi).value(); + const PointIndex target_pi(work_data->num_sub_mesh_elements[sub_mesh_id]++); + AddElementToBuilder(sub_mesh_id, pi, target_pi, mesh, work_data); + } +} + +namespace { + +void AddElementToBuilder( + int b_index, FaceIndex source_i, FaceIndex target_i, const Mesh &mesh, + MeshSplitterInternal::WorkData *work_data) { + const auto &face = mesh.face(source_i); + for (int ai = 0; ai < mesh.num_attributes(); ++ai) { + const PointAttribute *const src_att = mesh.attribute(ai); + const int target_att_id = work_data->att_id_map[ai]; + if (target_att_id == -1) { + continue; + } + // Add value for each corner of the face. + work_data->builders[b_index].SetAttributeValuesForFace( + target_att_id, target_i, src_att->GetAddressOfMappedIndex(face[0]), + src_att->GetAddressOfMappedIndex(face[1]), + src_att->GetAddressOfMappedIndex(face[2])); + } +} + +void AddElementToBuilder( + int b_index, PointIndex source_i, PointIndex target_i, const Mesh &mesh, + MeshSplitterInternal::WorkData *work_data) { + for (int ai = 0; ai < mesh.num_attributes(); ++ai) { + const PointAttribute *const src_att = mesh.attribute(ai); + const int target_att_id = work_data->att_id_map[ai]; + if (target_att_id == -1) { + continue; + } + // Add value for the point |target_i|. + work_data->builders[b_index].SetAttributeValueForPoint( + target_att_id, target_i, src_att->GetAddressOfMappedIndex(source_i)); + } +} + +} // namespace + +template <> +StatusOr +MeshSplitterInternal::BuildMeshes( + const Mesh &mesh, WorkData *work_data) const { + const int num_out_meshes = work_data->builders.size(); + MeshSplitter::MeshVector out_meshes(num_out_meshes); + for (int mi = 0; mi < num_out_meshes; ++mi) { + if (work_data->num_sub_mesh_elements[mi] == 0) { + continue; + } + out_meshes[mi] = work_data->builders[mi].Finalize(); + if (out_meshes[mi] == nullptr) { + continue; + } + } + return out_meshes; +} + +template <> +StatusOr +MeshSplitterInternal::BuildMeshes( + const Mesh &mesh, WorkData *work_data) const { + const int num_out_meshes = work_data->builders.size(); + MeshSplitter::MeshVector out_meshes(num_out_meshes); + for (int mi = 0; mi < num_out_meshes; ++mi) { + if (work_data->num_sub_mesh_elements[mi] == 0) { + continue; + } + // For point clouds, we first build a point cloud and copy it over into + // a draco::Mesh. + std::unique_ptr pc = work_data->builders[mi].Finalize(true); + if (pc == nullptr) { + continue; + } + std::unique_ptr mesh(new Mesh()); + PointCloud *mesh_pc = mesh.get(); + mesh_pc->Copy(*pc); + out_meshes[mi] = std::move(mesh); + } + return out_meshes; +} + +StatusOr MeshSplitter::FinalizeMeshes( + const Mesh &mesh, const WorkData &work_data, MeshVector out_meshes) const { + // Finalize meshes. + const int num_out_meshes = out_meshes.size(); + + // If we are going to preserve mesh features, we will need to update texture + // pointers for all mesh feature textures. Here we store the mapping between + // the old texture pointers and their indices. + std::unordered_map features_texture_to_index_map; + if (preserve_mesh_features_) { + features_texture_to_index_map = + mesh.GetNonMaterialTextureLibrary().ComputeTextureToIndexMap(); + } + + for (int mi = 0; mi < num_out_meshes; ++mi) { + if (out_meshes[mi] == nullptr) { + continue; + } + out_meshes[mi]->SetName(mesh.GetName()); + if (preserve_materials_) { + out_meshes[mi]->GetMaterialLibrary().Copy(mesh.GetMaterialLibrary()); + } + + // Copy metadata of the original mesh to the output meshes. + if (mesh.GetMetadata() != nullptr) { + const GeometryMetadata &metadata = *mesh.GetMetadata(); + out_meshes[mi]->AddMetadata( + std::unique_ptr(new GeometryMetadata(metadata))); + } + + // Copy over attribute unique ids. + for (int att_id = 0; att_id < mesh.num_attributes(); ++att_id) { + const int mapped_att_id = work_data.att_id_map[att_id]; + if (mapped_att_id == -1) { + continue; + } + const PointAttribute *const src_att = mesh.attribute(att_id); + PointAttribute *const dst_att = out_meshes[mi]->attribute(mapped_att_id); + dst_att->set_unique_id(src_att->unique_id()); + } + + // Copy compression settings of the original mesh to the output meshes. + out_meshes[mi]->SetCompressionEnabled(mesh.IsCompressionEnabled()); + out_meshes[mi]->SetCompressionOptions(mesh.GetCompressionOptions()); + + if (preserve_mesh_features_) { + // Copy mesh features from the source |mesh| to the |out_meshes[mi]|. + for (MeshFeaturesIndex mfi(0); mfi < mesh.NumMeshFeatures(); ++mfi) { + if (work_data.split_by_materials) { + // Copy over only those mesh features that were masked to the material + // corresponding to |mi|. + bool is_used = false; + if (mesh.NumMeshFeaturesMaterialMasks(mfi) == 0) { + is_used = true; + } else { + for (int mask_index = 0; + mask_index < mesh.NumMeshFeaturesMaterialMasks(mfi); + ++mask_index) { + if (mesh.GetMeshFeaturesMaterialMask(mfi, mask_index) == mi) { + is_used = true; + break; + } + } + } + if (!is_used) { + // Ignore this mesh features. + continue; + } + } + // Create a copy of source mesh features. + std::unique_ptr mf(new MeshFeatures()); + mf->Copy(mesh.GetMeshFeatures(mfi)); + const MeshFeaturesIndex new_mfi = + out_meshes[mi]->AddMeshFeatures(std::move(mf)); + if (work_data.split_by_materials && !preserve_materials_) { + // If the input |mesh| was split by materials and we didn't preserve + // the materials, all mesh features must be masked to material 0. + out_meshes[mi]->AddMeshFeaturesMaterialMask(new_mfi, 0); + } else { + // Otherwise mesh features use same masking as the source mesh because + // the material attribute is still present in the split meshes. + // Note that this masking can be later changed in + // RemoveUnusedMaterials() call below. + for (int mask_index = 0; + mask_index < mesh.NumMeshFeaturesMaterialMasks(mfi); + ++mask_index) { + out_meshes[mi]->AddMeshFeaturesMaterialMask( + new_mfi, mesh.GetMeshFeaturesMaterialMask(mfi, mask_index)); + } + } + } + + // Copy over all features textures to the split mesh. + out_meshes[mi]->GetNonMaterialTextureLibrary().Copy( + mesh.GetNonMaterialTextureLibrary()); + + // Update mesh features texture pointers to the new library. + for (MeshFeaturesIndex mfi(0); mfi < out_meshes[mi]->NumMeshFeatures(); + ++mfi) { + Mesh::UpdateMeshFeaturesTexturePointer( + features_texture_to_index_map, + &out_meshes[mi]->GetNonMaterialTextureLibrary(), + &out_meshes[mi]->GetMeshFeatures(mfi)); + } + + // This will remove any mesh features that may not be be actually used + // by this |out_meshes[mi]| (e.g. because corresponding material indices + // were not present in this split mesh). This also removes any unused + // features textures from the non-material texture library. + DRACO_RETURN_IF_ERROR( + MeshUtils::RemoveUnusedMeshFeatures(out_meshes[mi].get())); + } + + // Remove unused materials after we remove mesh features because some of + // the mesh features may have referenced old material indices. + if (preserve_materials_) { + out_meshes[mi]->RemoveUnusedMaterials(remove_unused_material_indices_); + } + + // Copy structural metadata from input mesh to each of the output meshes. + out_meshes[mi]->GetStructuralMetadata().Copy(mesh.GetStructuralMetadata()); + } + return std::move(out_meshes); +} + +StatusOr MeshSplitter::SplitMeshToComponents( + const Mesh &mesh, const MeshConnectedComponents &connected_components) { + // Create the sub-meshes. + const int num_out_meshes = connected_components.NumConnectedComponents(); + MeshSplitterInternal splitter_internal; + typename MeshSplitterInternal::WorkData work_data; + work_data.builders.resize(num_out_meshes); + work_data.num_sub_mesh_elements.resize(num_out_meshes, 0); + work_data.att_id_map.resize(mesh.num_attributes(), -1); + for (int mi = 0; mi < num_out_meshes; ++mi) { + const int num_faces = connected_components.NumConnectedComponentFaces(mi); + work_data.num_sub_mesh_elements[mi] = num_faces; + splitter_internal.InitializeBuilder(mi, num_faces, mesh, -1, &work_data); + } + + // Go over all faces of the input mesh and add them to the appropriate + // sub-mesh. + for (int mi = 0; mi < num_out_meshes; ++mi) { + for (int cfi = 0; cfi < connected_components.NumConnectedComponentFaces(mi); + ++cfi) { + const FaceIndex fi( + connected_components.GetConnectedComponent(mi).faces[cfi]); + const FaceIndex target_fi(cfi); + AddElementToBuilder(mi, fi, target_fi, mesh, &work_data); + } + } + DRACO_ASSIGN_OR_RETURN(auto out_meshes, + splitter_internal.BuildMeshes(mesh, &work_data)); + return FinalizeMeshes(mesh, work_data, std::move(out_meshes)); +} + +} // namespace draco +#endif // DRACO_TRANSCODER_SUPPORTED diff --git a/contrib/draco/src/draco/mesh/mesh_splitter.h b/contrib/draco/src/draco/mesh/mesh_splitter.h new file mode 100644 index 000000000..bf5cd9794 --- /dev/null +++ b/contrib/draco/src/draco/mesh/mesh_splitter.h @@ -0,0 +1,109 @@ +// Copyright 2017 The Draco Authors. +// +// 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 DRACO_MESH_MESH_SPLITTER_H_ +#define DRACO_MESH_MESH_SPLITTER_H_ + +#include +#include + +#include "draco/draco_features.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED +#include "draco/core/status_or.h" +#include "draco/mesh/mesh.h" +#include "draco/mesh/mesh_connected_components.h" +#include "draco/mesh/triangle_soup_mesh_builder.h" + +namespace draco { + +// Class that can be used to split a single mesh into multiple sub-meshes +// according to specified criteria. +class MeshSplitter { + public: + typedef std::vector> MeshVector; + MeshSplitter(); + + // Sets a flag that tells the splitter to preserve all materials on the input + // mesh during mesh splitting. When set, the materials used on sub-meshes are + // going to be copied over. Any redundant materials on sub-meshes are going to + // be deleted but material indices may still be preserved depending on the + // SetRemoveUnusedMaterialIndices() flag. + // Default = false. + void SetPreserveMaterials(bool flag) { preserve_materials_ = flag; } + + // Sets a flag that tells the splitter to delete any unused material indices + // on the generated sub-meshes. This option is currently used only when + // SetPreserveMaterials() was set to true. If this option is set to false, the + // material indices of the MATERIAL attribute will be the same as in the + // source mesh. If the flag is true, then the unused material indices will be + // removed and they may no longer correspond to the source mesh. Note that + // when this flag is false, any unused materials would be replaced with empty + // (default) materials. + // Default = true. + void SetRemoveUnusedMaterialIndices(bool flag) { + remove_unused_material_indices_ = flag; + } + + // Sets a flag that tells the splitter to preserve all mesh features on the + // input mesh during mesh splitting. When set, the mesh features used on + // sub-meshes are going to be copied over. Any redundant mesh features on + // sub-meshes are going to be deleted. + // Default = false. + void SetPreserveMeshFeatures(bool flag) { preserve_mesh_features_ = flag; } + + // Splits the input |mesh| according to attribute values stored in the + // specified attribute. If the |mesh| contains faces, the attribute values + // need to be defined per-face, that is, all points attached to a single face + // must share the same attribute value. Meshes without faces are treated as + // point clouds and the attribute values can be defined per-point. Each + // attribute value (AttributeValueIndex) is mapped to a single output mesh. If + // an AttributeValueIndex is unused, no mesh is created for the given value. + StatusOr SplitMesh(const Mesh &mesh, uint32_t split_attribute_id); + + // Splits the input |mesh| into separate components defined in + // |connected_components|. That is, all faces associated with a given + // component index will be stored in the same mesh. The number of generated + // meshes will correspond to |connected_components.NumConnectedComponents()|. + StatusOr SplitMeshToComponents( + const Mesh &mesh, const MeshConnectedComponents &connected_components); + + private: + struct WorkData { + // Map between attribute ids of the input and output meshes. + std::vector att_id_map; + std::vector num_sub_mesh_elements; + bool split_by_materials = false; + }; + + template + StatusOr SplitMeshInternal(const Mesh &mesh, + int split_attribute_id); + + StatusOr FinalizeMeshes(const Mesh &mesh, + const WorkData &work_data, + MeshVector out_meshes) const; + + bool preserve_materials_; + bool remove_unused_material_indices_; + bool preserve_mesh_features_; + + template + friend class MeshSplitterInternal; +}; + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED +#endif // DRACO_MESH_MESH_SPLITTER_H_ diff --git a/contrib/draco/src/draco/javascript/emscripten/draco_animation_encoder_glue_wrapper.cc b/contrib/draco/src/draco/mesh/mesh_splitter_test.cc similarity index 51% rename from contrib/draco/src/draco/javascript/emscripten/draco_animation_encoder_glue_wrapper.cc rename to contrib/draco/src/draco/mesh/mesh_splitter_test.cc index 29e7ed3ba..7432c4736 100644 --- a/contrib/draco/src/draco/javascript/emscripten/draco_animation_encoder_glue_wrapper.cc +++ b/contrib/draco/src/draco/mesh/mesh_splitter_test.cc @@ -12,14 +12,17 @@ // See the License for the specific language governing permissions and // limitations under the License. // -// This file is used by emscripten's WebIDL Binder. -// http://kripken.github.io/emscripten-site/docs/porting/connecting_cpp_and_javascript/WebIDL-Binder.html -#include "draco/attributes/geometry_attribute.h" -#include "draco/attributes/point_attribute.h" -#include "draco/compression/encode.h" -#include "draco/javascript/emscripten/animation_encoder_webidl_wrapper.h" -#include "draco/mesh/mesh.h" -#include "draco/point_cloud/point_cloud.h" +#include "draco/mesh/mesh_splitter.h" -// glue_animation_encoder.cpp is generated by Makefile.emcc build_glue target. -#include "glue_animation_encoder.cpp" +#ifdef DRACO_TRANSCODER_SUPPORTED +#include +#include + +#include "draco/core/draco_test_base.h" +#include "draco/core/draco_test_utils.h" +#include "draco/core/vector_d.h" +#include "draco/io/mesh_io.h" +#include "draco/mesh/mesh_misc_functions.h" + +namespace {} // namespace +#endif // DRACO_TRANSCODER_SUPPORTED diff --git a/contrib/draco/src/draco/mesh/mesh_stripifier.h b/contrib/draco/src/draco/mesh/mesh_stripifier.h index 262e3c792..8e8d8d9f2 100644 --- a/contrib/draco/src/draco/mesh/mesh_stripifier.h +++ b/contrib/draco/src/draco/mesh/mesh_stripifier.h @@ -71,8 +71,6 @@ class MeshStripifier { mesh_ = &mesh; num_strips_ = 0; num_encoded_faces_ = 0; - // TODO(ostava): We may be able to avoid computing the corner table if we - // already have it stored somewhere. corner_table_ = CreateCornerTableFromPositionAttribute(mesh_); if (corner_table_ == nullptr) { return false; diff --git a/contrib/draco/src/draco/mesh/mesh_test.cc b/contrib/draco/src/draco/mesh/mesh_test.cc new file mode 100644 index 000000000..7cc046a7e --- /dev/null +++ b/contrib/draco/src/draco/mesh/mesh_test.cc @@ -0,0 +1,644 @@ +// Copyright 2018 The Draco Authors. +// +// 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 "draco/mesh/mesh.h" + +#include +#include + +#include "draco/core/draco_test_base.h" +#include "draco/core/draco_test_utils.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED +#include "draco/compression/draco_compression_options.h" +#include "draco/material/material_utils.h" +#include "draco/mesh/mesh_are_equivalent.h" +#include "draco/mesh/mesh_features.h" +#include "draco/mesh/mesh_utils.h" +#include "draco/mesh/triangle_soup_mesh_builder.h" +#endif // DRACO_TRANSCODER_SUPPORTED + +namespace { + +#ifdef DRACO_TRANSCODER_SUPPORTED +// Tests naming of a mesh. +TEST(MeshTest, MeshName) { + draco::Mesh mesh; + ASSERT_TRUE(mesh.GetName().empty()); + mesh.SetName("Bob"); + ASSERT_EQ(mesh.GetName(), "Bob"); +} + +// Tests copying of a mesh. +TEST(MeshTest, MeshCopy) { + const std::unique_ptr mesh = + draco::ReadMeshFromTestFile("cube_att.obj"); + ASSERT_NE(mesh, nullptr); + draco::Mesh mesh_copy; + mesh_copy.Copy(*mesh); + draco::MeshAreEquivalent eq; + ASSERT_TRUE(eq(*mesh, mesh_copy)); +} + +// Tests that we can copy a mesh to a different mesh that already contains some +// data. +TEST(MeshTest, MeshCopyToExistingMesh) { + const std::unique_ptr mesh_0 = + draco::ReadMeshFromTestFile("cube_att.obj"); + const std::unique_ptr mesh_1 = + draco::ReadMeshFromTestFile("test_nm.obj"); + ASSERT_NE(mesh_0, nullptr); + ASSERT_NE(mesh_1, nullptr); + draco::MeshAreEquivalent eq; + ASSERT_FALSE(eq(*mesh_0, *mesh_1)); + + mesh_1->Copy(*mesh_0); + ASSERT_TRUE(eq(*mesh_0, *mesh_1)); +} + +// Tests that we can remove unused materials from a mesh. +TEST(MeshTest, RemoveUnusedMaterials) { + // Input mesh has 29 materials defined in the source file but only 7 are + // actually used. + const std::unique_ptr mesh = + draco::ReadMeshFromTestFile("mat_test.obj"); + ASSERT_NE(mesh, nullptr); + + const draco::PointAttribute *const mat_att = + mesh->GetNamedAttribute(draco::GeometryAttribute::MATERIAL); + ASSERT_NE(mat_att, nullptr); + ASSERT_EQ(mat_att->size(), 29); + + ASSERT_EQ(mesh->GetMaterialLibrary().NumMaterials(), mat_att->size()); + + // Get materials on all faces. + std::vector face_materials(mesh->num_faces(), + nullptr); + for (draco::FaceIndex fi(0); fi < mesh->num_faces(); ++fi) { + uint32_t mat_index = 0; + mat_att->GetMappedValue(mesh->face(fi)[0], &mat_index); + face_materials[fi.value()] = + mesh->GetMaterialLibrary().GetMaterial(mat_index); + } + + mesh->RemoveUnusedMaterials(); + + ASSERT_EQ(mesh->GetMaterialLibrary().NumMaterials(), 7); + + // Ensure the material attribute contains material indices in the valid range. + for (draco::AttributeValueIndex avi(0); avi < mat_att->size(); ++avi) { + uint32_t mat_index = 0; + mat_att->GetValue(avi, &mat_index); + ASSERT_LT(mat_index, mesh->GetMaterialLibrary().NumMaterials()); + } + + // Ensure all materials are still the same for all faces. + for (draco::FaceIndex fi(0); fi < mesh->num_faces(); ++fi) { + uint32_t mat_index = 0; + mat_att->GetMappedValue(mesh->face(fi)[0], &mat_index); + ASSERT_EQ(mesh->GetMaterialLibrary().GetMaterial(mat_index), + face_materials[fi.value()]); + } +} + +TEST(MeshTest, RemoveUnusedMaterialsOnPointClud) { + // Input mesh has 29 materials defined in the source file but only 7 are + // actually used. Same as above test but we remove all faces and treat the + // model as a point cloud. + const std::unique_ptr mesh = + draco::ReadMeshFromTestFile("mat_test.obj"); + ASSERT_NE(mesh, nullptr); + + // Make it a point cloud. + mesh->SetNumFaces(0); + + const draco::PointAttribute *const mat_att = + mesh->GetNamedAttribute(draco::GeometryAttribute::MATERIAL); + ASSERT_NE(mat_att, nullptr); + ASSERT_EQ(mat_att->size(), 29); + + ASSERT_EQ(mesh->GetMaterialLibrary().NumMaterials(), mat_att->size()); + + // Get materials on all points. + std::vector point_materials(mesh->num_points(), + nullptr); + for (draco::PointIndex pi(0); pi < mesh->num_points(); ++pi) { + uint32_t mat_index = 0; + mat_att->GetMappedValue(pi, &mat_index); + point_materials[pi.value()] = + mesh->GetMaterialLibrary().GetMaterial(mat_index); + } + + mesh->RemoveUnusedMaterials(); + + ASSERT_EQ(mesh->GetMaterialLibrary().NumMaterials(), 7); + + // Ensure the material attribute contains material indices in the valid range. + for (draco::AttributeValueIndex avi(0); avi < mat_att->size(); ++avi) { + uint32_t mat_index = 0; + mat_att->GetValue(avi, &mat_index); + ASSERT_LT(mat_index, mesh->GetMaterialLibrary().NumMaterials()); + } + + // Ensure all materials are still the same for all points. + for (draco::PointIndex pi(0); pi < mesh->num_points(); ++pi) { + uint32_t mat_index = 0; + mat_att->GetMappedValue(pi, &mat_index); + ASSERT_EQ(mesh->GetMaterialLibrary().GetMaterial(mat_index), + point_materials[pi.value()]); + } +} + +TEST(MeshTest, RemoveUnusedMaterialsNoIndices) { + // The same as above but we actually want to remove only materials and not + // material indices. Therefore we should end up with the same number of + // materials as source but all unused materials should be "default". + const std::unique_ptr mesh = + draco::ReadMeshFromTestFile("mat_test.obj"); + ASSERT_NE(mesh, nullptr); + + const draco::PointAttribute *const mat_att = + mesh->GetNamedAttribute(draco::GeometryAttribute::MATERIAL); + ASSERT_NE(mat_att, nullptr); + ASSERT_EQ(mat_att->size(), 29); + + ASSERT_EQ(mesh->GetMaterialLibrary().NumMaterials(), mat_att->size()); + + // Do not remove unused material indices. + mesh->RemoveUnusedMaterials(false); + + ASSERT_EQ(mesh->GetMaterialLibrary().NumMaterials(), 29); + + // Gether which materials were actually used and check that all remaining + // materials are "default". + std::vector is_mat_used(mesh->GetMaterialLibrary().NumMaterials(), + false); + for (draco::AttributeValueIndex avi(0); avi < mat_att->size(); ++avi) { + uint32_t mat_index = 0; + mat_att->GetValue(avi, &mat_index); + is_mat_used[mat_index] = true; + } + + for (int mi = 0; mi < mesh->GetMaterialLibrary().NumMaterials(); ++mi) { + if (!is_mat_used[mi]) { + ASSERT_TRUE(draco::MaterialUtils::AreMaterialsEquivalent( + *mesh->GetMaterialLibrary().GetMaterial(mi), draco::Material())); + } + } +} + +TEST(MeshTest, TestAddNewAttributeWithConnectivity) { + // Tests that we can add new attributes with arbitrary connectivity to an + // existing mesh. + + // Create a simple quad. See corner indices of the quad on the figure below: + // + // *-------* + // |2\3 5| + // | \ | + // | \ | + // | \ | + // | \4| + // |0 1\| + // *-------* + // + draco::TriangleSoupMeshBuilder mb; + mb.Start(2); + mb.AddAttribute(draco::GeometryAttribute::POSITION, 3, draco::DT_FLOAT32); + mb.SetAttributeValuesForFace( + 0, draco::FaceIndex(0), draco::Vector3f(0, 0, 0).data(), + draco::Vector3f(1, 0, 0).data(), draco::Vector3f(1, 1, 0).data()); + mb.SetAttributeValuesForFace( + 0, draco::FaceIndex(1), draco::Vector3f(1, 1, 0).data(), + draco::Vector3f(1, 0, 0).data(), draco::Vector3f(1, 1, 1).data()); + std::unique_ptr mesh = mb.Finalize(); + ASSERT_NE(mesh, nullptr); + ASSERT_EQ(mesh->num_points(), 4); + ASSERT_EQ(mesh->GetNamedAttribute(draco::GeometryAttribute::POSITION)->size(), + 4); + + // Create a simple attribute that has a constant value on every corner. + std::unique_ptr pa(new draco::PointAttribute()); + pa->Init(draco::GeometryAttribute::GENERIC, 1 /*One components*/, + draco::DT_UINT8, false, 1); + uint8_t val = 10; + pa->SetAttributeValue(draco::AttributeValueIndex(0), &val); + + // Map all corners to the same value. + draco::IndexTypeVector + corner_to_point(6, draco::AttributeValueIndex(0)); + + // Adding this attribute to the mesh should not increase the number of points. + const int new_att_id_0 = + mesh->AddAttributeWithConnectivity(std::move(pa), corner_to_point); + + ASSERT_EQ(mesh->num_attributes(), 2); + ASSERT_EQ(mesh->num_points(), 4); + + const draco::PointAttribute *const new_att_0 = mesh->attribute(new_att_id_0); + ASSERT_NE(new_att_0, nullptr); + + // All points of the mesh should be mapped to the same attribute value. + for (draco::PointIndex pi(0); pi < mesh->num_points(); ++pi) { + uint8_t att_val = 0; + new_att_0->GetMappedValue(pi, &att_val); + ASSERT_EQ(att_val, 10); + } + + // Add a new attribute with two values and different connectivity. + pa = std::unique_ptr(new draco::PointAttribute()); + pa->Init(draco::GeometryAttribute::GENERIC, 1 /*One components*/, + draco::DT_UINT8, false, 2); + val = 11; + pa->SetAttributeValue(draco::AttributeValueIndex(0), &val); + val = 12; + pa->SetAttributeValue(draco::AttributeValueIndex(1), &val); + + // Map all corners to the value index 0 except for corner 1 that is mapped to + // value index 1. This should result in a new point being created on either + // corner 1 or corner 4 (see figure at the beginning of this test). + corner_to_point.assign(6, draco::AttributeValueIndex(0)); + corner_to_point[draco::CornerIndex(1)] = draco::AttributeValueIndex(1); + + const int new_att_id_1 = + mesh->AddAttributeWithConnectivity(std::move(pa), corner_to_point); + + ASSERT_EQ(mesh->num_attributes(), 3); + + // One new point should have been created by adding the new attribute. + ASSERT_EQ(mesh->num_points(), 5); + + const draco::PointAttribute *const new_att_1 = mesh->attribute(new_att_id_1); + ASSERT_NE(new_att_1, nullptr); + ASSERT_TRUE(mesh->CornerToPointId(1) == draco::PointIndex(4) || + mesh->CornerToPointId(4) == draco::PointIndex(4)); + + new_att_1->GetMappedValue(mesh->CornerToPointId(1), &val); + ASSERT_EQ(val, 12); + + new_att_1->GetMappedValue(mesh->CornerToPointId(4), &val); + ASSERT_EQ(val, 11); + + // Ensure the attribute values of the remaining attributes are well defined + // on the new point. + draco::Vector3f pos; + mesh->attribute(0)->GetMappedValue(draco::PointIndex(4), &pos[0]); + ASSERT_EQ(pos, draco::Vector3f(1, 0, 0)); + + new_att_0->GetMappedValue(draco::PointIndex(4), &val); + ASSERT_EQ(val, 10); + + new_att_0->GetMappedValue(mesh->CornerToPointId(1), &val); + ASSERT_EQ(val, 10); + new_att_0->GetMappedValue(mesh->CornerToPointId(4), &val); + ASSERT_EQ(val, 10); +} + +TEST(MeshTest, TestAddNewAttributeWithConnectivityWithIsolatedVertices) { + // Tests that we can add a new attribute with connectivity to a mesh that + // contains isolated vertices. + const std::unique_ptr mesh = + draco::ReadMeshFromTestFile("isolated_vertices.ply"); + ASSERT_NE(mesh, nullptr); + const draco::PointAttribute *const pos_att = + mesh->GetNamedAttribute(draco::GeometryAttribute::POSITION); + ASSERT_NE(pos_att, nullptr); + ASSERT_TRUE(pos_att->is_mapping_identity()); + ASSERT_EQ(pos_att->size(), 5); + ASSERT_EQ(mesh->num_points(), 5); + ASSERT_EQ(mesh->num_faces(), 2); + + // Add a new attribute with two values (one for each face). + auto pa = std::unique_ptr(new draco::PointAttribute()); + pa->Init(draco::GeometryAttribute::GENERIC, 1 /*One component*/, + draco::DT_UINT8, false, 2); + uint8_t val = 11; + pa->SetAttributeValue(draco::AttributeValueIndex(0), &val); + val = 12; + pa->SetAttributeValue(draco::AttributeValueIndex(1), &val); + + draco::IndexTypeVector + corner_to_point(6, draco::AttributeValueIndex(0)); + // All corners on the second face are mapped to the value 1. + for (draco::CornerIndex ci(3); ci < 6; ++ci) { + corner_to_point[ci] = draco::AttributeValueIndex(1); + } + + const draco::PointAttribute *const pa_raw = pa.get(); + mesh->AddAttributeWithConnectivity(std::move(pa), corner_to_point); + + // Two new point should have been added. + ASSERT_EQ(mesh->num_points(), 7); + + for (draco::PointIndex pi(0); pi < mesh->num_points(); ++pi) { + ASSERT_NE(pa_raw->mapped_index(pi), draco::kInvalidAttributeValueIndex); + ASSERT_NE(pos_att->mapped_index(pi), draco::kInvalidAttributeValueIndex); + } +} + +TEST(MeshTest, TestAddPerVertexAttribute) { + std::unique_ptr mesh = + draco::ReadMeshFromTestFile("cube_att.obj"); + + ASSERT_NE(mesh, nullptr); + const draco::PointAttribute *const pos_att = + mesh->GetNamedAttribute(draco::GeometryAttribute::POSITION); + ASSERT_NE(pos_att, nullptr); + + // The input mesh should have 8 spatial vertices. + ASSERT_EQ(pos_att->size(), 8); + + // Add a new scalar attribute where each value corresponds to the position + // value index (vertex). + std::unique_ptr pa(new draco::PointAttribute()); + pa->Init(draco::GeometryAttribute::GENERIC, /* scalar */ 1, draco::DT_FLOAT32, + false, /* one value per position value */ 8); + + // Set the value for the new attribute. + for (draco::AttributeValueIndex avi(0); avi < 8; ++avi) { + const float att_value = avi.value(); + pa->SetAttributeValue(avi, &att_value); + } + + // Add the attribute to the existing mesh. + const int new_att_id = mesh->AddPerVertexAttribute(std::move(pa)); + ASSERT_NE(new_att_id, -1); + + // Make sure all the attribute values are set correctly for every point of the + // mesh. + for (draco::PointIndex pi(0); pi < mesh->num_points(); ++pi) { + const draco::AttributeValueIndex pos_avi = pos_att->mapped_index(pi); + const draco::AttributeValueIndex new_att_avi = + mesh->attribute(new_att_id)->mapped_index(pi); + ASSERT_EQ(pos_avi, new_att_avi); + + float new_att_value; + mesh->attribute(new_att_id)->GetValue(new_att_avi, &new_att_value); + ASSERT_EQ(new_att_value, new_att_avi.value()); + } +} + +TEST(MeshTest, TestRemovalOfIsolatedPoints) { + const std::unique_ptr mesh = + draco::ReadMeshFromTestFile("isolated_vertices.ply"); + + draco::Mesh mesh_copy; + mesh_copy.Copy(*mesh); + + ASSERT_EQ(mesh_copy.num_points(), 5); + mesh_copy.RemoveIsolatedPoints(); + ASSERT_EQ(mesh_copy.num_points(), 4); + + draco::MeshAreEquivalent eq; + ASSERT_TRUE(eq(*mesh, mesh_copy)); +} + +TEST(MeshTest, TestCompressionSettings) { + // Tests compression settings of a mesh. + const std::unique_ptr mesh = + draco::ReadMeshFromTestFile("cube_att.obj"); + ASSERT_NE(mesh, nullptr); + + // Check that compression is disabled and compression settings are default. + ASSERT_FALSE(mesh->IsCompressionEnabled()); + const draco::DracoCompressionOptions default_compression_options; + ASSERT_EQ(mesh->GetCompressionOptions(), default_compression_options); + + // Check that compression options can be set without enabling compression. + draco::DracoCompressionOptions compression_options; + compression_options.quantization_bits_normal = 12; + mesh->SetCompressionOptions(compression_options); + ASSERT_EQ(mesh->GetCompressionOptions(), compression_options); + ASSERT_FALSE(mesh->IsCompressionEnabled()); + + // Check that compression can be enabled. + mesh->SetCompressionEnabled(true); + ASSERT_TRUE(mesh->IsCompressionEnabled()); + + // Check that individual compression options can be updated. + mesh->GetCompressionOptions().compression_level++; + mesh->GetCompressionOptions().compression_level--; + + // Check that compression settings can be copied. + draco::Mesh mesh_copy; + mesh_copy.Copy(*mesh); + ASSERT_TRUE(mesh_copy.IsCompressionEnabled()); + ASSERT_EQ(mesh_copy.GetCompressionOptions(), compression_options); +} + +// Tests adding and removing of mesh features to a mesh. +TEST(MeshTest, TestMeshFeatures) { + // Create a mesh with two feature ID sets. + draco::Mesh mesh; + ASSERT_EQ(mesh.NumMeshFeatures(), 0); + std::unique_ptr oceans(new draco::MeshFeatures()); + std::unique_ptr continents(new draco::MeshFeatures()); + oceans->SetLabel("oceans"); + continents->SetLabel("continents"); + const draco::MeshFeaturesIndex index_0 = + mesh.AddMeshFeatures(std::move(oceans)); + const draco::MeshFeaturesIndex index_1 = + mesh.AddMeshFeatures(std::move(continents)); + ASSERT_EQ(index_0, draco::MeshFeaturesIndex(0)); + ASSERT_EQ(index_1, draco::MeshFeaturesIndex(1)); + + // Check that the mesh has two feature ID sets. + ASSERT_EQ(mesh.NumMeshFeatures(), 2); + ASSERT_EQ(mesh.GetMeshFeatures(index_0).GetLabel(), "oceans"); + ASSERT_EQ(mesh.GetMeshFeatures(index_1).GetLabel(), "continents"); + + // Remove one feature ID set and check the remaining feature ID set. + mesh.RemoveMeshFeatures(draco::MeshFeaturesIndex(1)); + ASSERT_EQ(mesh.NumMeshFeatures(), 1); + ASSERT_EQ(mesh.GetMeshFeatures(draco::MeshFeaturesIndex(0)).GetLabel(), + "oceans"); + + // Remove the remaining feature ID set and check that no sets remain. + mesh.RemoveMeshFeatures(draco::MeshFeaturesIndex(0)); + ASSERT_EQ(mesh.NumMeshFeatures(), 0); +} + +// Tests copying of a mesh with feature ID sets. +TEST(MeshTest, MeshCopyWithMeshFeatures) { + const std::unique_ptr mesh = + draco::ReadMeshFromTestFile("cube_att.obj"); + ASSERT_NE(mesh, nullptr); + + // Add two textures to the non-material texture library of the mesh. + std::unique_ptr texture0(new draco::Texture()); + std::unique_ptr texture1(new draco::Texture()); + texture0->Resize(128, 128); + texture1->Resize(256, 256); + texture0->FillImage(draco::RGBA(100, 0, 0, 0)); + texture1->FillImage(draco::RGBA(200, 0, 0, 0)); + draco::TextureLibrary &library = mesh->GetNonMaterialTextureLibrary(); + library.PushTexture(std::move(texture0)); + library.PushTexture(std::move(texture1)); + + // Add feature ID set referring to an attribute. + const draco::MeshFeaturesIndex index_0 = mesh->AddMeshFeatures( + std::unique_ptr(new draco::MeshFeatures())); + mesh->GetMeshFeatures(index_0).SetLabel("planet"); + mesh->GetMeshFeatures(index_0).SetFeatureCount(2); + mesh->GetMeshFeatures(index_0).SetAttributeIndex(1); + + // Add feature ID set referring to texture at index 0. + const draco::MeshFeaturesIndex index_1 = mesh->AddMeshFeatures( + std::unique_ptr(new draco::MeshFeatures())); + mesh->GetMeshFeatures(index_1).SetLabel("continents"); + mesh->GetMeshFeatures(index_1).SetFeatureCount(7); + mesh->GetMeshFeatures(index_1).GetTextureMap().SetTexture( + library.GetTexture(0)); + + // Add feature ID set referring to a texture at index 1. + const draco::MeshFeaturesIndex index_2 = mesh->AddMeshFeatures( + std::unique_ptr(new draco::MeshFeatures())); + mesh->GetMeshFeatures(index_2).SetLabel("oceans"); + mesh->GetMeshFeatures(index_2).SetFeatureCount(5); + mesh->GetMeshFeatures(index_2).GetTextureMap().SetTexture( + library.GetTexture(1)); + + // Check mesh feature ID set texture pointers. + ASSERT_EQ(library.NumTextures(), 2); + ASSERT_EQ(mesh->NumMeshFeatures(), 3); + ASSERT_EQ(mesh->GetMeshFeatures(index_0).GetTextureMap().texture(), nullptr); + ASSERT_EQ(mesh->GetMeshFeatures(index_1).GetTextureMap().texture(), + library.GetTexture(0)); + ASSERT_EQ(mesh->GetMeshFeatures(index_2).GetTextureMap().texture(), + library.GetTexture(1)); + + // Copy the mesh. + draco::Mesh mesh_copy; + mesh_copy.Copy(*mesh); + + // Check that the meshes are equivalent. + draco::MeshAreEquivalent eq; + ASSERT_TRUE(eq(*mesh, mesh_copy)); + + // Also check that the texture pointers have been updated correctly. + const draco::TextureLibrary &library_copy = + mesh_copy.GetNonMaterialTextureLibrary(); + ASSERT_EQ(library_copy.NumTextures(), 2); + ASSERT_EQ(mesh_copy.NumMeshFeatures(), 3); + ASSERT_EQ(mesh_copy.GetMeshFeatures(index_0).GetTextureMap().texture(), + nullptr); + ASSERT_EQ(mesh_copy.GetMeshFeatures(index_1).GetTextureMap().texture(), + library_copy.GetTexture(0)); + ASSERT_EQ(mesh_copy.GetMeshFeatures(index_2).GetTextureMap().texture(), + library_copy.GetTexture(1)); +} + +// Tests copying of a mesh with structural metadata. +TEST(MeshTest, TestCopyWithStructuralMetadata) { + const std::unique_ptr mesh = + draco::ReadMeshFromTestFile("cube_att.obj"); + ASSERT_NE(mesh, nullptr); + + // Add structural metadata to the mesh. + draco::PropertyTable::Schema schema; + schema.json.SetString("Data"); + mesh->GetStructuralMetadata().SetPropertyTableSchema(schema); + + // Copy the mesh. + draco::Mesh copy; + copy.Copy(*mesh); + + // Check that the structural metadata has been copied. + ASSERT_EQ( + copy.GetStructuralMetadata().GetPropertyTableSchema().json.GetString(), + "Data"); +} + +// Tests removing of unused materials for a mesh with mesh features. +TEST(MeshTest, RemoveUnusedMaterialsWithMeshFeatures) { + const std::unique_ptr mesh = + draco::ReadMeshFromTestFile("BoxesMeta/glTF/BoxesMeta.gltf"); + ASSERT_NE(mesh, nullptr); + + // Input has five mesh features, two associated with material 0 and three with + // material 1. + ASSERT_EQ(mesh->NumMeshFeatures(), 5); + ASSERT_EQ(mesh->GetMeshFeaturesMaterialMask(draco::MeshFeaturesIndex(0), 0), + 0); + ASSERT_EQ(mesh->GetMeshFeaturesMaterialMask(draco::MeshFeaturesIndex(1), 0), + 0); + ASSERT_EQ(mesh->GetMeshFeaturesMaterialMask(draco::MeshFeaturesIndex(2), 0), + 1); + ASSERT_EQ(mesh->GetMeshFeaturesMaterialMask(draco::MeshFeaturesIndex(3), 0), + 1); + ASSERT_EQ(mesh->GetMeshFeaturesMaterialMask(draco::MeshFeaturesIndex(4), 0), + 1); + + // Remove material 0. + draco::PointAttribute *mat_att = mesh->attribute( + mesh->GetNamedAttributeId(draco::GeometryAttribute::MATERIAL)); + // Map mat value 0 to 1. + uint32_t new_mat_index = 1; + mat_att->SetAttributeValue(draco::AttributeValueIndex(0), &new_mat_index); + + // This should not do anything because we still have the material 0 referenced + // by mesh features 0 and 1. + mesh->RemoveUnusedMaterials(); + + ASSERT_EQ(mesh->GetMaterialLibrary().NumMaterials(), 2); + ASSERT_EQ(mesh->NumMeshFeatures(), 5); + + // Now remove unused mesh features (should be 0 and 1). + DRACO_ASSERT_OK(draco::MeshUtils::RemoveUnusedMeshFeatures(mesh.get())); + + ASSERT_EQ(mesh->NumMeshFeatures(), 3); + // All remaining mesh features should be still mapped to material 1. + ASSERT_EQ(mesh->GetMeshFeaturesMaterialMask(draco::MeshFeaturesIndex(0), 0), + 1); + ASSERT_EQ(mesh->GetMeshFeaturesMaterialMask(draco::MeshFeaturesIndex(1), 0), + 1); + ASSERT_EQ(mesh->GetMeshFeaturesMaterialMask(draco::MeshFeaturesIndex(2), 0), + 1); + + // Now remove the unused materials (0). + mesh->RemoveUnusedMaterials(); + + // Only one material should be remaining and all the mesh features should now + // be mapped to material 0. + ASSERT_EQ(mesh->GetMaterialLibrary().NumMaterials(), 1); + ASSERT_EQ(mesh->GetMeshFeaturesMaterialMask(draco::MeshFeaturesIndex(0), 0), + 0); + ASSERT_EQ(mesh->GetMeshFeaturesMaterialMask(draco::MeshFeaturesIndex(1), 0), + 0); + ASSERT_EQ(mesh->GetMeshFeaturesMaterialMask(draco::MeshFeaturesIndex(2), 0), + 0); +} +#endif // DRACO_TRANSCODER_SUPPORTED + +// Test bounding box. +TEST(MeshTest, TestMeshBoundingBox) { + const draco::Vector3f max_pt(1, 1, 1); + const draco::Vector3f min_pt(0, 0, 0); + + const std::unique_ptr mesh = + draco::ReadMeshFromTestFile("cube_att.obj"); + ASSERT_NE(mesh, nullptr) << "Failed in Loading: " + << "cube_att.obj"; + const draco::BoundingBox bounding_box = mesh->ComputeBoundingBox(); + + EXPECT_EQ(max_pt[0], bounding_box.GetMaxPoint()[0]); + EXPECT_EQ(max_pt[1], bounding_box.GetMaxPoint()[1]); + EXPECT_EQ(max_pt[2], bounding_box.GetMaxPoint()[2]); + + EXPECT_EQ(min_pt[0], bounding_box.GetMinPoint()[0]); + EXPECT_EQ(min_pt[1], bounding_box.GetMinPoint()[1]); + EXPECT_EQ(min_pt[2], bounding_box.GetMinPoint()[2]); +} + +} // namespace diff --git a/contrib/draco/src/draco/mesh/mesh_utils.cc b/contrib/draco/src/draco/mesh/mesh_utils.cc new file mode 100644 index 000000000..0fbe366c1 --- /dev/null +++ b/contrib/draco/src/draco/mesh/mesh_utils.cc @@ -0,0 +1,492 @@ +// Copyright 2019 The Draco Authors. +// +// 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 "draco/mesh/mesh_utils.h" + +#include +#include +#include + +#ifdef DRACO_TRANSCODER_SUPPORTED +#include "draco/attributes/attribute_quantization_transform.h" +#include "draco/core/quantization_utils.h" + +namespace draco { + +void MeshUtils::TransformMesh(const Eigen::Matrix4d &transform, Mesh *mesh) { + // Transform positions. + PointAttribute *pos_att = + mesh->attribute(mesh->GetNamedAttributeId(GeometryAttribute::POSITION)); + for (AttributeValueIndex avi(0); avi < pos_att->size(); ++avi) { + Vector3f pos_val; + pos_att->GetValue(avi, &pos_val[0]); + Eigen::Vector4d transformed_val(pos_val[0], pos_val[1], pos_val[2], 1); + transformed_val = transform * transformed_val; + pos_val = + Vector3f(transformed_val[0], transformed_val[1], transformed_val[2]); + pos_att->SetAttributeValue(avi, &pos_val[0]); + } + + // Transform normals and tangents. + PointAttribute *normal_att = nullptr; + PointAttribute *tangent_att = nullptr; + if (mesh->NumNamedAttributes(GeometryAttribute::NORMAL) > 0) { + normal_att = + mesh->attribute(mesh->GetNamedAttributeId(GeometryAttribute::NORMAL)); + } + if (mesh->NumNamedAttributes(GeometryAttribute::TANGENT) > 0) { + tangent_att = + mesh->attribute(mesh->GetNamedAttributeId(GeometryAttribute::TANGENT)); + } + + if (normal_att || tangent_att) { + // Use inverse-transpose matrix to transform normals and tangents. + Eigen::Matrix3d it_transform = transform.block<3, 3>(0, 0); + + it_transform = it_transform.inverse().transpose(); + + if (normal_att) { + TransformNormalizedAttribute(it_transform, normal_att); + } + if (tangent_att) { + TransformNormalizedAttribute(it_transform, tangent_att); + } + } +} + +namespace { + +// Merges entries from |src_metadata| to |dst_metadata|. Any metadata entries +// with the same names are left unchanged. +void MergeMetadataInternal(const Metadata &src_metadata, + Metadata *dst_metadata) { + const auto &src_entries = src_metadata.entries(); + const auto &dst_entries = dst_metadata->entries(); + for (const auto &it : src_entries) { + if (dst_entries.find(it.first) != dst_entries.end()) { + // Source entry already exists in the target metadata. + continue; + } + // Copy over the entry (entries don't store the data type so binary copy + // is ok). + dst_metadata->AddEntryBinary(it.first, it.second.data()); + } + + // Merge any sub-metadata. + const auto &src_sub_metadata = src_metadata.sub_metadatas(); + const auto &dst_sub_metadata = dst_metadata->sub_metadatas(); + for (const auto &it : src_sub_metadata) { + if (dst_sub_metadata.find(it.first) == dst_sub_metadata.end()) { + // Source sub-metadata doesn't exists in the target metadata, copy it + // over. + std::unique_ptr sub_metadata(new Metadata(*it.second)); + dst_metadata->AddSubMetadata(it.first, std::move(sub_metadata)); + continue; + } + // Merge entries on the sub-metadata. + MergeMetadataInternal(*it.second, dst_metadata->sub_metadata(it.first)); + } +} + +} // namespace + +void MeshUtils::MergeMetadata(const Mesh &src_mesh, Mesh *dst_mesh) { + const auto *src_metadata = src_mesh.GetMetadata(); + if (src_metadata == nullptr) { + return; // Nothing to merge. + } + if (dst_mesh->GetMetadata() == nullptr) { + // Create new metadata for the |dst_mesh|. We do not copy the metadata + // directly because some of the underlying attribute metadata may need to + // be remapped to the format used by |dst_mesh| (e.g. unique ids of the + // attributes may have changed or some attributes may be missing on the + // |dst_mesh|). + std::unique_ptr new_metadata(new GeometryMetadata()); + dst_mesh->AddMetadata(std::move(new_metadata)); + } + auto *dst_metadata = dst_mesh->metadata(); + + // First go over all entries of the geometry part of |src_metadata|. + MergeMetadataInternal(*src_metadata, dst_metadata); + + // Go over attribute metadata. Merges only metadata for attributes that exist + // both on the source and target meshes. Attribute unique ids are remapped + // if needed. + for (int att_type_i = 0; + att_type_i < GeometryAttribute::NAMED_ATTRIBUTES_COUNT; ++att_type_i) { + const GeometryAttribute::Type att_type = + static_cast(att_type_i); + // TODO(ostava): Handle case when the number of attributes of a given type + // does not match. + if (src_mesh.NumNamedAttributes(att_type) != + dst_mesh->NumNamedAttributes(att_type)) { + continue; + } + for (int j = 0; j < src_mesh.NumNamedAttributes(att_type); ++j) { + // First check if we have a metadata for this attribute. + const PointAttribute *const src_att = + src_mesh.GetNamedAttribute(att_type, j); + const auto *src_metadata = + src_mesh.GetMetadata()->GetAttributeMetadataByUniqueId( + src_att->unique_id()); + if (src_metadata == nullptr) { + // No metadata at the source, ignore the attribute. + continue; + } + // Find target attribute corresponding to the source. + const PointAttribute *const dst_att = + dst_mesh->GetNamedAttribute(att_type, j); + if (dst_att == nullptr) { + // No corresponding attribute found, ignore the source metadata. + continue; + } + auto *dst_metadata = + dst_mesh->metadata()->attribute_metadata(dst_att->unique_id()); + if (dst_metadata == nullptr) { + // Copy over the metadata (with remapped attribute unique id). + std::unique_ptr new_metadata( + new AttributeMetadata(*src_metadata)); + new_metadata->set_att_unique_id(dst_att->unique_id()); + dst_mesh->metadata()->AddAttributeMetadata(std::move(new_metadata)); + continue; + } + // Merge metadata entries. + MergeMetadataInternal(*src_metadata, dst_metadata); + } + } +} + +Status MeshUtils::RemoveUnusedMeshFeatures(Mesh *mesh) { + // Unused mesh features are features that are not used by any face / vertex + // of the |mesh|. Currently, each mesh feature can be "masked" for specific + // materials, in which case we need to check whether the mask materials + // are present in the |mesh|. If not, we can remove the mesh features from the + // mesh. + const PointAttribute *const mat_att = + mesh->GetNamedAttribute(GeometryAttribute::MATERIAL); + // Find which materials are used. + std::unordered_set used_materials; + if (mat_att == nullptr) { + // Only material with index 0 is assumed to be used. + used_materials.insert(0); + } else { + for (AttributeValueIndex avi(0); avi < mat_att->size(); ++avi) { + uint32_t mat_index = 0; + mat_att->GetValue(avi, &mat_index); + used_materials.insert(mat_index); + } + } + + std::vector unused_mesh_features; + for (MeshFeaturesIndex mfi(0); mfi < mesh->NumMeshFeatures(); ++mfi) { + bool is_used = false; + if (mesh->NumMeshFeaturesMaterialMasks(mfi) == 0) { + is_used = true; + } else { + for (int mask_i = 0; mask_i < mesh->NumMeshFeaturesMaterialMasks(mfi); + ++mask_i) { + const int material_index = + mesh->GetMeshFeaturesMaterialMask(mfi, mask_i); + if (used_materials.count(material_index)) { + is_used = true; + break; + } + } + } + if (!is_used) { + unused_mesh_features.push_back(mfi); + } + } + + // Remove the unused mesh features (from back). + for (auto it = unused_mesh_features.rbegin(); + it != unused_mesh_features.rend(); ++it) { + const MeshFeaturesIndex mfi = *it; + mesh->RemoveMeshFeatures(mfi); + } + + // Remove all features textures that are not used anymore. + + // First find which textures are referenced by the mesh features. + std::unordered_set used_textures; + for (MeshFeaturesIndex mfi(0); mfi < mesh->NumMeshFeatures(); ++mfi) { + const Texture *const texture = + mesh->GetMeshFeatures(mfi).GetTextureMap().texture(); + if (texture) { + used_textures.insert(texture); + } + } + + if (!used_textures.empty() && + mesh->GetNonMaterialTextureLibrary().NumTextures() == 0) { + return ErrorStatus( + "Trying to remove mesh features textures that are not owned by the " + "mesh."); + } + + // Remove all unreferenced textures from the non-material texture library. + for (int ti = mesh->GetNonMaterialTextureLibrary().NumTextures() - 1; ti >= 0; + --ti) { + const Texture *const texture = + mesh->GetNonMaterialTextureLibrary().GetTexture(ti); + if (used_textures.count(texture) == 0) { + mesh->GetNonMaterialTextureLibrary().RemoveTexture(ti); + } + } + return OkStatus(); +} + +bool MeshUtils::FlipTextureUvValues(bool flip_u, bool flip_v, + PointAttribute *att) { + if (att->attribute_type() != GeometryAttribute::TEX_COORD) { + return false; + } + if (att->data_type() != DataType::DT_FLOAT32) { + return false; + } + if (att->num_components() != 2) { + return false; + } + + std::array value; + for (AttributeValueIndex avi(0); avi < att->size(); ++avi) { + if (!att->GetValue(avi, &value)) { + return false; + } + if (flip_u) { + value[0] = 1.0 - value[0]; + } + if (flip_v) { + value[1] = 1.0 - value[1]; + } + att->SetAttributeValue(avi, value.data()); + } + return true; +} + +// TODO(fgalligan): Change att_id to be of type const PointAttribute &. +int MeshUtils::CountDegenerateFaces(const Mesh &mesh, int att_id) { + const PointAttribute *const att = mesh.attribute(att_id); + if (att == nullptr) { + return -1; + } + const int num_components = att->num_components(); + switch (num_components) { + case 2: + return MeshUtils::CountDegenerateFaces(mesh, *att); + case 3: + return MeshUtils::CountDegenerateFaces(mesh, *att); + case 4: + return MeshUtils::CountDegenerateFaces(mesh, *att); + default: + break; + } + return -1; +} + +StatusOr MeshUtils::FindLowestTextureQuantization( + const Mesh &mesh, const PointAttribute &pos_att, int pos_quantization_bits, + const PointAttribute &tex_att, int tex_target_quantization_bits) { + if (tex_target_quantization_bits < 0 || tex_target_quantization_bits >= 30) { + return Status(Status::DRACO_ERROR, + "Target texture quantization is out of range."); + } + // The target quantization is no quantization, so return 0. + if (tex_target_quantization_bits == 0) { + return 0; + } + const uint32_t pos_max_quantized_value = (1 << (pos_quantization_bits)) - 1; + AttributeQuantizationTransform pos_transform; + if (!pos_transform.ComputeParameters(pos_att, pos_quantization_bits)) { + return Status(Status::DRACO_ERROR, + "Failed computing position quantization parameters."); + } + + // Get all degenerate faces for positions. If the model already has + // degenerate faces for positions, but valid faces for texture coordinates, + // those will not count as new degenerate faces for texture coordinates, + // because the faces would not have been rendered anyway. + const std::vector pos_degenerate_faces_sorted = + MeshUtils::ListDegenerateQuantizedFaces( + mesh, pos_att, pos_transform.range(), pos_max_quantized_value, false); + + // Initialize return value to zero signifying that it could not find a + // quantization that did not cause any new degenerate faces. + int lowest_quantization_bits = 0; + int min_quantization_bits = tex_target_quantization_bits; + int max_quantization_bits = 29; + while (true) { + const int curr_quantization_bits = + min_quantization_bits + + (max_quantization_bits - min_quantization_bits) / 2; + AttributeQuantizationTransform transform; + if (!transform.ComputeParameters(tex_att, curr_quantization_bits)) { + return Status(Status::DRACO_ERROR, + "Failed computing texture quantization parameters."); + } + + const uint32_t max_quantized_value = (1 << (curr_quantization_bits)) - 1; + + // Get only new degenerate faces for texture coordinates. If the model + // already has degenerate faces for texture coordinates, we don't want to + // take into account those faces in the source, because those faces would + // not have been rendered correctly anyway. + const std::vector tex_degenerate_faces_sorted = + MeshUtils::ListDegenerateQuantizedFaces( + mesh, tex_att, transform.range(), max_quantized_value, true); + + if (tex_degenerate_faces_sorted.size() <= + pos_degenerate_faces_sorted.size()) { + if (std::includes(pos_degenerate_faces_sorted.begin(), + pos_degenerate_faces_sorted.end(), + tex_degenerate_faces_sorted.begin(), + tex_degenerate_faces_sorted.end())) { + // Degenerate texture coordinate faces are a subset of position + // degenerate faces. + lowest_quantization_bits = curr_quantization_bits; + } + } + + if (lowest_quantization_bits == curr_quantization_bits) { + // The lowest quantization is the current quantization, see if lower + // quantization is possible. + max_quantization_bits = curr_quantization_bits - 1; + } else { + min_quantization_bits = curr_quantization_bits + 1; + } + if (min_quantization_bits > max_quantization_bits) { + break; + } + } + return lowest_quantization_bits; +} + +void MeshUtils::TransformNormalizedAttribute(const Eigen::Matrix3d &transform, + PointAttribute *att) { + for (AttributeValueIndex avi(0); avi < att->size(); ++avi) { + // Store up to 4 component values. + Vector4f val(0, 0, 0, 1); + att->GetValue(avi, &val); + // Ignore the last component during transformation. + Eigen::Vector3d transformed_val(val[0], val[1], val[2]); + transformed_val = transform * transformed_val; + transformed_val = transformed_val.normalized(); + // Last component is passed to the transformed value. + val = Vector4f(transformed_val[0], transformed_val[1], transformed_val[2], + val[3]); + + // Set the value to the attribute. Note that in case the attribute is using + // fewer than 4 components, the 4th component is going to be ignored. + att->SetAttributeValue(avi, &val[0]); + } +} + +template +int MeshUtils::CountDegenerateFaces(const Mesh &mesh, + const PointAttribute &att) { + if (att.data_type() != DataType::DT_FLOAT32) { + return -1; + } + std::array values; + int degenerate_values = 0; + for (FaceIndex fi(0); fi < mesh.num_faces(); ++fi) { + const auto &face = mesh.face(fi); + for (int c = 0; c < 3; ++c) { + att.GetMappedValue(face[c], &values[c][0]); + } + if (values[0] == values[1] || values[0] == values[2] || + values[1] == values[2]) { + degenerate_values++; + } + } + return degenerate_values; +} + +std::vector MeshUtils::ListDegenerateQuantizedFaces( + const Mesh &mesh, const PointAttribute &att, float range, + uint32_t max_quantized_value, bool quantized_degenerate_only) { + const int num_components = att.num_components(); + switch (num_components) { + case 2: + return MeshUtils::ListDegenerateQuantizedFaces>( + mesh, att, range, max_quantized_value, quantized_degenerate_only); + case 3: + return MeshUtils::ListDegenerateQuantizedFaces>( + mesh, att, range, max_quantized_value, quantized_degenerate_only); + case 4: + return MeshUtils::ListDegenerateQuantizedFaces>( + mesh, att, range, max_quantized_value, quantized_degenerate_only); + default: + break; + } + return std::vector(); +} + +template +std::vector MeshUtils::ListDegenerateQuantizedFaces( + const Mesh &mesh, const PointAttribute &att, float range, + uint32_t max_quantized_value, bool quantized_degenerate_only) { + std::array values; + std::array quantized_values; + + Quantizer quantizer; + quantizer.Init(range, max_quantized_value); + std::vector degenerate_faces; + + for (FaceIndex fi(0); fi < mesh.num_faces(); ++fi) { + const auto &face = mesh.face(fi); + for (int c = 0; c < 3; ++c) { + att.GetMappedValue(face[c], &values[c][0]); + for (int i = 0; i < att_components_t::dimension; ++i) { + quantized_values[c][i] = quantizer.QuantizeFloat(values[c][i]); + } + } + + if (quantized_degenerate_only && + (values[0] == values[1] || values[0] == values[2] || + values[1] == values[2])) { + continue; + } + if (quantized_values[0] == quantized_values[1] || + quantized_values[0] == quantized_values[2] || + quantized_values[1] == quantized_values[2]) { + degenerate_faces.push_back(fi); + } + } + return degenerate_faces; +} + +bool MeshUtils::HasAutoGeneratedTangents(const Mesh &mesh) { + const int tangent_att_id = + mesh.GetNamedAttributeId(draco::GeometryAttribute::TANGENT); + if (tangent_att_id == -1) { + return false; + } + const auto metadata = mesh.GetAttributeMetadataByAttributeId(tangent_att_id); + if (metadata) { + int is_auto_generated = 0; + if (metadata->GetEntryInt("auto_generated", &is_auto_generated) && + is_auto_generated == 1) { + return true; + } + } + return false; +} + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED diff --git a/contrib/draco/src/draco/mesh/mesh_utils.h b/contrib/draco/src/draco/mesh/mesh_utils.h new file mode 100644 index 000000000..e17dfd8ed --- /dev/null +++ b/contrib/draco/src/draco/mesh/mesh_utils.h @@ -0,0 +1,102 @@ +// Copyright 2019 The Draco Authors. +// +// 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 DRACO_MESH_MESH_UTILS_H_ +#define DRACO_MESH_MESH_UTILS_H_ + +#include "draco/draco_features.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED +#include "Eigen/Geometry" +#include "draco/core/status_or.h" +#include "draco/mesh/mesh.h" + +namespace draco { + +// Helper class containing various utilities operating on draco::Mesh. +// TODO(ostava): Move scattered functions in this folder here (e.g. corner table +// construction). +class MeshUtils { + public: + // Transforms |mesh| using the |transform| matrix. The mesh is transformed + // in-place. + static void TransformMesh(const Eigen::Matrix4d &transform, Mesh *mesh); + + // Merges metadata from |src_mesh| to |dst_mesh|. Any metadata with the same + // names are left unchanged. + static void MergeMetadata(const Mesh &src_mesh, Mesh *dst_mesh); + + // Removes unused MeshFeatures from |mesh|. If the |mesh| contains any mesh + // feature textures, the textures must be owned by the |mesh| otherwise an + // error is returned. + static Status RemoveUnusedMeshFeatures(Mesh *mesh); + + // Flips the UV values of |att|. + static bool FlipTextureUvValues(bool flip_u, bool flip_v, + PointAttribute *att); + + // Counts the number of degenerate faces in |mesh| for attribute |att_id|. + // Returns < 0 if counting of degenerate faces is not supported for |att_id|. + static int CountDegenerateFaces(const Mesh &mesh, int att_id); + + // Searches for the lowest texture quantization bits for |tex_att| that does + // not introduce any new texture coordinate degenerate faces. The range for + // the search is |tex_target_quantization_bits| - 29, inclusive. The function + // does not count texture coordinate degenerate faces already in the source. + // Nor does it count any new texture coordinate degenerate faces that are a + // subset of new position degenerate faces created from the quantization of + // |pos_att| using |pos_quantization_bits|. Returns the lowest quantization + // bits within the specified range or zero signifying that it could not find a + // quantization that did not cause any new degenerate faces. + static StatusOr FindLowestTextureQuantization( + const Mesh &mesh, const PointAttribute &pos_att, + int pos_quantization_bits, const PointAttribute &tex_att, + int tex_target_quantization_bits); + + // Helper function that checks whether a mesh has auto-generated tangents. + // See go/tangents_and_draco_simplifier. + static bool HasAutoGeneratedTangents(const Mesh &mesh); + + private: + static void TransformNormalizedAttribute(const Eigen::Matrix3d &transform, + PointAttribute *att); + + template + static int CountDegenerateFaces(const Mesh &mesh, const PointAttribute &att); + + // Returns a sorted list of degenerate faces for |att|. |att| must use |mesh| + // for its connectivity. |range| and |max_quantized_value| are the values + // passed into the quantizer. |quantized_degenerate_only|, is true will only + // include degenerate faces caused by the quantization. Otherwise all + // degenerate faces will be included, those made by the quantization and those + // already in the source. + static std::vector ListDegenerateQuantizedFaces( + const Mesh &mesh, const PointAttribute &att, float range, + uint32_t max_quantized_value, bool quantized_degenerate_only); + + // Returns a sorted list of degenerate faces for |att|. |att_components_t| is + // the component count for |att| as a VectorD. E.g. Vector2f, Vector3f, or + // Vector4f. |quantized_components_t| is the quantized component count for + // |att| as a VectorD. E.g. VectorD, VectorD, or + // VectorD. + template + static std::vector ListDegenerateQuantizedFaces( + const Mesh &mesh, const PointAttribute &att, float range, + uint32_t max_quantized_value, bool quantized_degenerate_only); +}; + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED +#endif // DRACO_MESH_MESH_UTILS_H_ diff --git a/contrib/draco/src/draco/mesh/mesh_utils_test.cc b/contrib/draco/src/draco/mesh/mesh_utils_test.cc new file mode 100644 index 000000000..022669cb0 --- /dev/null +++ b/contrib/draco/src/draco/mesh/mesh_utils_test.cc @@ -0,0 +1,391 @@ +// Copyright 2019 The Draco Authors. +// +// 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 "draco/mesh/mesh_utils.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED +#include "draco/core/draco_test_base.h" +#include "draco/core/draco_test_utils.h" + +namespace { + +// Compare normal vector rotated by |angle| around the x-axis. +void CompareRotatedNormals(const draco::Mesh &mesh_0, const draco::Mesh &mesh_1, + float angle) { + const draco::PointAttribute *const norm_att_0 = + mesh_0.GetNamedAttribute(draco::GeometryAttribute::NORMAL); + const draco::PointAttribute *const norm_att_1 = + mesh_1.GetNamedAttribute(draco::GeometryAttribute::NORMAL); + ASSERT_EQ(norm_att_0->size(), norm_att_1->size()); + for (draco::AttributeValueIndex avi(0); avi < norm_att_0->size(); ++avi) { + Eigen::Vector3f norm_0, norm_1; + norm_att_0->GetValue(avi, norm_0.data()); + norm_att_1->GetValue(avi, norm_1.data()); + + // Project the normals into yz plane + norm_0[0] = 0.f; + norm_1[0] = 0.f; + + if (norm_0.squaredNorm() < 1e-6f) { + // Normal pointing towards X. Make sure the rotated normal is about the + // same. + ASSERT_NEAR(norm_1.squaredNorm(), 0.f, 1e-6f); + continue; + } + + // Ensure the angle between the normals is as expected. + norm_0.normalize(); + norm_1.normalize(); + const float norm_angle = + std::atan2(norm_0.cross(norm_1).norm(), norm_0.dot(norm_1)); + ASSERT_NEAR(std::abs(norm_angle), angle, 1e-6f); + } +} + +TEST(MeshUtilsTest, TestTransform) { + auto mesh = draco::ReadMeshFromTestFile("cube_att.obj"); + ASSERT_NE(mesh, nullptr); + + draco::Mesh transformed_mesh; + transformed_mesh.Copy(*mesh); + Eigen::Matrix4d transform = Eigen::Matrix4d::Identity(); + draco::MeshUtils::TransformMesh(transform, &transformed_mesh); + + // Rotate the mesh by 45 deg around the x-axis. + transform.block<3, 3>(0, 0) = + Eigen::Quaterniond( + Eigen::AngleAxisd(M_PI / 4.f, Eigen::Vector3d::UnitX())) + .normalized() + .toRotationMatrix(); + draco::MeshUtils::TransformMesh(transform, &transformed_mesh); + CompareRotatedNormals(*mesh, transformed_mesh, M_PI / 4.f); + + // Now rotate the cube back. + transform.block<3, 3>(0, 0) = + Eigen::Quaterniond( + Eigen::AngleAxisd(-M_PI / 4.f, Eigen::Vector3d::UnitX())) + .normalized() + .toRotationMatrix(); + + draco::MeshUtils::TransformMesh(transform, &transformed_mesh); + CompareRotatedNormals(*mesh, transformed_mesh, 0.f); +} + +TEST(MeshUtilsTest, TestTextureUvFlips) { + std::unique_ptr mesh = + draco::ReadMeshFromTestFile("cube_att.obj"); + ASSERT_NE(mesh, nullptr); + + // Check that FlipTextureUvValues() only works on texture coordinates. + draco::PointAttribute *att = mesh->attribute(0); + ASSERT_EQ(att->attribute_type(), draco::GeometryAttribute::POSITION); + ASSERT_FALSE(draco::MeshUtils::FlipTextureUvValues(false, true, att)); + + att = mesh->attribute(1); + ASSERT_EQ(att->attribute_type(), draco::GeometryAttribute::TEX_COORD); + + // Get the values and flip the V values. + std::vector> check_uv_values; + check_uv_values.resize(att->size()); + for (draco::AttributeValueIndex avi(0); avi < att->size(); ++avi) { + att->GetValue(avi, &check_uv_values[avi.value()]); + check_uv_values[avi.value()][1] = 1.0 - check_uv_values[avi.value()][1]; + } + + ASSERT_TRUE(draco::MeshUtils::FlipTextureUvValues(false, true, att)); + + std::array value; + for (draco::AttributeValueIndex avi(0); avi < att->size(); ++avi) { + att->GetValue(avi, &value); + ASSERT_EQ(value[0], check_uv_values[avi.value()][0]); + ASSERT_EQ(value[1], check_uv_values[avi.value()][1]); + } + + // Flip the U values. + for (int i = 0; i < check_uv_values.size(); ++i) { + check_uv_values[i][0] = 1.0 - check_uv_values[i][0]; + } + + ASSERT_TRUE(draco::MeshUtils::FlipTextureUvValues(true, false, att)); + + for (draco::AttributeValueIndex avi(0); avi < att->size(); ++avi) { + att->GetValue(avi, &value); + ASSERT_EQ(value[0], check_uv_values[avi.value()][0]); + ASSERT_EQ(value[1], check_uv_values[avi.value()][1]); + } +} + +// Tests counting degenerate values for positions and texture coordinates for +// both scene and mesh. +TEST(MeshUtilsTest, CountDegenerateValuesLantern) { + int degenerate_positions_scene = 0; + int degenerate_tex_coords_scene = 0; + std::unique_ptr scene = + draco::ReadSceneFromTestFile("Lantern/glTF/Lantern.gltf"); + ASSERT_NE(scene, nullptr); + + for (int mgi = 0; mgi < scene->NumMeshGroups(); ++mgi) { + const draco::MeshGroup *const mesh_group = + scene->GetMeshGroup(draco::MeshGroupIndex(mgi)); + ASSERT_NE(mesh_group, nullptr); + + for (int mi = 0; mi < mesh_group->NumMeshInstances(); ++mi) { + const draco::MeshIndex mesh_index = + mesh_group->GetMeshInstance(mi).mesh_index; + const draco::Mesh &m = scene->GetMesh(mesh_index); + + for (int i = 0; i < m.num_attributes(); ++i) { + const draco::PointAttribute *const att = m.attribute(i); + ASSERT_NE(att, nullptr); + + if (att->attribute_type() == draco::GeometryAttribute::Type::POSITION) { + degenerate_positions_scene += + draco::MeshUtils::CountDegenerateFaces(m, i); + } else if (att->attribute_type() == + draco::GeometryAttribute::Type::TEX_COORD) { + degenerate_tex_coords_scene += + draco::MeshUtils::CountDegenerateFaces(m, i); + } + } + } + } + EXPECT_EQ(degenerate_positions_scene, 0); + EXPECT_EQ(degenerate_tex_coords_scene, 2); + + std::unique_ptr mesh = + draco::ReadMeshFromTestFile("Lantern/glTF/Lantern.gltf"); + ASSERT_NE(mesh, nullptr); + for (int i = 0; i < mesh->num_attributes(); ++i) { + const draco::PointAttribute *const att = mesh->attribute(i); + ASSERT_NE(att, nullptr); + if (att->attribute_type() == draco::GeometryAttribute::Type::POSITION) { + EXPECT_EQ(draco::MeshUtils::CountDegenerateFaces(*mesh, i), + degenerate_positions_scene); + } else if (att->attribute_type() == + draco::GeometryAttribute::Type::TEX_COORD) { + EXPECT_EQ(draco::MeshUtils::CountDegenerateFaces(*mesh, i), + degenerate_tex_coords_scene); + } + } +} + +// Tests finding the lowest quantization bits for the texture coordinate in a +// mesh. +TEST(MeshUtilsTest, FindLowsetTextureQuantizationLanternMesh) { + std::unique_ptr mesh = + draco::ReadMeshFromTestFile("Lantern/glTF/Lantern.gltf"); + ASSERT_NE(mesh, nullptr); + + const int pos_quantization_bits = 11; + const draco::PointAttribute *const pos_att = + mesh->GetNamedAttribute(draco::GeometryAttribute::Type::POSITION, 0); + ASSERT_NE(pos_att, nullptr); + + const draco::PointAttribute *const tex_att = + mesh->GetNamedAttribute(draco::GeometryAttribute::Type::TEX_COORD, 0); + ASSERT_NE(tex_att, nullptr); + + // Tests target no quantization returns no quantization. + const int target_no_quantization_bits = 0; + DRACO_ASSIGN_OR_ASSERT(const int no_quantization_bits, + draco::MeshUtils::FindLowestTextureQuantization( + *mesh, *pos_att, pos_quantization_bits, *tex_att, + target_no_quantization_bits)); + ASSERT_EQ(no_quantization_bits, 0); + + // Test failures. + const int out_of_range_low = -1; + const auto statusor_low = draco::MeshUtils::FindLowestTextureQuantization( + *mesh, *pos_att, pos_quantization_bits, *tex_att, out_of_range_low); + ASSERT_FALSE(statusor_low.ok()); + + const int out_of_range_high = 30; + const auto statusor_high = draco::MeshUtils::FindLowestTextureQuantization( + *mesh, *pos_att, pos_quantization_bits, *tex_att, out_of_range_high); + ASSERT_FALSE(statusor_high.ok()); + + // Tests finding the lowest quantization bits for the texture coordinate. + const int target_bits = 6; + DRACO_ASSIGN_OR_ASSERT( + const int lowest_bits, + draco::MeshUtils::FindLowestTextureQuantization( + *mesh, *pos_att, pos_quantization_bits, *tex_att, target_bits)); + ASSERT_EQ(lowest_bits, 14); +} + +// Tests finding the lowest quantization bits for the texture coordinates for +// the three meshes in the scene. +TEST(MeshUtilsTest, FindLowsetTextureQuantizationLanternScene) { + std::unique_ptr scene = + draco::ReadSceneFromTestFile("Lantern/glTF/Lantern.gltf"); + ASSERT_NE(scene, nullptr); + + const std::vector expected_mesh_quantization_bits{11, 8, 14}; + for (int mi = 0; mi < scene->NumMeshes(); ++mi) { + const draco::Mesh &mesh = scene->GetMesh(draco::MeshIndex(mi)); + + const int pos_quantization_bits = 11; + const draco::PointAttribute *const pos_att = + mesh.GetNamedAttribute(draco::GeometryAttribute::Type::POSITION, 0); + ASSERT_NE(pos_att, nullptr); + + const draco::PointAttribute *const tex_att = + mesh.GetNamedAttribute(draco::GeometryAttribute::Type::TEX_COORD, 0); + ASSERT_NE(tex_att, nullptr); + + const int target_bits = 8; + DRACO_ASSIGN_OR_ASSERT( + const int lowest_bits, + draco::MeshUtils::FindLowestTextureQuantization( + mesh, *pos_att, pos_quantization_bits, *tex_att, target_bits)); + ASSERT_EQ(lowest_bits, expected_mesh_quantization_bits[mi]); + } +} + +TEST(MeshUtilsTest, CheckAutoGeneratedTangents) { + // Test verifies that MeshUtils::HasAutoGeneratedTangents works as intended. + std::unique_ptr mesh = + draco::ReadMeshFromTestFile("sphere_no_tangents.gltf"); + ASSERT_NE(mesh, nullptr); + + ASSERT_TRUE(draco::MeshUtils::HasAutoGeneratedTangents(*mesh)); +} + +TEST(MeshUtilsTest, CheckMergeMetadata) { + // Test verifies that we can merge metadata using MeshUtils::MergeMetadata(). + std::unique_ptr mesh = + draco::ReadMeshFromTestFile("sphere_no_tangents.gltf"); + ASSERT_NE(mesh, nullptr); + + std::unique_ptr other_mesh = + draco::ReadMeshFromTestFile("cube_att.obj"); + + ASSERT_NE(mesh->GetMetadata(), nullptr); + // One attribute metadata (for the tangent attribute) and no other entries. + ASSERT_EQ(mesh->GetMetadata()->attribute_metadatas().size(), 1); + ASSERT_EQ(mesh->GetMetadata()->num_entries(), 0); + + // No metadata at the other attribute. + ASSERT_EQ(other_mesh->GetMetadata(), nullptr); + + // First try to merge |other_mesh| metadata to |mesh|. This shouldn't do + // anything. + draco::MeshUtils::MergeMetadata(*other_mesh, mesh.get()); + ASSERT_EQ(mesh->GetMetadata()->attribute_metadatas().size(), 1); + ASSERT_EQ(mesh->GetMetadata()->num_entries(), 0); + + // Merge |mesh| metadata to |other_mesh|. This will create empty metadata but + // not any attribute metadata because |other_mesh| doesn't have the tangent + // attribute. + draco::MeshUtils::MergeMetadata(*mesh, other_mesh.get()); + ASSERT_NE(other_mesh->GetMetadata(), nullptr); + ASSERT_EQ(other_mesh->GetMetadata()->attribute_metadatas().size(), 0); + ASSERT_EQ(other_mesh->GetMetadata()->num_entries(), 0); + ASSERT_FALSE(draco::MeshUtils::HasAutoGeneratedTangents(*other_mesh)); + + // Add dummy tangent attribute to the |other_mesh|. + std::unique_ptr tang_att(new draco::PointAttribute()); + draco::PointAttribute *const tang_att_ptr = tang_att.get(); + tang_att->set_attribute_type(draco::GeometryAttribute::TANGENT); + other_mesh->AddAttribute(std::move(tang_att)); + + // Merge |mesh| metadata to |other_mesh|. This time the tangent metadata + // should be copied over. + draco::MeshUtils::MergeMetadata(*mesh, other_mesh.get()); + ASSERT_NE(other_mesh->GetMetadata(), nullptr); + ASSERT_EQ(other_mesh->GetMetadata()->attribute_metadatas().size(), 1); + ASSERT_EQ(other_mesh->GetMetadata()->num_entries(), 0); + ASSERT_NE(other_mesh->GetMetadata()->GetAttributeMetadataByUniqueId( + tang_att_ptr->unique_id()), + nullptr); + ASSERT_TRUE(draco::MeshUtils::HasAutoGeneratedTangents(*other_mesh)); + + // Now add some entries to the geometry metadata and merge again. + mesh->metadata()->AddEntryInt("test_int_0", 0); + mesh->metadata()->AddEntryInt("test_int_1", 1); + mesh->metadata()->AddEntryInt("test_int_shared", 2); + other_mesh->metadata()->AddEntryInt("test_int_shared", 3); + + // "test_int_0" and "test_int_1" should be copied over while + // "test_entry_shared" should stay unchanged. + draco::MeshUtils::MergeMetadata(*mesh, other_mesh.get()); + ASSERT_NE(other_mesh->GetMetadata(), nullptr); + // Attribute metadata should stay unchanged. + ASSERT_EQ(other_mesh->GetMetadata()->attribute_metadatas().size(), 1); + ASSERT_NE(other_mesh->GetMetadata()->GetAttributeMetadataByUniqueId( + tang_att_ptr->unique_id()), + nullptr); + ASSERT_EQ(other_mesh->GetMetadata() + ->GetAttributeMetadataByUniqueId(tang_att_ptr->unique_id()) + ->num_entries(), + 1); + + // Check the geometry metadata entries. + ASSERT_EQ(other_mesh->GetMetadata()->num_entries(), 3); + int metadata_value; + ASSERT_TRUE( + other_mesh->GetMetadata()->GetEntryInt("test_int_0", &metadata_value)); + ASSERT_EQ(metadata_value, 0); + ASSERT_TRUE( + other_mesh->GetMetadata()->GetEntryInt("test_int_1", &metadata_value)); + ASSERT_EQ(metadata_value, 1); + + // The shared entry should have an unchanged value. + ASSERT_TRUE(other_mesh->GetMetadata()->GetEntryInt("test_int_shared", + &metadata_value)); + ASSERT_EQ(metadata_value, 3); +} + +TEST(MeshUtilsTest, RemoveUnusedMeshFeatures) { + // Test verifies that MeshUtils::RemoveUnusedMeshFeatures works as intended. + std::unique_ptr mesh = + draco::ReadMeshFromTestFile("BoxesMeta/glTF/BoxesMeta.gltf"); + ASSERT_NE(mesh, nullptr); + + // The input mesh should have five mesh features and two features textures. + ASSERT_EQ(mesh->NumMeshFeatures(), 5); + ASSERT_EQ(mesh->GetNonMaterialTextureLibrary().NumTextures(), 2); + + // All of those features and textures should be used so calling the method + // below shouldn't do anything. + draco::MeshUtils::RemoveUnusedMeshFeatures(mesh.get()); + ASSERT_EQ(mesh->NumMeshFeatures(), 5); + ASSERT_EQ(mesh->GetNonMaterialTextureLibrary().NumTextures(), 2); + + // Now remove material 1 that is mapped to first two mesh features. + draco::PointAttribute *mat_att = mesh->attribute( + mesh->GetNamedAttributeId(draco::GeometryAttribute::MATERIAL)); + + // This basically remaps all faces from material 1 to material 0. + uint32_t mat_index = 0; + mat_att->SetAttributeValue(draco::AttributeValueIndex(1), &mat_index); + + // Try to remove the mesh features again. + draco::MeshUtils::RemoveUnusedMeshFeatures(mesh.get()); + + // Three of the mesh features should have been removed as well as one mesh + // features texture. + ASSERT_EQ(mesh->NumMeshFeatures(), 2); + ASSERT_EQ(mesh->GetNonMaterialTextureLibrary().NumTextures(), 1); + + // Ensure the remaining mesh features are mapped to the correct material. + for (draco::MeshFeaturesIndex mfi(0); mfi < mesh->NumMeshFeatures(); ++mfi) { + ASSERT_EQ(mesh->NumMeshFeaturesMaterialMasks(mfi), 1); + ASSERT_EQ(mesh->GetMeshFeaturesMaterialMask(mfi, 0), 0); + } +} + +} // namespace + +#endif // DRACO_TRANSCODER_SUPPORTED diff --git a/contrib/draco/src/draco/mesh/triangle_soup_mesh_builder.cc b/contrib/draco/src/draco/mesh/triangle_soup_mesh_builder.cc index 60b0c50b8..2af94a052 100644 --- a/contrib/draco/src/draco/mesh/triangle_soup_mesh_builder.cc +++ b/contrib/draco/src/draco/mesh/triangle_soup_mesh_builder.cc @@ -23,11 +23,23 @@ void TriangleSoupMeshBuilder::Start(int num_faces) { attribute_element_types_.clear(); } +#ifdef DRACO_TRANSCODER_SUPPORTED +void TriangleSoupMeshBuilder::SetName(const std::string &name) { + mesh_->SetName(name); +} +#endif // DRACO_TRANSCODER_SUPPORTED + int TriangleSoupMeshBuilder::AddAttribute( GeometryAttribute::Type attribute_type, int8_t num_components, DataType data_type) { + return AddAttribute(attribute_type, num_components, data_type, false); +} + +int TriangleSoupMeshBuilder::AddAttribute( + GeometryAttribute::Type attribute_type, int8_t num_components, + DataType data_type, bool normalized) { GeometryAttribute va; - va.Init(attribute_type, nullptr, num_components, data_type, false, + va.Init(attribute_type, nullptr, num_components, data_type, normalized, DataTypeLength(data_type) * num_components, 0); attribute_element_types_.push_back(-1); return mesh_->AddAttribute(va, true, mesh_->num_points()); @@ -41,8 +53,6 @@ void TriangleSoupMeshBuilder::SetAttributeValuesForFace( att->SetAttributeValue(AttributeValueIndex(start_index), corner_value_0); att->SetAttributeValue(AttributeValueIndex(start_index + 1), corner_value_1); att->SetAttributeValue(AttributeValueIndex(start_index + 2), corner_value_2); - // TODO(ostava): The below code should be called only for one attribute. - // It will work OK even for multiple attributes, but it's redundant. mesh_->SetFace(face_id, {{PointIndex(start_index), PointIndex(start_index + 1), PointIndex(start_index + 2)}}); diff --git a/contrib/draco/src/draco/mesh/triangle_soup_mesh_builder.h b/contrib/draco/src/draco/mesh/triangle_soup_mesh_builder.h index 89466e1d8..503fe84c5 100644 --- a/contrib/draco/src/draco/mesh/triangle_soup_mesh_builder.h +++ b/contrib/draco/src/draco/mesh/triangle_soup_mesh_builder.h @@ -15,7 +15,14 @@ #ifndef DRACO_MESH_TRIANGLE_SOUP_MESH_BUILDER_H_ #define DRACO_MESH_TRIANGLE_SOUP_MESH_BUILDER_H_ +#include +#include + #include "draco/draco_features.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED +#include "draco/core/status.h" +#endif #include "draco/mesh/mesh.h" namespace draco { @@ -25,15 +32,25 @@ namespace draco { // deduplicated. class TriangleSoupMeshBuilder { public: + // Index type of the inserted element. + typedef FaceIndex ElementIndex; + // Starts mesh building for a given number of faces. // TODO(ostava): Currently it's necessary to select the correct number of // faces upfront. This should be generalized, but it will require us to // rewrite our attribute resizing functions. void Start(int num_faces); +#ifdef DRACO_TRANSCODER_SUPPORTED + // Sets mesh name. + void SetName(const std::string &name); +#endif // DRACO_TRANSCODER_SUPPORTED + // Adds an empty attribute to the mesh. Returns the new attribute's id. int AddAttribute(GeometryAttribute::Type attribute_type, int8_t num_components, DataType data_type); + int AddAttribute(GeometryAttribute::Type attribute_type, + int8_t num_components, DataType data_type, bool normalized); // Sets values for a given attribute on all corners of a given face. void SetAttributeValuesForFace(int att_id, FaceIndex face_id, @@ -41,12 +58,34 @@ class TriangleSoupMeshBuilder { const void *corner_value_1, const void *corner_value_2); +#ifdef DRACO_TRANSCODER_SUPPORTED + // Converts input values of type T into internal representation used by + // |att_id|. Each input value needs to have |input_num_components| entries. + template + Status ConvertAndSetAttributeValuesForFace(int att_id, FaceIndex face_id, + int input_num_components, + const T *corner_value_0, + const T *corner_value_1, + const T *corner_value_2); +#endif + // Sets value for a per-face attribute. If all faces of a given attribute are // set with this method, the attribute will be marked as per-face, otherwise // it will be marked as per-corner attribute. void SetPerFaceAttributeValueForFace(int att_id, FaceIndex face_id, const void *value); + // Add metadata. + void AddMetadata(std::unique_ptr metadata) { + mesh_->AddMetadata(std::move(metadata)); + } + + // Add metadata for an attribute. + void AddAttributeMetadata(int32_t att_id, + std::unique_ptr metadata) { + mesh_->AddAttributeMetadata(att_id, std::move(metadata)); + } + // Finalizes the mesh or returns nullptr on error. // Once this function is called, the builder becomes invalid and cannot be // used until the method Start() is called again. @@ -58,6 +97,30 @@ class TriangleSoupMeshBuilder { std::unique_ptr mesh_; }; +#ifdef DRACO_TRANSCODER_SUPPORTED +template +Status TriangleSoupMeshBuilder::ConvertAndSetAttributeValuesForFace( + int att_id, FaceIndex face_id, int input_num_components, + const T *corner_value_0, const T *corner_value_1, const T *corner_value_2) { + const int start_index = 3 * face_id.value(); + PointAttribute *const att = mesh_->attribute(att_id); + DRACO_RETURN_IF_ERROR( + att->ConvertAndSetAttributeValue(AttributeValueIndex(start_index + 0), + input_num_components, corner_value_0)); + DRACO_RETURN_IF_ERROR( + att->ConvertAndSetAttributeValue(AttributeValueIndex(start_index + 1), + input_num_components, corner_value_1)); + DRACO_RETURN_IF_ERROR( + att->ConvertAndSetAttributeValue(AttributeValueIndex(start_index + 2), + input_num_components, corner_value_2)); + mesh_->SetFace(face_id, + {{PointIndex(start_index), PointIndex(start_index + 1), + PointIndex(start_index + 2)}}); + attribute_element_types_[att_id] = MESH_CORNER_ATTRIBUTE; + return OkStatus(); +} +#endif // DRACO_TRANSCODER_SUPPORTED + } // namespace draco #endif // DRACO_MESH_TRIANGLE_SOUP_MESH_BUILDER_H_ diff --git a/contrib/draco/src/draco/mesh/triangle_soup_mesh_builder_test.cc b/contrib/draco/src/draco/mesh/triangle_soup_mesh_builder_test.cc index 171f8fe24..b23641760 100644 --- a/contrib/draco/src/draco/mesh/triangle_soup_mesh_builder_test.cc +++ b/contrib/draco/src/draco/mesh/triangle_soup_mesh_builder_test.cc @@ -14,7 +14,11 @@ // #include "draco/mesh/triangle_soup_mesh_builder.h" +#include +#include + #include "draco/core/draco_test_base.h" +#include "draco/core/draco_test_utils.h" #include "draco/core/vector_d.h" namespace draco { @@ -26,6 +30,9 @@ TEST_F(TriangleSoupMeshBuilderTest, CubeTest) { // of the provided triangle soup data. TriangleSoupMeshBuilder mb; mb.Start(12); +#ifdef DRACO_TRANSCODER_SUPPORTED + mb.SetName("Cube"); +#endif const int pos_att_id = mb.AddAttribute(GeometryAttribute::POSITION, 3, DT_FLOAT32); // clang-format off @@ -92,6 +99,9 @@ TEST_F(TriangleSoupMeshBuilderTest, CubeTest) { std::unique_ptr mesh = mb.Finalize(); ASSERT_NE(mesh, nullptr) << "Failed to build the cube mesh."; +#ifdef DRACO_TRANSCODER_SUPPORTED + EXPECT_EQ(mesh->GetName(), "Cube"); +#endif EXPECT_EQ(mesh->num_points(), 8) << "Unexpected number of vertices."; EXPECT_EQ(mesh->num_faces(), 12) << "Unexpected number of faces."; } @@ -139,7 +149,7 @@ TEST_F(TriangleSoupMeshBuilderTest, TestPerFaceAttribs) { Vector3f(0.f, 1.f, 0.f).data(), Vector3f(1.f, 1.f, 0.f).data(), Vector3f(0.f, 1.f, 1.f).data()); - mb.SetPerFaceAttributeValueForFace(gen_att_id, FaceIndex(4), &bool_false);; + mb.SetPerFaceAttributeValueForFace(gen_att_id, FaceIndex(4), &bool_false); mb.SetAttributeValuesForFace(pos_att_id, FaceIndex(5), Vector3f(0.f, 1.f, 1.f).data(), @@ -189,9 +199,69 @@ TEST_F(TriangleSoupMeshBuilderTest, TestPerFaceAttribs) { std::unique_ptr mesh = mb.Finalize(); ASSERT_NE(mesh, nullptr) << "Failed to build the cube mesh."; +#ifdef DRACO_TRANSCODER_SUPPORTED + EXPECT_TRUE(mesh->GetName().empty()); +#endif EXPECT_EQ(mesh->num_faces(), 12) << "Unexpected number of faces."; EXPECT_EQ(mesh->GetAttributeElementType(gen_att_id), MESH_FACE_ATTRIBUTE) << "Unexpected attribute element type."; } +#ifdef DRACO_TRANSCODER_SUPPORTED +TEST_F(TriangleSoupMeshBuilderTest, NormalizedColor) { + // This tests, verifies that the mesh builder constructs a valid model with + // normalized integer colors using floating points as input. + TriangleSoupMeshBuilder mb; + mb.Start(2); + const int pos_att_id = + mb.AddAttribute(GeometryAttribute::POSITION, 3, DT_FLOAT32); + const int color_att_id = + mb.AddAttribute(GeometryAttribute::COLOR, 3, DT_UINT8, true); + + mb.SetAttributeValuesForFace( + pos_att_id, FaceIndex(0), Vector3f(0.f, 0.f, 0.f).data(), + Vector3f(1.f, 0.f, 0.f).data(), Vector3f(0.f, 1.f, 0.f).data()); + DRACO_ASSERT_OK(mb.ConvertAndSetAttributeValuesForFace( + color_att_id, FaceIndex(0), 4, Vector4f(0.f, 0.f, 0.f, 1.f).data(), + Vector4f(1.f, 1.f, 1.f, 1.f).data(), + Vector4f(0.5f, 0.5f, 0.5f, 1.f).data())); + mb.SetAttributeValuesForFace( + pos_att_id, FaceIndex(1), Vector3f(0.f, 1.f, 0.f).data(), + Vector3f(1.f, 0.f, 0.f).data(), Vector3f(1.f, 1.f, 0.f).data()); + + DRACO_ASSERT_OK(mb.ConvertAndSetAttributeValuesForFace( + color_att_id, FaceIndex(1), 4, Vector4f(0.5f, 0.5f, 0.5f, 1.f).data(), + Vector4f(1.f, 1.f, 1.f, 1.f).data(), + Vector4f(0.25f, 0.0f, 1.f, 1.f).data())); + + std::unique_ptr mesh = mb.Finalize(); + ASSERT_NE(mesh, nullptr) << "Failed to build the test mesh."; + + EXPECT_EQ(mesh->num_points(), 4) << "Unexpected number of vertices."; + EXPECT_EQ(mesh->num_faces(), 2) << "Unexpected number of faces."; + + const auto *col_att = + mesh->GetNamedAttribute(draco::GeometryAttribute::COLOR); + ASSERT_NE(col_att, nullptr) << "Missing color attribute."; + ASSERT_EQ(col_att->size(), 4); + + // All colors should be in range 0-255. + uint8_t max_val = 0, min_val = 255; + for (draco::AttributeValueIndex avi(0); avi < col_att->size(); ++avi) { + VectorD cval; + col_att->GetValue(avi, &cval); + const uint8_t max = cval.MaxCoeff(); + const uint8_t min = cval.MinCoeff(); + if (max > max_val) { + max_val = max; + } + if (min < min_val) { + min_val = min; + } + } + ASSERT_EQ(max_val, 255); + ASSERT_EQ(min_val, 0); +} +#endif + } // namespace draco diff --git a/contrib/draco/src/draco/metadata/geometry_metadata.cc b/contrib/draco/src/draco/metadata/geometry_metadata.cc index b83898140..b6a882c0b 100644 --- a/contrib/draco/src/draco/metadata/geometry_metadata.cc +++ b/contrib/draco/src/draco/metadata/geometry_metadata.cc @@ -18,6 +18,19 @@ namespace draco { +AttributeMetadata::AttributeMetadata(const AttributeMetadata &metadata) + : Metadata(metadata) { + att_unique_id_ = metadata.att_unique_id_; +} + +GeometryMetadata::GeometryMetadata(const GeometryMetadata &metadata) + : Metadata(metadata) { + for (size_t i = 0; i < metadata.att_metadatas_.size(); ++i) { + att_metadatas_.push_back(std::unique_ptr( + new AttributeMetadata(*metadata.att_metadatas_[i]))); + } +} + const AttributeMetadata *GeometryMetadata::GetAttributeMetadataByStringEntry( const std::string &entry_name, const std::string &entry_value) const { for (auto &&att_metadata : att_metadatas_) { @@ -35,7 +48,7 @@ const AttributeMetadata *GeometryMetadata::GetAttributeMetadataByStringEntry( bool GeometryMetadata::AddAttributeMetadata( std::unique_ptr att_metadata) { - if (!att_metadata.get()) { + if (!att_metadata) { return false; } att_metadatas_.push_back(std::move(att_metadata)); diff --git a/contrib/draco/src/draco/metadata/geometry_metadata.h b/contrib/draco/src/draco/metadata/geometry_metadata.h index ec7ecb9ee..531bdef25 100644 --- a/contrib/draco/src/draco/metadata/geometry_metadata.h +++ b/contrib/draco/src/draco/metadata/geometry_metadata.h @@ -25,6 +25,7 @@ namespace draco { class AttributeMetadata : public Metadata { public: AttributeMetadata() : att_unique_id_(0) {} + AttributeMetadata(const AttributeMetadata &metadata); explicit AttributeMetadata(const Metadata &metadata) : Metadata(metadata), att_unique_id_(0) {} @@ -57,6 +58,7 @@ struct AttributeMetadataHasher { class GeometryMetadata : public Metadata { public: GeometryMetadata() {} + GeometryMetadata(const GeometryMetadata &metadata); explicit GeometryMetadata(const Metadata &metadata) : Metadata(metadata) {} const AttributeMetadata *GetAttributeMetadataByStringEntry( diff --git a/contrib/draco/src/draco/metadata/metadata.cc b/contrib/draco/src/draco/metadata/metadata.cc index 9141907ed..51b4e93a3 100644 --- a/contrib/draco/src/draco/metadata/metadata.cc +++ b/contrib/draco/src/draco/metadata/metadata.cc @@ -122,6 +122,14 @@ const Metadata *Metadata::GetSubMetadata(const std::string &name) const { return sub_ptr->second.get(); } +Metadata *Metadata::sub_metadata(const std::string &name) { + auto sub_ptr = sub_metadatas_.find(name); + if (sub_ptr == sub_metadatas_.end()) { + return nullptr; + } + return sub_ptr->second.get(); +} + void Metadata::RemoveEntry(const std::string &name) { // Actually just remove "name", no need to check if it exists. auto entry_ptr = entries_.find(name); diff --git a/contrib/draco/src/draco/metadata/metadata.h b/contrib/draco/src/draco/metadata/metadata.h index 56d05e46a..12c1ba974 100644 --- a/contrib/draco/src/draco/metadata/metadata.h +++ b/contrib/draco/src/draco/metadata/metadata.h @@ -147,6 +147,7 @@ class Metadata { bool AddSubMetadata(const std::string &name, std::unique_ptr sub_metadata); const Metadata *GetSubMetadata(const std::string &name) const; + Metadata *sub_metadata(const std::string &name); void RemoveEntry(const std::string &name); diff --git a/contrib/draco/src/draco/metadata/metadata_decoder.cc b/contrib/draco/src/draco/metadata/metadata_decoder.cc index a8e66f854..6468e3207 100644 --- a/contrib/draco/src/draco/metadata/metadata_decoder.cc +++ b/contrib/draco/src/draco/metadata/metadata_decoder.cc @@ -59,18 +59,25 @@ bool MetadataDecoder::DecodeGeometryMetadata(DecoderBuffer *in_buffer, } bool MetadataDecoder::DecodeMetadata(Metadata *metadata) { - struct MetadataPair { + // Limit metadata nesting depth to avoid stack overflow in destructor. + constexpr int kMaxSubmetadataLevel = 1000; + + struct MetadataTuple { Metadata *parent_metadata; Metadata *decoded_metadata; + int level; }; - std::vector metadata_stack; - metadata_stack.push_back({nullptr, metadata}); + std::vector metadata_stack; + metadata_stack.push_back({nullptr, metadata, 0}); while (!metadata_stack.empty()) { - const MetadataPair mp = metadata_stack.back(); + const MetadataTuple mp = metadata_stack.back(); metadata_stack.pop_back(); metadata = mp.decoded_metadata; if (mp.parent_metadata != nullptr) { + if (mp.level > kMaxSubmetadataLevel) { + return false; + } std::string sub_metadata_name; if (!DecodeName(&sub_metadata_name)) { return false; @@ -105,7 +112,8 @@ bool MetadataDecoder::DecodeMetadata(Metadata *metadata) { return false; } for (uint32_t i = 0; i < num_sub_metadata; ++i) { - metadata_stack.push_back({metadata, nullptr}); + metadata_stack.push_back( + {metadata, nullptr, mp.parent_metadata ? mp.level + 1 : mp.level}); } } return true; @@ -123,6 +131,9 @@ bool MetadataDecoder::DecodeEntry(Metadata *metadata) { if (data_size == 0) { return false; } + if (data_size > buffer_->remaining_size()) { + return false; + } std::vector entry_value(data_size); if (!buffer_->Decode(&entry_value[0], data_size)) { return false; diff --git a/contrib/draco/src/draco/metadata/metadata_test.cc b/contrib/draco/src/draco/metadata/metadata_test.cc index cf7ae6eee..03104e03e 100644 --- a/contrib/draco/src/draco/metadata/metadata_test.cc +++ b/contrib/draco/src/draco/metadata/metadata_test.cc @@ -104,12 +104,16 @@ TEST_F(MetadataTest, TestNestedMetadata) { sub_metadata->AddEntryInt("int", 100); metadata.AddSubMetadata("sub0", std::move(sub_metadata)); - const auto sub_metadata_ptr = metadata.GetSubMetadata("sub0"); + const auto sub_metadata_ptr = metadata.sub_metadata("sub0"); ASSERT_NE(sub_metadata_ptr, nullptr); int32_t int_value = 0; ASSERT_TRUE(sub_metadata_ptr->GetEntryInt("int", &int_value)); ASSERT_EQ(int_value, 100); + + sub_metadata_ptr->AddEntryInt("new_entry", 20); + ASSERT_TRUE(sub_metadata_ptr->GetEntryInt("new_entry", &int_value)); + ASSERT_EQ(int_value, 20); } TEST_F(MetadataTest, TestHardCopyMetadata) { diff --git a/contrib/draco/src/draco/metadata/property_table.cc b/contrib/draco/src/draco/metadata/property_table.cc new file mode 100644 index 000000000..c6a5fd984 --- /dev/null +++ b/contrib/draco/src/draco/metadata/property_table.cc @@ -0,0 +1,183 @@ +// Copyright 2022 The Draco Authors. +// +// 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 "draco/metadata/property_table.h" + +#include +#include +#include +#include + +#ifdef DRACO_TRANSCODER_SUPPORTED + +namespace draco { + +bool PropertyTable::Schema::Object::operator==(const Object& other) const { + if (type_ != other.type_ || name_ != other.name_) { + return false; + } + switch (type_) { + case OBJECT: + if (objects_.size() != other.objects_.size()) { + return false; + } + for (int i = 0; i < objects_.size(); ++i) { + if (objects_[i] != other.objects_[i]) { + return false; + } + } + break; + case ARRAY: + if (array_.size() != other.array_.size()) { + return false; + } + for (int i = 0; i < array_.size(); ++i) { + if (array_[i] != other.array_[i]) { + return false; + } + } + break; + case STRING: + return string_ == other.string_; + case INTEGER: + return integer_ == other.integer_; + case BOOLEAN: + return boolean_ == other.boolean_; + } + return true; +} + +void PropertyTable::Schema::Object::Copy(const Object& src) { + name_ = src.name_; + type_ = src.type_; + objects_.reserve(src.objects_.size()); + for (const Object& obj : src.objects_) { + objects_.emplace_back(); + objects_.back().Copy(obj); + } + array_.reserve(src.array_.size()); + for (const Object& obj : src.array_) { + array_.emplace_back(); + array_.back().Copy(obj); + } + string_ = src.string_; + integer_ = src.integer_; + boolean_ = src.boolean_; +} + +PropertyTable::Property::Property() {} + +bool PropertyTable::Property::Data::operator==(const Data& other) const { + return data == other.data && target == other.target; +} + +bool PropertyTable::Property::Offsets::operator==(const Offsets& other) const { + return data == other.data && type == other.type; +} + +bool PropertyTable::Property::operator==(const Property& other) const { + return name_ == other.name_ && data_ == other.data_ && + array_offsets_ == other.array_offsets_ && + string_offsets_ == other.string_offsets_; +} + +void PropertyTable::Property::Copy(const Property& src) { + name_ = src.name_; + data_ = src.data_; + array_offsets_ = src.array_offsets_; + string_offsets_ = src.string_offsets_; +} + +void PropertyTable::Property::SetName(const std::string& name) { name_ = name; } +const std::string& PropertyTable::Property::GetName() const { return name_; } + +PropertyTable::Property::Data& PropertyTable::Property::GetData() { + return data_; +} +const PropertyTable::Property::Data& PropertyTable::Property::GetData() const { + return data_; +} + +const PropertyTable::Property::Offsets& +PropertyTable::Property::GetArrayOffsets() const { + return array_offsets_; +} +PropertyTable::Property::Offsets& PropertyTable::Property::GetArrayOffsets() { + return array_offsets_; +} + +const PropertyTable::Property::Offsets& +PropertyTable::Property::GetStringOffsets() const { + return string_offsets_; +} +PropertyTable::Property::Offsets& PropertyTable::Property::GetStringOffsets() { + return string_offsets_; +} + +PropertyTable::PropertyTable() : count_(0) {} + +bool PropertyTable::operator==(const PropertyTable& other) const { + if (name_ != other.name_ || class_ != other.class_ || + count_ != other.count_ || + properties_.size() != other.properties_.size()) { + return false; + } + for (int i = 0; i < properties_.size(); ++i) { + if (*properties_[i] != *other.properties_[i]) { + return false; + } + } + return true; +} + +void PropertyTable::Copy(const PropertyTable& src) { + name_ = src.name_; + class_ = src.class_; + count_ = src.count_; + properties_.clear(); + properties_.reserve(src.properties_.size()); + for (int i = 0; i < src.properties_.size(); ++i) { + std::unique_ptr property(new Property()); + property->Copy(src.GetProperty(i)); + properties_.push_back(std::move(property)); + } +} + +void PropertyTable::SetName(const std::string& value) { name_ = value; } +const std::string& PropertyTable::GetName() const { return name_; } + +void PropertyTable::SetClass(const std::string& value) { class_ = value; } +const std::string& PropertyTable::GetClass() const { return class_; } + +void PropertyTable::SetCount(int count) { count_ = count; } +int PropertyTable::GetCount() const { return count_; } + +int PropertyTable::AddProperty(std::unique_ptr property) { + properties_.push_back(std::move(property)); + return properties_.size() - 1; +} +int PropertyTable::NumProperties() const { return properties_.size(); } +const PropertyTable::Property& PropertyTable::GetProperty(int index) const { + return *properties_[index]; +} +PropertyTable::Property& PropertyTable::GetProperty(int index) { + return *properties_[index]; +} +void PropertyTable::RemoveProperty(int index) { + properties_.erase(properties_.begin() + index); +} + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED diff --git a/contrib/draco/src/draco/metadata/property_table.h b/contrib/draco/src/draco/metadata/property_table.h new file mode 100644 index 000000000..41efb0163 --- /dev/null +++ b/contrib/draco/src/draco/metadata/property_table.h @@ -0,0 +1,243 @@ +// Copyright 2022 The Draco Authors. +// +// 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 DRACO_METADATA_PROPERTY_TABLE_H_ +#define DRACO_METADATA_PROPERTY_TABLE_H_ + +#include "draco/draco_features.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED + +#include +#include +#include + +namespace draco { + +// Describes a property table as defined in the EXT_structural_metadata glTF +// extension, including property table schema and table properties (columns). +class PropertyTable { + public: + // Describes property table schema in the form of a JSON object. + struct Schema { + // JSON object of the schema. + // TODO(vytyaz): Consider using a third_party/json library. Currently there + // is a conflict between Filament's assert_invariant() macro and JSON + // library's assert_invariant() method that causes compile errors in Draco + // visualization library. + class Object { + public: + enum Type { OBJECT, ARRAY, STRING, INTEGER, BOOLEAN }; + + // Constructors. + Object() : Object("") {} + explicit Object(const std::string& name) + : name_(name), type_(OBJECT), integer_(0), boolean_(false) {} + Object(const std::string& name, const std::string& value) : Object(name) { + SetString(value); + } + Object(const std::string& name, const char* value) : Object(name) { + SetString(value); + } + Object(const std::string& name, int value) : Object(name) { + SetInteger(value); + } + Object(const std::string& name, bool value) : Object(name) { + SetBoolean(value); + } + + // Methods for comparing two objects. + bool operator==(const Object& other) const; + bool operator!=(const Object& other) const { return !(*this == other); } + + // Method for copying the object. + void Copy(const Object& src); + + // Methods for getting object name and type. + const std::string& GetName() const { return name_; } + Type GetType() const { return type_; } + + // Methods for getting object value. + const std::vector& GetObjects() const { return objects_; } + const std::vector& GetArray() const { return array_; } + const std::string& GetString() const { return string_; } + int GetInteger() const { return integer_; } + bool GetBoolean() const { return boolean_; } + + // Methods for setting object value. + std::vector& SetObjects() { + type_ = OBJECT; + return objects_; + } + std::vector& SetArray() { + type_ = ARRAY; + return array_; + } + void SetString(const std::string& value) { + type_ = STRING; + string_ = value; + } + void SetInteger(int value) { + type_ = INTEGER; + integer_ = value; + } + void SetBoolean(bool value) { + type_ = BOOLEAN; + boolean_ = value; + } + + private: + std::string name_; + Type type_; + std::vector objects_; + std::vector array_; + std::string string_; + int integer_; + bool boolean_; + }; + + // Valid schema top-level JSON object name is "schema". + Schema() : json("schema") {} + + // Methods for comparing two schemas. + bool operator==(const Schema& other) const { return json == other.json; } + bool operator!=(const Schema& other) const { return !(*this == other); } + + // Valid schema top-level JSON object is required to have child objects. + bool Empty() const { return json.GetObjects().empty(); } + + // Top-level JSON object of the schema. + Object json; + }; + + // Describes a property (column) of a property table. + class Property { + public: + // Describes glTF buffer view data. + struct Data { + // Methods for comparing two data objects. + bool operator==(const Data& other) const; + bool operator!=(const Data& other) const { return !(*this == other); } + + // Buffer view data. + std::vector data; + + // Data target corresponds to the target property of the glTF bufferView + // object and classifies the type or nature of the data. + int target = 0; + }; + + // Describes offsets of the entries in property data when the data + // represents an array of strings or an array of variable-length number + // arrays. + struct Offsets { + // Methods for comparing two offsets. + bool operator==(const Offsets& other) const; + bool operator!=(const Offsets& other) const { return !(*this == other); } + + // Data containing the offset entries. + Data data; + + // Data type of the offset entries. + std::string type; + }; + + // Creates an empty property. + Property(); + + // Methods for comparing two properties. + bool operator==(const Property& other) const; + bool operator!=(const Property& other) const { return !(*this == other); } + + // Copies all data from |src| property. + void Copy(const Property& src); + + // Name of this property. + void SetName(const std::string& name); + const std::string& GetName() const; + + // Property data stores one table column worth of data. For example, when + // the data of type UINT8 is [11, 22] then the property values are 11 and 22 + // for the first and second table rows. See EXT_structural_metadata glTF + // extension documentation for more details. + Data& GetData(); + const Data& GetData() const; + + // Array offsets are used when property data contains a variable-length + // number arrays. For example, when the data is [0, 1, 2, 3, 4] and the + // array offsets are [0, 2, 5] for a two-row table, then the property value + // arrays are [0, 1] and [2, 3, 4] for the first and second table rows, + // respectively. See EXT_structural_metadata glTF extension documentation + // for more details. + const Offsets& GetArrayOffsets() const; + Offsets& GetArrayOffsets(); + + // String offsets are used when property data contains strings. For example, + // when the data is "SeaLand" and the array offsets are [0, 3, 7] for a + // two-row table, then the property strings are "Sea" and "Land" for the + // first and second table rows, respectively. See EXT_structural_metadata + // glTF extension documentation for more details. + const Offsets& GetStringOffsets() const; + Offsets& GetStringOffsets(); + + private: + std::string name_; + Data data_; + Offsets array_offsets_; + Offsets string_offsets_; + // TODO(vytyaz): Support property value modifiers min, max, offset, scale. + }; + + // Creates an empty property table. + PropertyTable(); + + // Methods for comparing two property tables. + bool operator==(const PropertyTable& other) const; + bool operator!=(const PropertyTable& other) const { + return !(*this == other); + } + + // Copies all data from |src| property table. + void Copy(const PropertyTable& src); + + // Name of this property table. + void SetName(const std::string& value); + const std::string& GetName() const; + + // Class of this property table. + void SetClass(const std::string& value); + const std::string& GetClass() const; + + // Number of rows in this property table. + void SetCount(int count); + int GetCount() const; + + // Table properties (columns). + int AddProperty(std::unique_ptr property); + int NumProperties() const; + const Property& GetProperty(int index) const; + Property& GetProperty(int index); + void RemoveProperty(int index); + + private: + std::string name_; + std::string class_; + int count_; + std::vector> properties_; +}; + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED +#endif // DRACO_METADATA_PROPERTY_TABLE_H_ diff --git a/contrib/draco/src/draco/metadata/property_table_test.cc b/contrib/draco/src/draco/metadata/property_table_test.cc new file mode 100644 index 000000000..4d5ee2d2c --- /dev/null +++ b/contrib/draco/src/draco/metadata/property_table_test.cc @@ -0,0 +1,624 @@ +// Copyright 2022 The Draco Authors. +// +// 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 "draco/metadata/property_table.h" + +#include +#include +#include + +#include "draco/core/draco_test_base.h" +#include "draco/core/draco_test_utils.h" + +namespace { + +#ifdef DRACO_TRANSCODER_SUPPORTED + +TEST(PropertyTableTest, TestPropertyDataDefaults) { + // Test construction of an empty property data. + draco::PropertyTable::Property::Data data; + ASSERT_TRUE(data.data.empty()); + ASSERT_EQ(data.target, 0); +} + +TEST(PropertyTableTest, TestPropertyDefaults) { + // Test construction of an empty property table property. + draco::PropertyTable::Property property; + ASSERT_TRUE(property.GetName().empty()); + ASSERT_TRUE(property.GetData().data.empty()); + { + const auto &offsets = property.GetArrayOffsets(); + ASSERT_TRUE(offsets.type.empty()); + ASSERT_TRUE(offsets.data.data.empty()); + ASSERT_EQ(offsets.data.target, 0); + } + { + const auto &offsets = property.GetStringOffsets(); + ASSERT_TRUE(offsets.type.empty()); + ASSERT_TRUE(offsets.data.data.empty()); + ASSERT_EQ(offsets.data.target, 0); + } +} + +TEST(PropertyTableTest, TestPropertyTableDefaults) { + // Test construction of an empty property table. + draco::PropertyTable table; + ASSERT_TRUE(table.GetName().empty()); + ASSERT_TRUE(table.GetClass().empty()); + ASSERT_EQ(table.GetCount(), 0); + ASSERT_EQ(table.NumProperties(), 0); +} + +TEST(PropertyTableTest, TestSchemaDefaults) { + // Test construction of an empty property table schema. + draco::PropertyTable::Schema schema; + ASSERT_TRUE(schema.Empty()); + ASSERT_EQ(schema.json.GetName(), "schema"); + ASSERT_EQ(schema.json.GetType(), + draco::PropertyTable::Schema::Object::OBJECT); + ASSERT_TRUE(schema.json.GetObjects().empty()); + ASSERT_TRUE(schema.json.GetArray().empty()); + ASSERT_TRUE(schema.json.GetString().empty()); + ASSERT_EQ(schema.json.GetInteger(), 0); + ASSERT_FALSE(schema.json.GetBoolean()); +} + +TEST(PropertyTableTest, TestSchemaObjectDefaultConstructor) { + // Test construction of an empty property table schema object. + draco::PropertyTable::Schema::Object object; + ASSERT_TRUE(object.GetName().empty()); + ASSERT_EQ(object.GetType(), draco::PropertyTable::Schema::Object::OBJECT); + ASSERT_TRUE(object.GetObjects().empty()); + ASSERT_TRUE(object.GetArray().empty()); + ASSERT_TRUE(object.GetString().empty()); + ASSERT_EQ(object.GetInteger(), 0); + ASSERT_FALSE(object.GetBoolean()); +} + +TEST(PropertyTableTest, TestSchemaObjectNamedConstructor) { + // Test construction of a named property table schema object. + draco::PropertyTable::Schema::Object object("Flexible Demeanour"); + ASSERT_EQ(object.GetName(), "Flexible Demeanour"); + ASSERT_EQ(object.GetType(), draco::PropertyTable::Schema::Object::OBJECT); + ASSERT_TRUE(object.GetObjects().empty()); +} + +TEST(PropertyTableTest, TestSchemaObjectStringConstructor) { + // Test construction of property table schema object storing a string. + draco::PropertyTable::Schema::Object object("Flexible Demeanour", "GCU"); + ASSERT_EQ(object.GetName(), "Flexible Demeanour"); + ASSERT_EQ(object.GetType(), draco::PropertyTable::Schema::Object::STRING); + ASSERT_EQ(object.GetString(), "GCU"); +} + +TEST(PropertyTableTest, TestSchemaObjectIntegerConstructor) { + // Test construction of property table schema object storing an integer. + draco::PropertyTable::Schema::Object object("Flexible Demeanour", 12); + ASSERT_EQ(object.GetName(), "Flexible Demeanour"); + ASSERT_EQ(object.GetType(), draco::PropertyTable::Schema::Object::INTEGER); + ASSERT_EQ(object.GetInteger(), 12); +} + +TEST(PropertyTableTest, TestSchemaObjectBooleanConstructor) { + // Test construction of property table schema object storing a boolean. + draco::PropertyTable::Schema::Object object("Flexible Demeanour", true); + ASSERT_EQ(object.GetName(), "Flexible Demeanour"); + ASSERT_EQ(object.GetType(), draco::PropertyTable::Schema::Object::BOOLEAN); + ASSERT_TRUE(object.GetBoolean()); +} + +TEST(PropertyTableTest, TestSchemaObjectSettersAndGetters) { + // Test value setters and getters of property table schema object. + typedef draco::PropertyTable::Schema::Object Object; + Object object; + ASSERT_EQ(object.GetType(), Object::OBJECT); + + object.SetArray().push_back(Object("entry", 12)); + ASSERT_EQ(object.GetType(), Object::ARRAY); + ASSERT_EQ(object.GetArray().size(), 1); + ASSERT_EQ(object.GetArray()[0].GetName(), "entry"); + ASSERT_EQ(object.GetArray()[0].GetInteger(), 12); + + object.SetObjects().push_back(Object("object", 9)); + ASSERT_EQ(object.GetType(), Object::OBJECT); + ASSERT_EQ(object.GetObjects().size(), 1); + ASSERT_EQ(object.GetObjects()[0].GetName(), "object"); + ASSERT_EQ(object.GetObjects()[0].GetInteger(), 9); + + object.SetString("matter"); + ASSERT_EQ(object.GetType(), Object::STRING); + ASSERT_EQ(object.GetString(), "matter"); + + object.SetInteger(5); + ASSERT_EQ(object.GetType(), Object::INTEGER); + ASSERT_EQ(object.GetInteger(), 5); + + object.SetBoolean(true); + ASSERT_EQ(object.GetType(), Object::BOOLEAN); + ASSERT_EQ(object.GetBoolean(), true); +} + +TEST(PropertyTableTest, TestSchemaCompare) { + typedef draco::PropertyTable::Schema Schema; + // Test comparison of two schema objects. + { + // Compare the same empty schema object. + Schema a; + ASSERT_TRUE(a == a); + ASSERT_FALSE(a != a); + } + { + // Compare two empty schema objects. + Schema a; + Schema b; + ASSERT_TRUE(a == b); + ASSERT_FALSE(a != b); + } + { + // Compare two schema objects with different JSON objects. + Schema a; + Schema b; + a.json.SetBoolean(true); + b.json.SetBoolean(false); + ASSERT_FALSE(a == b); + ASSERT_TRUE(a != b); + } +} + +TEST(PropertyTableTest, TestSchemaObjectCompare) { + // Test comparison of two schema JSON objects. + typedef draco::PropertyTable::Schema::Object Object; + { + // Compare the same object. + Object a; + ASSERT_TRUE(a == a); + ASSERT_FALSE(a != a); + } + { + // Compare two default objects. + Object a; + Object b; + ASSERT_TRUE(a == b); + ASSERT_FALSE(a != b); + } + { + // Compare two objects with different names. + Object a("one"); + Object b("two"); + ASSERT_FALSE(a == b); + ASSERT_TRUE(a != b); + } + { + // Compare two objects with different types. + Object a; + Object b; + a.SetInteger(1); + b.SetString("one"); + ASSERT_FALSE(a == b); + ASSERT_TRUE(a != b); + } + { + // Compare two identical string-type objects. + Object a; + Object b; + a.SetString("one"); + b.SetString("one"); + ASSERT_TRUE(a == b); + ASSERT_FALSE(a != b); + } + { + // Compare two different string-type objects. + Object a; + Object b; + a.SetString("one"); + b.SetString("two"); + ASSERT_FALSE(a == b); + ASSERT_TRUE(a != b); + } + { + // Compare two identical integer-type objects. + Object a; + Object b; + a.SetInteger(1); + b.SetInteger(1); + ASSERT_TRUE(a == b); + ASSERT_FALSE(a != b); + } + { + // Compare two different integer-type objects. + Object a; + Object b; + a.SetInteger(1); + b.SetInteger(2); + ASSERT_FALSE(a == b); + ASSERT_TRUE(a != b); + } + { + // Compare two identical boolean-type objects. + Object a; + Object b; + a.SetBoolean(true); + b.SetBoolean(true); + ASSERT_TRUE(a == b); + ASSERT_FALSE(a != b); + } + { + // Compare two different boolean-type objects. + Object a; + Object b; + a.SetBoolean(true); + b.SetBoolean(false); + ASSERT_FALSE(a == b); + ASSERT_TRUE(a != b); + } + { + // Compare two identical object-type objects. + Object a; + Object b; + a.SetObjects().emplace_back("one"); + b.SetObjects().emplace_back("one"); + ASSERT_TRUE(a == b); + ASSERT_FALSE(a != b); + } + { + // Compare two different object-type objects. + Object a; + Object b; + a.SetObjects().emplace_back("one"); + b.SetObjects().emplace_back("two"); + ASSERT_FALSE(a == b); + ASSERT_TRUE(a != b); + } + { + // Compare two object-type objects with different counts. + Object a; + Object b; + a.SetObjects().emplace_back("one"); + b.SetObjects().emplace_back("one"); + b.SetObjects().emplace_back("two"); + ASSERT_FALSE(a == b); + ASSERT_TRUE(a != b); + } + { + // Compare two identical array-type objects. + Object a; + Object b; + a.SetArray().emplace_back("", 1); + b.SetArray().emplace_back("", 1); + ASSERT_TRUE(a == b); + ASSERT_FALSE(a != b); + } + { + // Compare two different array-type objects. + Object a; + Object b; + a.SetArray().emplace_back("", 1); + b.SetArray().emplace_back("", 2); + ASSERT_FALSE(a == b); + ASSERT_TRUE(a != b); + } + { + // Compare two array-type objects with different counts. + Object a; + Object b; + a.SetArray().emplace_back("", 1); + b.SetArray().emplace_back("", 1); + b.SetArray().emplace_back("", 2); + ASSERT_FALSE(a == b); + ASSERT_TRUE(a != b); + } +} + +TEST(PropertyTableTest, TestPropertySettersAndGetters) { + // Test setter and getter methods of the property table property. + draco::PropertyTable::Property property; + property.SetName("Unfortunate Conflict Of Evidence"); + property.GetData().data.push_back(2); + + // Check that property members can be accessed via getters. + ASSERT_EQ(property.GetName(), "Unfortunate Conflict Of Evidence"); + ASSERT_EQ(property.GetData().data.size(), 1); + ASSERT_EQ(property.GetData().data[0], 2); +} + +TEST(PropertyTableTest, TestPropertyTableSettersAndGetters) { + // Test setter and getter methods of the property table. + draco::PropertyTable table; + table.SetName("Just Read The Instructions"); + table.SetClass("General Contact Unit"); + table.SetCount(456); + { + std::unique_ptr property( + new draco::PropertyTable::Property()); + property->SetName("Determinist"); + ASSERT_EQ(table.AddProperty(std::move(property)), 0); + } + { + std::unique_ptr property( + new draco::PropertyTable::Property()); + property->SetName("Revisionist"); + ASSERT_EQ(table.AddProperty(std::move(property)), 1); + } + + // Check that property table members can be accessed via getters. + ASSERT_EQ(table.GetName(), "Just Read The Instructions"); + ASSERT_EQ(table.GetClass(), "General Contact Unit"); + ASSERT_EQ(table.GetCount(), 456); + ASSERT_EQ(table.NumProperties(), 2); + ASSERT_EQ(table.GetProperty(0).GetName(), "Determinist"); + ASSERT_EQ(table.GetProperty(1).GetName(), "Revisionist"); + + // Check that proeprties can be removed. + table.RemoveProperty(0); + ASSERT_EQ(table.NumProperties(), 1); + ASSERT_EQ(table.GetProperty(0).GetName(), "Revisionist"); + table.RemoveProperty(0); + ASSERT_EQ(table.NumProperties(), 0); +} + +TEST(PropertyTableTest, TestPropertyCopy) { + // Test that property table property can be copied. + draco::PropertyTable::Property property; + property.SetName("Unfortunate Conflict Of Evidence"); + property.GetData().data.push_back(2); + + // Make a copy. + draco::PropertyTable::Property copy; + copy.Copy(property); + + // Check the copy. + ASSERT_EQ(copy.GetName(), "Unfortunate Conflict Of Evidence"); + ASSERT_EQ(copy.GetData().data.size(), 1); + ASSERT_EQ(copy.GetData().data[0], 2); +} + +TEST(PropertyTableTest, TestPropertyTableCopy) { + // Test that property table can be copied. + draco::PropertyTable table; + table.SetName("Just Read The Instructions"); + table.SetClass("General Contact Unit"); + table.SetCount(456); + { + std::unique_ptr property( + new draco::PropertyTable::Property()); + property->SetName("Determinist"); + table.AddProperty(std::move(property)); + } + { + std::unique_ptr property( + new draco::PropertyTable::Property()); + property->SetName("Revisionist"); + table.AddProperty(std::move(property)); + } + + // Make a copy. + draco::PropertyTable copy; + copy.Copy(table); + + // Check the copy. + ASSERT_EQ(copy.GetName(), "Just Read The Instructions"); + ASSERT_EQ(copy.GetClass(), "General Contact Unit"); + ASSERT_EQ(copy.GetCount(), 456); + ASSERT_EQ(copy.NumProperties(), 2); + ASSERT_EQ(copy.GetProperty(0).GetName(), "Determinist"); + ASSERT_EQ(copy.GetProperty(1).GetName(), "Revisionist"); +} + +TEST(PropertyTableTest, TestPropertyDataCompare) { + // Test comparison of two property data objects. + typedef draco::PropertyTable::Property::Data Data; + { + // Compare the same data object. + Data a; + ASSERT_TRUE(a == a); + ASSERT_FALSE(a != a); + } + { + // Compare two default data objects. + Data a; + Data b; + ASSERT_TRUE(a == b); + ASSERT_FALSE(a != b); + } + { + // Compare two data objects with different targets. + Data a; + Data b; + a.target = 1; + b.target = 2; + ASSERT_FALSE(a == b); + ASSERT_TRUE(a != b); + } + { + // Compare two data objects with different data vectors. + Data a; + Data b; + a.data = {1}; + a.data = {2}; + ASSERT_FALSE(a == b); + ASSERT_TRUE(a != b); + } +} + +TEST(PropertyTableTest, TestPropertyOffsets) { + // Test comparison of two property offsets. + typedef draco::PropertyTable::Property::Offsets Offsets; + { + // Compare the same offsets object. + Offsets a; + ASSERT_TRUE(a == a); + ASSERT_FALSE(a != a); + } + { + // Compare two default offsets objects. + Offsets a; + Offsets b; + ASSERT_TRUE(a == b); + ASSERT_FALSE(a != b); + } + { + // Compare two offsets objects with different types. + Offsets a; + Offsets b; + a.type = 1; + b.type = 2; + ASSERT_FALSE(a == b); + ASSERT_TRUE(a != b); + } + { + // Compare two offsets objects with different data objects. + Offsets a; + Offsets b; + a.data.target = 1; + b.data.target = 2; + ASSERT_FALSE(a == b); + ASSERT_TRUE(a != b); + } +} + +TEST(PropertyTableTest, TestPropertyCompare) { + // Test comparison of two properties. + typedef draco::PropertyTable::Property Property; + { + // Compare the same property object. + Property a; + ASSERT_TRUE(a == a); + ASSERT_FALSE(a != a); + } + { + // Compare two default property objects. + Property a; + Property b; + ASSERT_TRUE(a == b); + ASSERT_FALSE(a != b); + } + { + // Compare two property objects with different names. + Property a; + Property b; + a.SetName("one"); + b.SetName("two"); + ASSERT_FALSE(a == b); + ASSERT_TRUE(a != b); + } + { + // Compare two property objects with different data. + Property a; + Property b; + a.GetData().target = 1; + b.GetData().target = 2; + ASSERT_FALSE(a == b); + ASSERT_TRUE(a != b); + } + { + // Compare two property objects with different array offsets. + Property a; + Property b; + a.GetArrayOffsets().data.target = 1; + b.GetArrayOffsets().data.target = 2; + ASSERT_FALSE(a == b); + ASSERT_TRUE(a != b); + } + { + // Compare two property objects with different string offsets. + Property a; + Property b; + a.GetStringOffsets().data.target = 1; + b.GetStringOffsets().data.target = 2; + ASSERT_FALSE(a == b); + ASSERT_TRUE(a != b); + } +} + +TEST(PropertyTableTest, TestPropertyTableCompare) { + // Test comparison of two property tables. + typedef draco::PropertyTable PropertyTable; + typedef draco::PropertyTable::Property Property; + { + // Compare the same property table object. + PropertyTable a; + ASSERT_TRUE(a == a); + ASSERT_FALSE(a != a); + } + { + // Compare two default property tables. + PropertyTable a; + PropertyTable b; + ASSERT_TRUE(a == b); + ASSERT_FALSE(a != b); + } + { + // Compare two property tables with different names. + PropertyTable a; + PropertyTable b; + a.SetName("one"); + b.SetName("two"); + ASSERT_FALSE(a == b); + ASSERT_TRUE(a != b); + } + { + // Compare two property tables with different classes. + PropertyTable a; + PropertyTable b; + a.SetClass("one"); + b.SetClass("two"); + ASSERT_FALSE(a == b); + ASSERT_TRUE(a != b); + } + { + // Compare two property tables with different counts. + PropertyTable a; + PropertyTable b; + a.SetCount(1); + b.SetCount(2); + ASSERT_FALSE(a == b); + ASSERT_TRUE(a != b); + } + { + // Compare two property tables with identical properties. + PropertyTable a; + PropertyTable b; + a.AddProperty(std::unique_ptr(new Property)); + b.AddProperty(std::unique_ptr(new Property)); + ASSERT_TRUE(a == b); + ASSERT_FALSE(a != b); + } + { + // Compare two property tables with different number of properties. + PropertyTable a; + PropertyTable b; + a.AddProperty(std::unique_ptr(new Property)); + b.AddProperty(std::unique_ptr(new Property)); + b.AddProperty(std::unique_ptr(new Property)); + ASSERT_FALSE(a == b); + ASSERT_TRUE(a != b); + } + { + // Compare two property tables with different properties. + PropertyTable a; + PropertyTable b; + std::unique_ptr p1(new Property); + std::unique_ptr p2(new Property); + p1->SetName("one"); + p2->SetName("two"); + a.AddProperty(std::move(p1)); + b.AddProperty(std::move(p2)); + ASSERT_FALSE(a == b); + ASSERT_TRUE(a != b); + } +} + +#endif // DRACO_TRANSCODER_SUPPORTED + +} // namespace diff --git a/contrib/draco/src/draco/metadata/structural_metadata.cc b/contrib/draco/src/draco/metadata/structural_metadata.cc new file mode 100644 index 000000000..48fff2b25 --- /dev/null +++ b/contrib/draco/src/draco/metadata/structural_metadata.cc @@ -0,0 +1,74 @@ +// Copyright 2022 The Draco Authors. +// +// 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 "draco/metadata/structural_metadata.h" + +#include +#include + +#ifdef DRACO_TRANSCODER_SUPPORTED + +namespace draco { + +StructuralMetadata::StructuralMetadata() {} + +bool StructuralMetadata::operator==(const StructuralMetadata &other) const { + return property_table_schema_ == other.property_table_schema_ && + property_tables_ == other.property_tables_; +} + +void StructuralMetadata::Copy(const StructuralMetadata &src) { + property_table_schema_.json.Copy(src.property_table_schema_.json); + property_tables_.resize(src.property_tables_.size()); + for (int i = 0; i < property_tables_.size(); ++i) { + property_tables_[i] = std::unique_ptr(new PropertyTable()); + property_tables_[i]->Copy(*src.property_tables_[i]); + } +} + +void StructuralMetadata::SetPropertyTableSchema( + const PropertyTable::Schema &schema) { + property_table_schema_ = schema; +} + +const PropertyTable::Schema &StructuralMetadata::GetPropertyTableSchema() + const { + return property_table_schema_; +} + +int StructuralMetadata::AddPropertyTable( + std::unique_ptr property_table) { + property_tables_.push_back(std::move(property_table)); + return property_tables_.size() - 1; +} + +int StructuralMetadata::NumPropertyTables() const { + return property_tables_.size(); +} + +const PropertyTable &StructuralMetadata::GetPropertyTable(int index) const { + return *property_tables_[index]; +} + +PropertyTable &StructuralMetadata::GetPropertyTable(int index) { + return *property_tables_[index]; +} + +void StructuralMetadata::RemovePropertyTable(int index) { + property_tables_.erase(property_tables_.begin() + index); +} + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED diff --git a/contrib/draco/src/draco/metadata/structural_metadata.h b/contrib/draco/src/draco/metadata/structural_metadata.h new file mode 100644 index 000000000..24756c70c --- /dev/null +++ b/contrib/draco/src/draco/metadata/structural_metadata.h @@ -0,0 +1,64 @@ +// Copyright 2022 The Draco Authors. +// +// 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 DRACO_METADATA_STRUCTURAL_METADATA_H_ +#define DRACO_METADATA_STRUCTURAL_METADATA_H_ + +#include "draco/draco_features.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED + +#include +#include +#include + +#include "draco/metadata/property_table.h" + +namespace draco { + +// Holds data associated with EXT_structural_metadata glTF extension. +class StructuralMetadata { + public: + StructuralMetadata(); + + // Methods for comparing two structural metadata objects. + bool operator==(const StructuralMetadata &other) const; + bool operator!=(const StructuralMetadata &other) const { + return !(*this == other); + } + + // Copies |src| structural metadata into this object. + void Copy(const StructuralMetadata &src); + + // Property table schema. + void SetPropertyTableSchema(const PropertyTable::Schema &schema); + const PropertyTable::Schema &GetPropertyTableSchema() const; + + // Property tables. + int AddPropertyTable(std::unique_ptr property_table); + int NumPropertyTables() const; + const PropertyTable &GetPropertyTable(int index) const; + PropertyTable &GetPropertyTable(int index); + void RemovePropertyTable(int index); + + private: + // Property table schema and property tables. + PropertyTable::Schema property_table_schema_; + std::vector> property_tables_; +}; + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED +#endif // DRACO_METADATA_STRUCTURAL_METADATA_H_ diff --git a/contrib/draco/src/draco/metadata/structural_metadata_test.cc b/contrib/draco/src/draco/metadata/structural_metadata_test.cc new file mode 100644 index 000000000..d0429a568 --- /dev/null +++ b/contrib/draco/src/draco/metadata/structural_metadata_test.cc @@ -0,0 +1,170 @@ +// Copyright 2022 The Draco Authors. +// +// 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 "draco/metadata/structural_metadata.h" + +#include +#include +#include + +#include "draco/core/draco_test_base.h" +#include "draco/core/draco_test_utils.h" + +namespace { + +#ifdef DRACO_TRANSCODER_SUPPORTED + +TEST(StructuralMetadataTest, TestCopy) { + // Tests copying of structural metadata. + draco::StructuralMetadata structural_metadata; + + // Add property table schema to structural metadata. + draco::PropertyTable::Schema schema; + schema.json.SetString("Culture"); + structural_metadata.SetPropertyTableSchema(schema); + + // Add property table to structural metadata. + std::unique_ptr table(new draco::PropertyTable()); + table->SetName("Just Read The Instructions"); + table->SetClass("General Contact Unit"); + table->SetCount(456); + { + std::unique_ptr property( + new draco::PropertyTable::Property()); + property->SetName("Determinist"); + table->AddProperty(std::move(property)); + } + { + std::unique_ptr property( + new draco::PropertyTable::Property()); + property->SetName("Revisionist"); + table->AddProperty(std::move(property)); + } + ASSERT_EQ(structural_metadata.AddPropertyTable(std::move(table)), 0); + + // Copy the structural metadata. + draco::StructuralMetadata copy; + copy.Copy(structural_metadata); + + // Check that the structural metadata property table schema has been copied. + ASSERT_EQ(copy.GetPropertyTableSchema().json.GetString(), "Culture"); + + // Check that the structural metadata property table has been copied. + ASSERT_EQ(copy.NumPropertyTables(), 1); + ASSERT_EQ(copy.GetPropertyTable(0).GetName(), "Just Read The Instructions"); + ASSERT_EQ(copy.GetPropertyTable(0).GetClass(), "General Contact Unit"); + ASSERT_EQ(copy.GetPropertyTable(0).GetCount(), 456); + ASSERT_EQ(copy.GetPropertyTable(0).NumProperties(), 2); + ASSERT_EQ(copy.GetPropertyTable(0).GetProperty(0).GetName(), "Determinist"); + ASSERT_EQ(copy.GetPropertyTable(0).GetProperty(1).GetName(), "Revisionist"); +} + +TEST(StructuralMetadataTest, TestPropertyTables) { + // Tests adding and removing of property tables to structural metadata. + draco::StructuralMetadata structural_metadata; + + // Check that property tables can be added. + { + std::unique_ptr table(new draco::PropertyTable()); + table->SetName("Just Read The Instructions"); + ASSERT_EQ(structural_metadata.AddPropertyTable(std::move(table)), 0); + } + { + std::unique_ptr table(new draco::PropertyTable()); + table->SetName("So Much For Subtlety"); + ASSERT_EQ(structural_metadata.AddPropertyTable(std::move(table)), 1); + } + { + std::unique_ptr table(new draco::PropertyTable()); + table->SetName("Of Course I Still Love You"); + ASSERT_EQ(structural_metadata.AddPropertyTable(std::move(table)), 2); + } + draco::StructuralMetadata &sm = structural_metadata; + + // Check that the property tables can be removed. + ASSERT_EQ(sm.NumPropertyTables(), 3); + ASSERT_EQ(sm.GetPropertyTable(0).GetName(), "Just Read The Instructions"); + ASSERT_EQ(sm.GetPropertyTable(1).GetName(), "So Much For Subtlety"); + ASSERT_EQ(sm.GetPropertyTable(2).GetName(), "Of Course I Still Love You"); + + sm.RemovePropertyTable(1); + ASSERT_EQ(sm.NumPropertyTables(), 2); + ASSERT_EQ(sm.GetPropertyTable(0).GetName(), "Just Read The Instructions"); + ASSERT_EQ(sm.GetPropertyTable(1).GetName(), "Of Course I Still Love You"); + + sm.RemovePropertyTable(1); + ASSERT_EQ(sm.NumPropertyTables(), 1); + ASSERT_EQ(sm.GetPropertyTable(0).GetName(), "Just Read The Instructions"); + + sm.RemovePropertyTable(0); + ASSERT_EQ(sm.NumPropertyTables(), 0); +} + +TEST(StructuralMetadataTest, TestCompare) { + // Test comparison of two structural metadata objects. + typedef draco::PropertyTable PropertyTable; + { + // Compare the same structural metadata object. + draco::StructuralMetadata a; + ASSERT_TRUE(a == a); + ASSERT_FALSE(a != a); + } + { + // Compare two identical structural metadata objects. + draco::StructuralMetadata a; + draco::StructuralMetadata b; + ASSERT_TRUE(a == b); + ASSERT_FALSE(a != b); + } + { + // Compare two structural metadata objects with different schemas. + draco::StructuralMetadata a; + draco::StructuralMetadata b; + PropertyTable::Schema s1; + PropertyTable::Schema s2; + s1.json.SetString("one"); + s2.json.SetString("two"); + a.SetPropertyTableSchema(s1); + b.SetPropertyTableSchema(s2); + ASSERT_FALSE(a == b); + ASSERT_TRUE(a != b); + } + { + // Compare two objects with different number of proeprty tables. + draco::StructuralMetadata a; + draco::StructuralMetadata b; + a.AddPropertyTable(std::unique_ptr(new PropertyTable())); + b.AddPropertyTable(std::unique_ptr(new PropertyTable())); + b.AddPropertyTable(std::unique_ptr(new PropertyTable())); + ASSERT_FALSE(a == b); + ASSERT_TRUE(a != b); + } + { + // Compare two objects with different proeprty tables. + draco::StructuralMetadata a; + draco::StructuralMetadata b; + auto p1 = std::unique_ptr(new PropertyTable()); + auto p2 = std::unique_ptr(new PropertyTable()); + p1->SetName("one"); + p2->SetName("two"); + a.AddPropertyTable(std::move(p1)); + b.AddPropertyTable(std::move(p2)); + ASSERT_FALSE(a == b); + ASSERT_TRUE(a != b); + } +} + +#endif // DRACO_TRANSCODER_SUPPORTED + +} // namespace diff --git a/contrib/draco/src/draco/point_cloud/point_cloud.cc b/contrib/draco/src/draco/point_cloud/point_cloud.cc index a9f9ea2af..039c4f201 100644 --- a/contrib/draco/src/draco/point_cloud/point_cloud.cc +++ b/contrib/draco/src/draco/point_cloud/point_cloud.cc @@ -16,11 +16,41 @@ #include #include +#include + +#ifdef DRACO_TRANSCODER_SUPPORTED +#include "draco/attributes/point_attribute.h" +#endif namespace draco { PointCloud::PointCloud() : num_points_(0) {} +#ifdef DRACO_TRANSCODER_SUPPORTED +void PointCloud::Copy(const PointCloud &src) { + num_points_ = src.num_points_; + for (int i = 0; i < GeometryAttribute::NAMED_ATTRIBUTES_COUNT; ++i) { + named_attribute_index_[i] = src.named_attribute_index_[i]; + } + attributes_.resize(src.attributes_.size()); + for (int i = 0; i < src.attributes_.size(); ++i) { + attributes_[i] = std::unique_ptr(new PointAttribute()); + attributes_[i]->CopyFrom(*src.attributes_[i]); + } + CopyMetadata(src); +} + +void PointCloud::CopyMetadata(const PointCloud &src) { + if (src.metadata_ == nullptr) { + metadata_ = nullptr; + } else { + // Copy base metadata. + const GeometryMetadata *const metadata = src.metadata_.get(); + metadata_.reset(new GeometryMetadata(*metadata)); + } +} +#endif + int32_t PointCloud::NumNamedAttributes(GeometryAttribute::Type type) const { if (type == GeometryAttribute::INVALID || type >= GeometryAttribute::NAMED_ATTRIBUTES_COUNT) { @@ -253,11 +283,16 @@ bool PointCloud::DeduplicateAttributeValues() { } #endif -// TODO(xiaoxumeng): Consider to cash the BBox. +// TODO(b/199760503): Consider to cache the BBox. BoundingBox PointCloud::ComputeBoundingBox() const { BoundingBox bounding_box; auto pc_att = GetNamedAttribute(GeometryAttribute::POSITION); - // TODO(xiaoxumeng): Make the BoundingBox a template type, it may not be easy + if (pc_att == nullptr) { + // Return default invalid bounding box. + return bounding_box; + } + + // TODO(b/199760503): Make the BoundingBox a template type, it may not be easy // because PointCloud is not a template. // Or simply add some preconditioning here to make sure the position attribute // is valid, because the current code works only if the position attribute is diff --git a/contrib/draco/src/draco/point_cloud/point_cloud.h b/contrib/draco/src/draco/point_cloud/point_cloud.h index d11bd47a3..17d0bc392 100644 --- a/contrib/draco/src/draco/point_cloud/point_cloud.h +++ b/contrib/draco/src/draco/point_cloud/point_cloud.h @@ -31,6 +31,11 @@ class PointCloud { PointCloud(); virtual ~PointCloud() = default; +#ifdef DRACO_TRANSCODER_SUPPORTED + // Copies all data from the |src| point cloud. + void Copy(const PointCloud &src); +#endif + // Returns the number of named attributes of a given type. int32_t NumNamedAttributes(GeometryAttribute::Type type) const; @@ -185,6 +190,11 @@ class PointCloud { void set_num_points(PointIndex::ValueType num) { num_points_ = num; } protected: +#ifdef DRACO_TRANSCODER_SUPPORTED + // Copies metadata from the |src| point cloud. + void CopyMetadata(const PointCloud &src); +#endif + #ifdef DRACO_ATTRIBUTE_INDICES_DEDUPLICATION_SUPPORTED // Applies id mapping of deduplicated points (called by DeduplicatePointIds). virtual void ApplyPointIdDeduplication( diff --git a/contrib/draco/src/draco/point_cloud/point_cloud_builder.cc b/contrib/draco/src/draco/point_cloud/point_cloud_builder.cc index 431ae505f..90ec37962 100644 --- a/contrib/draco/src/draco/point_cloud/point_cloud_builder.cc +++ b/contrib/draco/src/draco/point_cloud/point_cloud_builder.cc @@ -14,6 +14,8 @@ // #include "draco/point_cloud/point_cloud_builder.h" +#include + namespace draco { PointCloudBuilder::PointCloudBuilder() {} diff --git a/contrib/draco/src/draco/point_cloud/point_cloud_builder.h b/contrib/draco/src/draco/point_cloud/point_cloud_builder.h index cf55a728b..512b0f71f 100644 --- a/contrib/draco/src/draco/point_cloud/point_cloud_builder.h +++ b/contrib/draco/src/draco/point_cloud/point_cloud_builder.h @@ -15,6 +15,8 @@ #ifndef DRACO_POINT_CLOUD_POINT_CLOUD_BUILDER_H_ #define DRACO_POINT_CLOUD_POINT_CLOUD_BUILDER_H_ +#include + #include "draco/point_cloud/point_cloud.h" namespace draco { @@ -37,6 +39,9 @@ namespace draco { class PointCloudBuilder { public: + // Index type of the inserted element. + typedef PointIndex ElementIndex; + PointCloudBuilder(); // Starts collecting point cloud data. @@ -71,6 +76,12 @@ class PointCloudBuilder { // used until the method Start() is called again. std::unique_ptr Finalize(bool deduplicate_points); + // Add metadata for an attribute. + void AddAttributeMetadata(int32_t att_id, + std::unique_ptr metadata) { + point_cloud_->AddAttributeMetadata(att_id, std::move(metadata)); + } + private: std::unique_ptr point_cloud_; }; diff --git a/contrib/draco/src/draco/point_cloud/point_cloud_test.cc b/contrib/draco/src/draco/point_cloud/point_cloud_test.cc index 4e9460370..1cd780db2 100644 --- a/contrib/draco/src/draco/point_cloud/point_cloud_test.cc +++ b/contrib/draco/src/draco/point_cloud/point_cloud_test.cc @@ -14,6 +14,9 @@ // #include "draco/point_cloud/point_cloud.h" +#include +#include + #include "draco/core/draco_test_base.h" #include "draco/core/draco_test_utils.h" #include "draco/metadata/geometry_metadata.h" @@ -25,6 +28,57 @@ class PointCloudTest : public ::testing::Test { PointCloudTest() {} }; +#ifdef DRACO_TRANSCODER_SUPPORTED +TEST_F(PointCloudTest, PointCloudCopy) { + // Tests that we can copy a point cloud. + std::unique_ptr pc = + draco::ReadPointCloudFromTestFile("pc_kd_color.drc"); + ASSERT_NE(pc, nullptr); + + // Add metadata to the point cloud. + std::unique_ptr metadata( + new draco::GeometryMetadata()); + metadata->AddEntryInt("speed", 1050); + metadata->AddEntryString("code", "YT-1300f"); + + // Add attribute metadata. + std::unique_ptr a_metadata( + new draco::AttributeMetadata()); + a_metadata->set_att_unique_id(pc->attribute(0)->unique_id()); + a_metadata->AddEntryInt("attribute_test", 3); + metadata->AddAttributeMetadata(std::move(a_metadata)); + pc->AddMetadata(std::move(metadata)); + + // Create a copy of the point cloud. + draco::PointCloud pc_copy; + pc_copy.Copy(*pc); + + // Check the point cloud data. + ASSERT_EQ(pc->num_points(), pc_copy.num_points()); + ASSERT_EQ(pc->num_attributes(), pc_copy.num_attributes()); + for (int i = 0; i < pc->num_attributes(); ++i) { + ASSERT_EQ(pc->attribute(i)->attribute_type(), + pc_copy.attribute(i)->attribute_type()); + } + + // Check the point cloud metadata. + int speed; + std::string code; + ASSERT_NE(pc->GetMetadata(), nullptr); + ASSERT_TRUE(pc->GetMetadata()->GetEntryInt("speed", &speed)); + ASSERT_TRUE(pc->GetMetadata()->GetEntryString("code", &code)); + ASSERT_EQ(speed, 1050); + ASSERT_EQ(code, "YT-1300f"); + + const auto *const att_metadata_copy = + pc->GetMetadata()->GetAttributeMetadataByUniqueId(0); + ASSERT_NE(att_metadata_copy, nullptr); + int att_test; + ASSERT_TRUE(att_metadata_copy->GetEntryInt("attribute_test", &att_test)); + ASSERT_EQ(att_test, 3); +} +#endif + TEST_F(PointCloudTest, TestAttributeDeletion) { draco::PointCloud pc; // Test whether we can correctly delete an attribute from a point cloud. diff --git a/contrib/draco/src/draco/scene/instance_array.cc b/contrib/draco/src/draco/scene/instance_array.cc new file mode 100644 index 000000000..bcd49996a --- /dev/null +++ b/contrib/draco/src/draco/scene/instance_array.cc @@ -0,0 +1,45 @@ +// Copyright 2022 The Draco Authors. +// +// 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 "draco/scene/instance_array.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED + +#include + +namespace draco { + +void InstanceArray::Copy(const InstanceArray &other) { + instances_.resize(other.instances_.size()); + for (int i = 0; i < instances_.size(); i++) { + instances_[i].trs.Copy(other.instances_[i].trs); + } +} + +Status InstanceArray::AddInstance(const Instance &instance) { + // Check that the |instance.trs| does not have the transformation matrix set, + // because the EXT_mesh_gpu_instancing glTF extension dictates that only the + // individual TRS vectors are stored. + if (instance.trs.MatrixSet()) { + return ErrorStatus("Instance must have no matrix set."); + } + + // Move the |instance| to the end of the instances vector. + instances_.push_back(instance); + return OkStatus(); +} + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED diff --git a/contrib/draco/src/draco/scene/instance_array.h b/contrib/draco/src/draco/scene/instance_array.h new file mode 100644 index 000000000..fb1fbde1e --- /dev/null +++ b/contrib/draco/src/draco/scene/instance_array.h @@ -0,0 +1,61 @@ +// Copyright 2022 The Draco Authors. +// +// 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 DRACO_SCENE_INSTANCE_ARRAY_H_ +#define DRACO_SCENE_INSTANCE_ARRAY_H_ + +#include "draco/draco_features.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED +#include +#include + +#include "draco/core/vector_d.h" +#include "draco/scene/trs_matrix.h" + +namespace draco { + +// Describes a mesh group instancing array that includes TRS transformation +// for multiple instance positions and possibly other custom instance attributes +// (not yet supported), following the EXT_mesh_gpu_instancing glTF extension. +class InstanceArray { + public: + struct Instance { + // Translation, rotation, and scale vectors. + TrsMatrix trs; + // TODO(vytyaz): Support custom instance attributes, e.g., _ID, _COLOR, etc. + }; + + InstanceArray() = default; + + void Copy(const InstanceArray &other); + + // Adds one |instance| into this mesh group instance array where the + // |instance.trs| may have optional translation, rotation, and scale set. + Status AddInstance(const Instance &instance); + + // Returns the count of instances in this mesh group instance array. + int NumInstances() const { return instances_.size(); } + + // Returns an instance from this mesh group instance array. + const Instance &GetInstance(int index) const { return instances_[index]; } + + private: + std::vector instances_; +}; + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED +#endif // DRACO_SCENE_INSTANCE_ARRAY_H_ diff --git a/contrib/draco/src/draco/scene/instance_array_test.cc b/contrib/draco/src/draco/scene/instance_array_test.cc new file mode 100644 index 000000000..d3802f901 --- /dev/null +++ b/contrib/draco/src/draco/scene/instance_array_test.cc @@ -0,0 +1,179 @@ +// Copyright 2022 The Draco Authors. +// +// 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 "draco/scene/instance_array.h" + +#include +#include + +#include "draco/core/constants.h" +#include "draco/core/draco_test_base.h" +#include "draco/core/draco_test_utils.h" + +namespace { + +#ifdef DRACO_TRANSCODER_SUPPORTED + +TEST(InstanceArrayTest, TestInstance) { + // Test construction of an empty draco::InstanceArray::Instance struct. + const draco::InstanceArray::Instance instance; + ASSERT_FALSE(instance.trs.TranslationSet()); + ASSERT_FALSE(instance.trs.RotationSet()); + ASSERT_FALSE(instance.trs.ScaleSet()); + ASSERT_FALSE(instance.trs.MatrixSet()); +} + +TEST(InstanceArrayTest, TestDefaults) { + // Test construction of an empty draco::InstanceArray object. + const draco::InstanceArray array; + ASSERT_EQ(array.NumInstances(), 0); +} + +TEST(InstanceArrayTest, TestAddInstance) { + // Test population of draco::InstanceArray object with instances. + draco::InstanceArray array; + + // Create an instance and set its transformation TRS vectors. + const Eigen::Vector3d translation_0(1.0, 2.0, 3.0); + const Eigen::Quaterniond rotation_0(4.0, 5.0, 6.0, 7.0); + const Eigen::Vector3d scale_0(8.0, 9.0, 10.0); + draco::InstanceArray::Instance instance_0; + instance_0.trs.SetTranslation(translation_0); + instance_0.trs.SetRotation(rotation_0); + instance_0.trs.SetScale(scale_0); + + // Create another instance. + const Eigen::Vector3d translation_1(1.1, 2.1, 3.1); + const Eigen::Quaterniond rotation_1(4.1, 5.1, 6.1, 7.1); + const Eigen::Vector3d scale_1(8.1, 9.1, 10.1); + draco::InstanceArray::Instance instance_1; + instance_1.trs.SetTranslation(translation_1); + instance_1.trs.SetRotation(rotation_1); + instance_1.trs.SetScale(scale_1); + + // Add two instances to instance array. + DRACO_ASSERT_OK(array.AddInstance(instance_0)); + DRACO_ASSERT_OK(array.AddInstance(instance_1)); + + // Check that the instances have been added. + ASSERT_EQ(array.NumInstances(), 2); + + // Check transformation of the first instance. + const draco::TrsMatrix &trs_0 = array.GetInstance(0).trs; + ASSERT_TRUE(trs_0.TranslationSet()); + ASSERT_TRUE(trs_0.RotationSet()); + ASSERT_TRUE(trs_0.ScaleSet()); + ASSERT_FALSE(trs_0.MatrixSet()); + ASSERT_EQ(trs_0.Translation().value(), translation_0); + ASSERT_EQ(trs_0.Rotation().value(), rotation_0); + ASSERT_EQ(trs_0.Scale().value(), scale_0); + + // Check transformation of the second instance. + const draco::TrsMatrix &trs_1 = array.GetInstance(1).trs; + ASSERT_TRUE(trs_1.TranslationSet()); + ASSERT_TRUE(trs_1.RotationSet()); + ASSERT_TRUE(trs_1.ScaleSet()); + ASSERT_FALSE(trs_1.MatrixSet()); + ASSERT_EQ(trs_1.Translation().value(), translation_1); + ASSERT_EQ(trs_1.Rotation().value(), rotation_1); + ASSERT_EQ(trs_1.Scale().value(), scale_1); +} + +TEST(InstanceArrayTest, TestAddInstanceWithoutTransform) { + // Test that instance without any transformation can be added. + draco::InstanceArray array; + + // Do not set any transformation. + draco::InstanceArray::Instance instance; + + // Check that such instance can be added. + DRACO_ASSERT_OK(array.AddInstance(instance)); +} + +TEST(InstanceArrayTest, TestAddInstanceWithoutScale) { + // Test that instance without scale can be added. + draco::InstanceArray array; + + // Set only instance translation and rotation. + draco::InstanceArray::Instance instance; + instance.trs.SetTranslation(Eigen::Vector3d(1.0, 2.0, 3.0)); + instance.trs.SetRotation(Eigen::Quaterniond(4.0, 5.0, 6.0, 7.0)); + + // Check that such instance can be added. + DRACO_ASSERT_OK(array.AddInstance(instance)); +} + +TEST(InstanceArrayTest, TestAddInstanceWithMatrixFails) { + // Test that instance without scale cannot be added. + draco::InstanceArray array; + + // Set TRS vectors, as well as the matrix. + draco::InstanceArray::Instance instance; + instance.trs.SetTranslation(Eigen::Vector3d(1.0, 2.0, 3.0)); + instance.trs.SetRotation(Eigen::Quaterniond(4.0, 5.0, 6.0, 7.0)); + instance.trs.SetScale(Eigen::Vector3d(8.0, 9.0, 10.0)); + // clang-format off + Eigen::Matrix4d matrix; + matrix << 1.0, 2.0, 3.0, 4.0, + 5.0, 6.0, 7.0, 8.0, + 9.0, 10.0, 11.0, 12.0, + 13.0, 14.0, 15.0, 16.0; + // clang-format on + instance.trs.SetMatrix(matrix); + + // Check that such instance cannot be added. + const draco::Status status = array.AddInstance(instance); + ASSERT_FALSE(status.ok()); + ASSERT_EQ(status.error_msg_string(), "Instance must have no matrix set."); +} + +TEST(InstanceArrayTest, TestCopy) { + // Test copying of draco::InstanceArray object. + draco::InstanceArray array; + + // Create an instance and set its transformation TRS vectors. + const Eigen::Vector3d translation_0(1.0, 2.0, 3.0); + const Eigen::Quaterniond rotation_0(4.0, 5.0, 6.0, 7.0); + const Eigen::Vector3d scale_0(8.0, 9.0, 10.0); + draco::InstanceArray::Instance instance_0; + instance_0.trs.SetTranslation(translation_0); + instance_0.trs.SetRotation(rotation_0); + instance_0.trs.SetScale(scale_0); + + // Create another instance. + const Eigen::Vector3d translation_1(1.1, 2.1, 3.1); + const Eigen::Quaterniond rotation_1(4.1, 5.1, 6.1, 7.1); + const Eigen::Vector3d scale_1(8.1, 9.1, 10.1); + draco::InstanceArray::Instance instance_1; + instance_1.trs.SetTranslation(translation_1); + instance_1.trs.SetRotation(rotation_1); + instance_1.trs.SetScale(scale_1); + + // Add two instances to the instance array. + DRACO_ASSERT_OK(array.AddInstance(instance_0)); + DRACO_ASSERT_OK(array.AddInstance(instance_1)); + + // Create a copy of the populated instance array object. + draco::InstanceArray copy; + copy.Copy(array); + + // Check that the instances have been copied. + ASSERT_EQ(copy.NumInstances(), 2); + ASSERT_EQ(copy.GetInstance(0).trs, instance_0.trs); + ASSERT_EQ(copy.GetInstance(1).trs, instance_1.trs); +} + +#endif // DRACO_TRANSCODER_SUPPORTED + +} // namespace diff --git a/contrib/draco/src/draco/scene/light.cc b/contrib/draco/src/draco/scene/light.cc new file mode 100644 index 000000000..1fef98c0c --- /dev/null +++ b/contrib/draco/src/draco/scene/light.cc @@ -0,0 +1,45 @@ +// Copyright 2022 The Draco Authors. +// +// 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 "draco/scene/light.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED + +#include + +#include "draco/core/constants.h" + +namespace draco { + +Light::Light() + : color_(1.0f, 1.0f, 1.0f), + intensity_(1.0), + type_(POINT), + range_(std::numeric_limits::max()), // Infinity. + inner_cone_angle_(0.0), + outer_cone_angle_(DRACO_PI / 4.0) {} + +void Light::Copy(const Light &light) { + name_ = light.name_; + color_ = light.color_; + intensity_ = light.intensity_; + type_ = light.type_; + range_ = light.range_; + inner_cone_angle_ = light.inner_cone_angle_; + outer_cone_angle_ = light.outer_cone_angle_; +} + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED diff --git a/contrib/draco/src/draco/scene/light.h b/contrib/draco/src/draco/scene/light.h new file mode 100644 index 000000000..5ff0d4a6b --- /dev/null +++ b/contrib/draco/src/draco/scene/light.h @@ -0,0 +1,81 @@ +// Copyright 2022 The Draco Authors. +// +// 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 DRACO_SCENE_LIGHT_H_ +#define DRACO_SCENE_LIGHT_H_ + +#include "draco/draco_features.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED +#include + +#include "draco/core/vector_d.h" + +namespace draco { + +// Describes a light in a scene according to the KHR_lights_punctual extension. +class Light { + public: + enum Type { DIRECTIONAL, POINT, SPOT }; + + Light(); + + void Copy(const Light &light); + + // Name. + void SetName(const std::string &name) { name_ = name; } + const std::string &GetName() const { return name_; } + + // Color. + void SetColor(const Vector3f &color) { color_ = color; } + const Vector3f &GetColor() const { return color_; } + + // Intensity. + void SetIntensity(double intensity) { intensity_ = intensity; } + double GetIntensity() const { return intensity_; } + + // Type. + void SetType(Type type) { type_ = type; } + Type GetType() const { return type_; } + + // Range. + void SetRange(double range) { range_ = range; } + double GetRange() const { return range_; } + + // Inner cone angle. + void SetInnerConeAngle(double angle) { inner_cone_angle_ = angle; } + double GetInnerConeAngle() const { return inner_cone_angle_; } + + // Outer cone angle. + void SetOuterConeAngle(double angle) { outer_cone_angle_ = angle; } + double GetOuterConeAngle() const { return outer_cone_angle_; } + + private: + std::string name_; + Vector3f color_; + double intensity_; + Type type_; + + // The range is only applicable to lights with Type::POINT or Type::SPOT. + double range_; + + // The cone angles are only applicable to lights with Type::SPOT. + double inner_cone_angle_; + double outer_cone_angle_; +}; + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED +#endif // DRACO_SCENE_LIGHT_H_ diff --git a/contrib/draco/src/draco/scene/light_test.cc b/contrib/draco/src/draco/scene/light_test.cc new file mode 100644 index 000000000..bc24a14ad --- /dev/null +++ b/contrib/draco/src/draco/scene/light_test.cc @@ -0,0 +1,64 @@ +// Copyright 2022 The Draco Authors. +// +// 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 "draco/scene/light.h" + +#include + +#include "draco/core/constants.h" +#include "draco/core/draco_test_base.h" +#include "draco/core/draco_test_utils.h" + +namespace { + +#ifdef DRACO_TRANSCODER_SUPPORTED + +TEST(LightTest, TestDefaults) { + // Text constructing draco::Light object with default properties. + const draco::Light light; + ASSERT_EQ(light.GetName(), ""); + ASSERT_EQ(light.GetColor(), draco::Vector3f(1.0f, 1.0f, 1.0f)); + ASSERT_EQ(light.GetIntensity(), 1.0); + ASSERT_EQ(light.GetType(), draco::Light::POINT); + ASSERT_EQ(light.GetRange(), std::numeric_limits::max()); + ASSERT_EQ(light.GetInnerConeAngle(), 0.0); + ASSERT_EQ(light.GetOuterConeAngle(), DRACO_PI / 4.0); +} + +TEST(LightTest, TestCopy) { + // Test copying of draco::Light object. + draco::Light light; + light.SetName("The Star of Earendil"); + light.SetColor(draco::Vector3f(0.90, 0.97, 1.00)); + light.SetIntensity(5.0); + light.SetType(draco::Light::SPOT); + light.SetRange(1000.0); + light.SetInnerConeAngle(DRACO_PI / 8.0); + light.SetOuterConeAngle(DRACO_PI / 2.0); + + // Create a copy of the initialized light and check all properties. + draco::Light copy; + copy.Copy(light); + ASSERT_EQ(copy.GetName(), "The Star of Earendil"); + ASSERT_EQ(copy.GetColor(), draco::Vector3f(0.90, 0.97, 1.00)); + ASSERT_EQ(copy.GetIntensity(), 5.0); + ASSERT_EQ(copy.GetType(), draco::Light::SPOT); + ASSERT_EQ(copy.GetRange(), 1000.0); + ASSERT_EQ(copy.GetInnerConeAngle(), DRACO_PI / 8.0); + ASSERT_EQ(copy.GetOuterConeAngle(), DRACO_PI / 2.0); +} + +#endif // DRACO_TRANSCODER_SUPPORTED + +} // namespace diff --git a/contrib/draco/src/draco/scene/mesh_group.h b/contrib/draco/src/draco/scene/mesh_group.h new file mode 100644 index 000000000..852318f16 --- /dev/null +++ b/contrib/draco/src/draco/scene/mesh_group.h @@ -0,0 +1,138 @@ +// Copyright 2019 The Draco Authors. +// +// 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 DRACO_SCENE_MESH_GROUP_H_ +#define DRACO_SCENE_MESH_GROUP_H_ + +#include "draco/draco_features.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED +#include +#include + +#include "draco/core/macros.h" +#include "draco/scene/scene_indices.h" + +namespace draco { + +// This class is used to hold ordered mesh instances that refer to one or more +// base meshes, materials, and materials variants mappings. +class MeshGroup { + public: + // Stores a mapping from material index to materials variant indices. Each + // mesh instance may have multiple such mappings associated with it. See glTF + // extension KHR_materials_variants for more details. + struct MaterialsVariantsMapping { + MaterialsVariantsMapping() = delete; + MaterialsVariantsMapping(int material, const std::vector &variants) + : material(material), variants(variants) {} + bool operator==(const MaterialsVariantsMapping &other) const { + if (material != other.material) { + return false; + } + if (variants != other.variants) { + return false; + } + return true; + } + bool operator!=(const MaterialsVariantsMapping &other) const { + return !(*this == other); + } + int material; + std::vector variants; + }; + + // Describes mesh instance stored in a mesh group, including base mesh index, + // material index, and materials variants mappings. + struct MeshInstance { + MeshInstance() = delete; + MeshInstance(MeshIndex mesh_index, int material_index) + : MeshInstance(mesh_index, material_index, {}) {} + MeshInstance(MeshIndex mesh_index, int material_index, + const std::vector + &materials_variants_mappings) + : mesh_index(mesh_index), + material_index(material_index), + materials_variants_mappings(materials_variants_mappings) {} + bool operator==(const MeshInstance &other) const { + if (mesh_index != other.mesh_index) { + return false; + } + if (material_index != other.material_index) { + return false; + } + if (materials_variants_mappings.size() != + other.materials_variants_mappings.size()) { + return false; + } + if (materials_variants_mappings != other.materials_variants_mappings) { + return false; + } + return true; + } + bool operator!=(const MeshInstance &other) const { + return !(*this == other); + } + MeshIndex mesh_index; + int material_index; + std::vector materials_variants_mappings; + }; + + MeshGroup() {} + + void Copy(const MeshGroup &mg) { + name_ = mg.name_; + mesh_instances_ = mg.mesh_instances_; + } + + const std::string &GetName() const { return name_; } + void SetName(const std::string &name) { name_ = name; } + + void AddMeshInstance(const MeshInstance &instance) { + mesh_instances_.push_back(instance); + } + + void SetMeshInstance(int index, const MeshInstance &instance) { + mesh_instances_[index] = instance; + } + + const MeshInstance &GetMeshInstance(int index) const { + return mesh_instances_[index]; + } + + MeshInstance &GetMeshInstance(int index) { return mesh_instances_[index]; } + + int NumMeshInstances() const { return mesh_instances_.size(); } + + // Removes all mesh instances referring to base mesh at |mesh_index|. + void RemoveMeshInstances(MeshIndex mesh_index) { + int i = 0; + while (i != mesh_instances_.size()) { + if (mesh_instances_[i].mesh_index == mesh_index) { + mesh_instances_.erase(mesh_instances_.begin() + i); + } else { + i++; + } + } + } + + private: + std::string name_; + std::vector mesh_instances_; +}; + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED +#endif // DRACO_SCENE_MESH_GROUP_H_ diff --git a/contrib/draco/src/draco/scene/mesh_group_test.cc b/contrib/draco/src/draco/scene/mesh_group_test.cc new file mode 100644 index 000000000..76f1bf33e --- /dev/null +++ b/contrib/draco/src/draco/scene/mesh_group_test.cc @@ -0,0 +1,196 @@ +// Copyright 2020 The Draco Authors. +// +// 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 "draco/scene/mesh_group.h" + +#include "draco/core/draco_test_base.h" +#include "draco/core/draco_test_utils.h" +#include "draco/scene/scene_indices.h" + +namespace { + +#ifdef DRACO_TRANSCODER_SUPPORTED + +using draco::MeshGroup; +using draco::MeshIndex; + +// Test helper method generates materials variants mappings based on a |seed|. +std::vector MakeMappings(int seed) { + MeshGroup::MaterialsVariantsMapping a(10 * seed + 0, {seed + 0, seed + 1}); + MeshGroup::MaterialsVariantsMapping b(10 * seed + 1, {seed + 2, seed + 3}); + return {a, b}; +} + +TEST(MeshGroupTest, TestMeshInstanceTwoArgumentConstructor) { + MeshGroup::MeshInstance instance(MeshIndex(2), 3); + ASSERT_EQ(instance.mesh_index, MeshIndex(2)); + ASSERT_EQ(instance.material_index, 3); + ASSERT_EQ(instance.materials_variants_mappings.size(), 0); +} + +TEST(MeshGroupTest, TestMeshInstanceThreeArgumentConstructor) { + const auto mappings = MakeMappings(4); + MeshGroup::MeshInstance instance(MeshIndex(2), 3, mappings); + ASSERT_EQ(instance.mesh_index, MeshIndex(2)); + ASSERT_EQ(instance.material_index, 3); + ASSERT_EQ(instance.materials_variants_mappings, mappings); +} + +TEST(MeshGroupTest, TestMeshInstanceEqualsOperator) { + MeshGroup::MeshInstance instance_a(MeshIndex(2), 3, MakeMappings(4)); + MeshGroup::MeshInstance instance_b(MeshIndex(2), 3, MakeMappings(4)); + ASSERT_TRUE(instance_a == instance_b); + ASSERT_FALSE(instance_a != instance_b); + + MeshGroup::MeshInstance instance_c(MeshIndex(1), 3, MakeMappings(4)); + MeshGroup::MeshInstance instance_d(MeshIndex(2), 1, MakeMappings(4)); + MeshGroup::MeshInstance instance_e(MeshIndex(2), 3, MakeMappings(1)); + ASSERT_FALSE(instance_a == instance_c); + ASSERT_FALSE(instance_a == instance_d); + ASSERT_FALSE(instance_a == instance_e); + ASSERT_TRUE(instance_a != instance_c); + ASSERT_TRUE(instance_a != instance_d); + ASSERT_TRUE(instance_a != instance_e); +} + +TEST(MeshGroupTest, TestRemoveMeshInstanceWithNoOccurrences) { + // Test that no mesh instances are removed from mesh group when removing the + // instances by the base mesh index that is not in the mesh group. + + // Create test mesh group. + MeshGroup mesh_group; + mesh_group.AddMeshInstance({MeshIndex(1), 0, {}}); + mesh_group.AddMeshInstance({MeshIndex(3), 0, {}}); + + // Try to remove mesh that is not in the mesh group. + mesh_group.RemoveMeshInstances(MeshIndex(2)); + + // Check result. + ASSERT_EQ(mesh_group.NumMeshInstances(), 2); + ASSERT_EQ(mesh_group.GetMeshInstance(0).mesh_index, MeshIndex(1)); + ASSERT_EQ(mesh_group.GetMeshInstance(1).mesh_index, MeshIndex(3)); +} + +TEST(MeshGroupTest, TestRemoveTheOnlyMeshInstance) { + // Test that the only mesh instance can be removed from mesh group. + + // Create test mesh group. + MeshGroup mesh_group; + MeshGroup::MaterialsVariantsMapping mapping(70, {0, 1}); + mesh_group.AddMeshInstance({MeshIndex(7), 70, {mapping}}); + + // Remove a mesh instance. + mesh_group.RemoveMeshInstances(MeshIndex(7)); + + // Check result. + ASSERT_EQ(mesh_group.NumMeshInstances(), 0); +} + +TEST(MeshGroupTest, TestRemoveOneMeshInstances) { + // Test a mesh instance can be removed from mesh group. + + // Create test mesh group. + MeshGroup mesh_group; + mesh_group.AddMeshInstance({MeshIndex(1), 0, {}}); + mesh_group.AddMeshInstance({MeshIndex(3), 0, {}}); + mesh_group.AddMeshInstance({MeshIndex(5), 0, {}}); + mesh_group.AddMeshInstance({MeshIndex(7), 0, {}}); + + // Remove a mesh. + mesh_group.RemoveMeshInstances(MeshIndex(3)); + + // Check result. + ASSERT_EQ(mesh_group.NumMeshInstances(), 3); + ASSERT_EQ(mesh_group.GetMeshInstance(0).mesh_index, MeshIndex(1)); + ASSERT_EQ(mesh_group.GetMeshInstance(1).mesh_index, MeshIndex(5)); + ASSERT_EQ(mesh_group.GetMeshInstance(2).mesh_index, MeshIndex(7)); +} + +TEST(MeshGroupTest, TestRemoveThreeMeshInstances) { + // Test that multiple mesh instances can be removed from a mesh group. + + // Create test mesh group. + MeshGroup mesh_group; + mesh_group.AddMeshInstance({MeshIndex(1), 10, MakeMappings(1)}); + mesh_group.AddMeshInstance({MeshIndex(3), 30, MakeMappings(3)}); + mesh_group.AddMeshInstance({MeshIndex(5), 50, MakeMappings(5)}); + mesh_group.AddMeshInstance({MeshIndex(1), 10, MakeMappings(1)}); + mesh_group.AddMeshInstance({MeshIndex(7), 70, MakeMappings(7)}); + mesh_group.AddMeshInstance({MeshIndex(1), 10, MakeMappings(1)}); + + // Remove mesh instances. + mesh_group.RemoveMeshInstances(MeshIndex(1)); + + // Check result. + ASSERT_EQ(mesh_group.NumMeshInstances(), 3); + const MeshGroup::MeshInstance mi0 = mesh_group.GetMeshInstance(0); + const MeshGroup::MeshInstance mi1 = mesh_group.GetMeshInstance(1); + const MeshGroup::MeshInstance mi2 = mesh_group.GetMeshInstance(2); + ASSERT_EQ(mi0.mesh_index, MeshIndex(3)); + ASSERT_EQ(mi1.mesh_index, MeshIndex(5)); + ASSERT_EQ(mi2.mesh_index, MeshIndex(7)); + ASSERT_EQ(mi0.material_index, 30); + ASSERT_EQ(mi1.material_index, 50); + ASSERT_EQ(mi2.material_index, 70); + ASSERT_EQ(mi0.materials_variants_mappings, MakeMappings(3)); + ASSERT_EQ(mi1.materials_variants_mappings, MakeMappings(5)); + ASSERT_EQ(mi2.materials_variants_mappings, MakeMappings(7)); +} + +TEST(MeshGroupTest, TestCopy) { + // Tests that a mesh group can be copied. + + // Create test mesh group. + MeshGroup mesh_group; + mesh_group.SetName("Mesh-1-3-5-7"); + mesh_group.AddMeshInstance({MeshIndex(1), 10, MakeMappings(1)}); + mesh_group.AddMeshInstance({MeshIndex(3), 30, MakeMappings(3)}); + mesh_group.AddMeshInstance({MeshIndex(5), 50, MakeMappings(5)}); + mesh_group.AddMeshInstance({MeshIndex(7), 70, MakeMappings(7)}); + + // Verify source MeshGroup. + ASSERT_EQ(mesh_group.GetName(), "Mesh-1-3-5-7"); + ASSERT_EQ(mesh_group.NumMeshInstances(), 4); + const MeshGroup::MeshInstance mi0 = mesh_group.GetMeshInstance(0); + const MeshGroup::MeshInstance mi1 = mesh_group.GetMeshInstance(1); + const MeshGroup::MeshInstance mi2 = mesh_group.GetMeshInstance(2); + const MeshGroup::MeshInstance mi3 = mesh_group.GetMeshInstance(3); + ASSERT_EQ(mi0.mesh_index, MeshIndex(1)); + ASSERT_EQ(mi1.mesh_index, MeshIndex(3)); + ASSERT_EQ(mi2.mesh_index, MeshIndex(5)); + ASSERT_EQ(mi3.mesh_index, MeshIndex(7)); + ASSERT_EQ(mi0.material_index, 10); + ASSERT_EQ(mi1.material_index, 30); + ASSERT_EQ(mi2.material_index, 50); + ASSERT_EQ(mi3.material_index, 70); + ASSERT_EQ(mi0.materials_variants_mappings, MakeMappings(1)); + ASSERT_EQ(mi1.materials_variants_mappings, MakeMappings(3)); + ASSERT_EQ(mi2.materials_variants_mappings, MakeMappings(5)); + ASSERT_EQ(mi3.materials_variants_mappings, MakeMappings(7)); + + MeshGroup copy; + copy.Copy(mesh_group); + + // Verify that Copy worked. + ASSERT_EQ(mesh_group.GetName(), copy.GetName()); + ASSERT_EQ(mesh_group.NumMeshInstances(), copy.NumMeshInstances()); + ASSERT_EQ(mesh_group.GetMeshInstance(0), copy.GetMeshInstance(0)); + ASSERT_EQ(mesh_group.GetMeshInstance(1), copy.GetMeshInstance(1)); + ASSERT_EQ(mesh_group.GetMeshInstance(2), copy.GetMeshInstance(2)); + ASSERT_EQ(mesh_group.GetMeshInstance(3), copy.GetMeshInstance(3)); +} + +#endif // DRACO_TRANSCODER_SUPPORTED + +} // namespace diff --git a/contrib/draco/src/draco/scene/scene.cc b/contrib/draco/src/draco/scene/scene.cc new file mode 100644 index 000000000..9ad574835 --- /dev/null +++ b/contrib/draco/src/draco/scene/scene.cc @@ -0,0 +1,174 @@ +// Copyright 2019 The Draco Authors. +// +// 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 "draco/scene/scene.h" + +#include + +#ifdef DRACO_TRANSCODER_SUPPORTED +#include "draco/core/macros.h" +#include "draco/scene/scene_indices.h" + +namespace draco { + +void Scene::Copy(const Scene &s) { + meshes_.resize(s.meshes_.size()); + for (MeshIndex i(0); i < meshes_.size(); ++i) { + meshes_[i] = std::unique_ptr(new Mesh()); + meshes_[i]->Copy(*s.meshes_[i]); + } + + mesh_groups_.resize(s.mesh_groups_.size()); + for (MeshGroupIndex i(0); i < mesh_groups_.size(); ++i) { + mesh_groups_[i] = std::unique_ptr(new MeshGroup()); + mesh_groups_[i]->Copy(*s.mesh_groups_[i]); + } + + nodes_.resize(s.nodes_.size()); + for (SceneNodeIndex i(0); i < nodes_.size(); ++i) { + nodes_[i] = std::unique_ptr(new SceneNode()); + nodes_[i]->Copy(*s.nodes_[i]); + } + + root_node_indices_ = s.root_node_indices_; + + animations_.resize(s.animations_.size()); + for (AnimationIndex i(0); i < animations_.size(); ++i) { + animations_[i] = std::unique_ptr(new Animation()); + animations_[i]->Copy(*s.animations_[i]); + } + + skins_.resize(s.skins_.size()); + for (SkinIndex i(0); i < skins_.size(); ++i) { + skins_[i] = std::unique_ptr(new Skin()); + skins_[i]->Copy(*s.skins_[i]); + } + + lights_.resize(s.lights_.size()); + for (LightIndex i(0); i < lights_.size(); ++i) { + lights_[i] = std::unique_ptr(new Light()); + lights_[i]->Copy(*s.lights_[i]); + } + + instance_arrays_.resize(s.instance_arrays_.size()); + for (InstanceArrayIndex i(0); i < instance_arrays_.size(); ++i) { + instance_arrays_[i] = std::unique_ptr(new InstanceArray()); + instance_arrays_[i]->Copy(*s.instance_arrays_[i]); + } + + material_library_.Copy(s.material_library_); + +#ifdef DRACO_TRANSCODER_SUPPORTED + // Copy non-material textures. + non_material_texture_library_.Copy(s.non_material_texture_library_); + + // Update pointers to non-material textures in mesh feature ID sets of all + // scene meshes. + if (non_material_texture_library_.NumTextures() != 0) { + const auto texture_to_index_map = + s.non_material_texture_library_.ComputeTextureToIndexMap(); + for (MeshIndex i(0); i < NumMeshes(); ++i) { + for (MeshFeaturesIndex j(0); j < GetMesh(i).NumMeshFeatures(); ++j) { + Mesh::UpdateMeshFeaturesTexturePointer(texture_to_index_map, + &non_material_texture_library_, + &GetMesh(i).GetMeshFeatures(j)); + } + } + } +#endif // DRACO_TRANSCODER_SUPPORTED + + // Copy structural metadata. + structural_metadata_.Copy(s.structural_metadata_); +} + +Status Scene::RemoveMesh(MeshIndex index) { + // Remove base mesh at |index| from |meshes_| and corresponding material index + // from |mesh_material_indices_|. + const int new_num_meshes = meshes_.size() - 1; + for (MeshIndex i(index); i < new_num_meshes; i++) { + meshes_[i] = std::move(meshes_[i + 1]); + } + meshes_.resize(new_num_meshes); + + // Remove references to removed base mesh and corresponding materials from + // mesh groups, and update references to remaining base meshes in mesh groups. + for (MeshGroupIndex mgi(0); mgi < NumMeshGroups(); ++mgi) { + MeshGroup *const mesh_group = GetMeshGroup(mgi); + if (!mesh_group) { + return Status(Status::DRACO_ERROR, "MeshGroup is null."); + } + mesh_group->RemoveMeshInstances(index); + for (int i = 0; i < mesh_group->NumMeshInstances(); ++i) { + MeshGroup::MeshInstance &mesh_instance = mesh_group->GetMeshInstance(i); + if (mesh_instance.mesh_index > index && + mesh_instance.mesh_index != kInvalidMeshIndex) { + mesh_instance.mesh_index--; + } + } + } + return OkStatus(); +} + +Status Scene::RemoveMeshGroup(MeshGroupIndex index) { + // Remove mesh group at |index| from |mesh_groups_| vector. + const int new_num_mesh_groups = mesh_groups_.size() - 1; + for (MeshGroupIndex i(index); i < new_num_mesh_groups; i++) { + mesh_groups_[i] = std::move(mesh_groups_[i + 1]); + } + mesh_groups_.resize(new_num_mesh_groups); + + // Invalidate references to removed mesh group in scene nodes, and update + // references to remaining mesh groups in scene nodes. + for (SceneNodeIndex sni(0); sni < NumNodes(); ++sni) { + SceneNode *node = GetNode(sni); + if (!node) { + return Status(Status::DRACO_ERROR, "Node is null."); + } + const MeshGroupIndex mgi = node->GetMeshGroupIndex(); + if (mgi == index) { + // TODO(vytyaz): Remove the node if possible, e.g., when node has no + // geometry, no child nodes, no skins, no lights, and no mesh group + // instance arrays. + node->SetMeshGroupIndex(kInvalidMeshGroupIndex); + } else if (mgi > index && mgi != kInvalidMeshGroupIndex) { + node->SetMeshGroupIndex(mgi - 1); + } + } + return OkStatus(); +} + +Status Scene::RemoveMaterial(int index) { + if (index < 0 || index >= material_library_.NumMaterials()) { + return Status(Status::DRACO_ERROR, "Material index is out of range."); + } + material_library_.RemoveMaterial(index); + + // Update material indices of mesh instances. + for (MeshGroupIndex mgi(0); mgi < NumMeshGroups(); ++mgi) { + MeshGroup *const mesh_group = GetMeshGroup(mgi); + for (int i = 0; i < mesh_group->NumMeshInstances(); i++) { + MeshGroup::MeshInstance &mesh_instance = mesh_group->GetMeshInstance(i); + if (mesh_instance.material_index > index) { + mesh_instance.material_index--; + } else if (mesh_instance.material_index == index) { + return Status(Status::DRACO_ERROR, "Removed material has references."); + } + } + } + return OkStatus(); +} + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED diff --git a/contrib/draco/src/draco/scene/scene.h b/contrib/draco/src/draco/scene/scene.h new file mode 100644 index 000000000..3c76ead7a --- /dev/null +++ b/contrib/draco/src/draco/scene/scene.h @@ -0,0 +1,258 @@ +// Copyright 2019 The Draco Authors. +// +// 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 DRACO_SCENE_SCENE_H_ +#define DRACO_SCENE_SCENE_H_ + +#include "draco/draco_features.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED +#include + +#include "draco/animation/animation.h" +#include "draco/animation/skin.h" +#include "draco/mesh/mesh.h" +#include "draco/metadata/structural_metadata.h" +#include "draco/scene/instance_array.h" +#include "draco/scene/light.h" +#include "draco/scene/mesh_group.h" +#include "draco/scene/scene_indices.h" +#include "draco/scene/scene_node.h" + +namespace draco { + +// Class used to hold all of the geometry to create a scene. A scene is +// comprised of one or more meshes, one or more scene nodes, one or more +// mesh groups, and a material library. The meshes are defined in their +// local space. A mesh group is a list of meshes. The scene nodes create +// a scene hierarchy to transform meshes in their local space into scene space. +// The material library contains all of the materials and textures used by the +// meshes in this scene. +class Scene { + public: + Scene() {} + + void Copy(const Scene &s); + + // Adds a Draco |mesh| to the scene. Returns the index to the stored mesh or + // |kInvalidMeshIndex| if the mesh is a nullptr. + MeshIndex AddMesh(std::unique_ptr mesh) { + if (mesh == nullptr) { + return kInvalidMeshIndex; + } + meshes_.push_back(std::move(mesh)); + return MeshIndex(meshes_.size() - 1); + } + + // Removes base mesh and corresponding material at |index|, removes references + // to removed base mesh and corresponding materials from mesh groups, and + // updates references to remaining base meshes in mesh groups. + Status RemoveMesh(MeshIndex index); + + // Returns the number of meshes in a scene before instancing is applied. + int NumMeshes() const { return meshes_.size(); } + + // Returns a mesh in the scene before instancing is applied. The mesh + // coordinates are local to the mesh. + Mesh &GetMesh(MeshIndex index) { return *meshes_[index]; } + const Mesh &GetMesh(MeshIndex index) const { return *meshes_[index]; } + + // Creates a mesh group and returns the index to the mesh group. + MeshGroupIndex AddMeshGroup() { + std::unique_ptr mesh(new MeshGroup()); + mesh_groups_.push_back(std::move(mesh)); + return MeshGroupIndex(mesh_groups_.size() - 1); + } + + // Removes mesh group at |index|, invalidates references to removed mesh group + // in scene nodes, and updates references to remaining mesh groups in scene + // nodes. + Status RemoveMeshGroup(MeshGroupIndex index); + + // Removes unused material at |index| and updates references to materials at + // indices greater than |index|. Returns error status when |index| is out of + // valid range and when material at |index| is used in the scene. + Status RemoveMaterial(int index); + + // Returns the number of mesh groups in a scene. + int NumMeshGroups() const { return mesh_groups_.size(); } + + // Returns a mesh group in the scene. + MeshGroup *GetMeshGroup(MeshGroupIndex index) { + return mesh_groups_[index].get(); + } + const MeshGroup *GetMeshGroup(MeshGroupIndex index) const { + return mesh_groups_[index].get(); + } + + // Creates a scene node and returns the index to the node. + SceneNodeIndex AddNode() { + std::unique_ptr node(new SceneNode()); + nodes_.push_back(std::move(node)); + return SceneNodeIndex(nodes_.size() - 1); + } + + // Returns the number of nodes in a scene. + int NumNodes() const { return nodes_.size(); } + + // Returns a node in the scene. + SceneNode *GetNode(SceneNodeIndex index) { return nodes_[index].get(); } + const SceneNode *GetNode(SceneNodeIndex index) const { + return nodes_[index].get(); + } + + // Either allocates new nodes or removes existing nodes that are beyond + // |num_nodes|. + void ResizeNodes(int num_nodes) { + const size_t old_num_nodes = nodes_.size(); + nodes_.resize(num_nodes); + for (SceneNodeIndex i(old_num_nodes); i < num_nodes; ++i) { + nodes_[i].reset(new SceneNode()); + } + } + + // Returns the number of root node indices in a scene. + int NumRootNodes() const { return root_node_indices_.size(); } + SceneNodeIndex GetRootNodeIndex(int i) const { return root_node_indices_[i]; } + const std::vector &GetRootNodeIndices() const { + return root_node_indices_; + } + void AddRootNodeIndex(SceneNodeIndex index) { + root_node_indices_.push_back(index); + } + void SetRootNodeIndex(int i, SceneNodeIndex index) { + root_node_indices_[i] = index; + } + void RemoveAllRootNodeIndices() { root_node_indices_.clear(); } + + const MaterialLibrary &GetMaterialLibrary() const { + return material_library_; + } + MaterialLibrary &GetMaterialLibrary() { return material_library_; } + + // Library that contains non-material textures. + const TextureLibrary &GetNonMaterialTextureLibrary() const { + return non_material_texture_library_; + } + TextureLibrary &GetNonMaterialTextureLibrary() { + return non_material_texture_library_; + } + + // Structural metadata. + const StructuralMetadata &GetStructuralMetadata() const { + return structural_metadata_; + } + StructuralMetadata &GetStructuralMetadata() { return structural_metadata_; } + + // Creates an animation and returns the index to the animation. + AnimationIndex AddAnimation() { + std::unique_ptr animation(new Animation()); + animations_.push_back(std::move(animation)); + return AnimationIndex(animations_.size() - 1); + } + + // Returns the number of animations in a scene. + int NumAnimations() const { return animations_.size(); } + + // Returns an animation in the scene. + Animation *GetAnimation(AnimationIndex index) { + return animations_[index].get(); + } + const Animation *GetAnimation(AnimationIndex index) const { + return animations_[index].get(); + } + + // Creates a skin and returns the index to the skin. + SkinIndex AddSkin() { + std::unique_ptr skin(new Skin()); + skins_.push_back(std::move(skin)); + return SkinIndex(skins_.size() - 1); + } + + // Returns the number of skins in a scene. + int NumSkins() const { return skins_.size(); } + + // Returns a skin in the scene. + Skin *GetSkin(SkinIndex index) { return skins_[index].get(); } + const Skin *GetSkin(SkinIndex index) const { return skins_[index].get(); } + + // Creates a light and returns the index to the light. + LightIndex AddLight() { + std::unique_ptr light(new Light()); + lights_.push_back(std::move(light)); + return LightIndex(lights_.size() - 1); + } + + // Returns the number of lights in a scene. + int NumLights() const { return lights_.size(); } + + // Returns a light in the scene. + Light *GetLight(LightIndex index) { return lights_[index].get(); } + const Light *GetLight(LightIndex index) const { return lights_[index].get(); } + + // Creates a mesh group instance array and returns the index to it. This array + // is used for storing the attributes of the EXT_mesh_gpu_instancing glTF + // extension. + InstanceArrayIndex AddInstanceArray() { + std::unique_ptr array(new InstanceArray()); + instance_arrays_.push_back(std::move(array)); + return InstanceArrayIndex(instance_arrays_.size() - 1); + } + + // Returns the number of mesh group instance arrays in a scene. + int NumInstanceArrays() const { return instance_arrays_.size(); } + + // Returns a mesh group instance array in the scene. + InstanceArray *GetInstanceArray(InstanceArrayIndex index) { + return instance_arrays_[index].get(); + } + const InstanceArray *GetInstanceArray(InstanceArrayIndex index) const { + return instance_arrays_[index].get(); + } + + private: + IndexTypeVector> meshes_; + IndexTypeVector> mesh_groups_; + IndexTypeVector> nodes_; + std::vector root_node_indices_; + IndexTypeVector> animations_; + IndexTypeVector> skins_; + + // The lights will be written to the output scene but not used for internal + // rendering in Draco, e.g, while computing distortion metric. + IndexTypeVector> lights_; + + // The mesh group instance array information will be written to the output + // scene but not processed by Draco simplifier modules. + IndexTypeVector> + instance_arrays_; + + // Materials used by this scene. + MaterialLibrary material_library_; + + // Texture library for storing non-material textures used by this scene, e.g., + // textures containing mesh feature IDs of EXT_mesh_features glTF extension. + // Note that scene meshes contain pointers to non-material textures. It is + // responsibility of class user to update these pointers when updating the + // textures. See Scene::Copy() for example. + TextureLibrary non_material_texture_library_; + + // Structural metadata defined by the EXT_structural_metadata glTF extension. + StructuralMetadata structural_metadata_; +}; + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED +#endif // DRACO_SCENE_SCENE_H_ diff --git a/contrib/draco/src/draco/scene/scene_are_equivalent.cc b/contrib/draco/src/draco/scene/scene_are_equivalent.cc new file mode 100644 index 000000000..7d0663e08 --- /dev/null +++ b/contrib/draco/src/draco/scene/scene_are_equivalent.cc @@ -0,0 +1,109 @@ +// Copyright 2019 The Draco Authors. +// +// 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 "draco/scene/scene_are_equivalent.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED +#include "draco/mesh/mesh_are_equivalent.h" + +namespace draco { + +bool SceneAreEquivalent::operator()(const Scene &scene0, const Scene &scene1) { + // Check scene component sizes. + if (scene0.NumAnimations() != scene1.NumAnimations()) { + return false; + } + if (scene0.NumMeshGroups() != scene1.NumMeshGroups()) { + return false; + } + if (scene0.NumSkins() != scene1.NumSkins()) { + return false; + } + + // Check equivalence of each mesh. + if (scene0.NumMeshes() != scene1.NumMeshes()) { + return false; + } + for (MeshIndex i(0); i < scene0.NumMeshes(); i++) { + if (!AreEquivalent(scene0.GetMesh(i), scene1.GetMesh(i))) { + return false; + } + } + + // Check eqiuvalence of each node. + if (scene0.NumNodes() != scene1.NumNodes()) { + return false; + } + for (SceneNodeIndex i(0); i < scene0.NumNodes(); i++) { + if (!AreEquivalent(*scene0.GetNode(i), *scene1.GetNode(i))) { + return false; + } + } + + // Check non-material texture library sizes. + if (scene0.GetNonMaterialTextureLibrary().NumTextures() != + scene1.GetNonMaterialTextureLibrary().NumTextures()) { + return false; + } + + // TODO(vytyaz): Check remaining scene properties like animations and skins. + return true; +} + +bool SceneAreEquivalent::AreEquivalent(const Mesh &mesh0, const Mesh &mesh1) { + MeshAreEquivalent eq; + return eq(mesh0, mesh1); +} + +bool SceneAreEquivalent::AreEquivalent(const SceneNode &node0, + const SceneNode &node1) { + // Check equivalence of node indices. + if (node0.GetMeshGroupIndex() != node1.GetMeshGroupIndex()) { + return false; + } + if (node0.GetSkinIndex() != node1.GetSkinIndex()) { + return false; + } + + // Check equivalence of node transformations. + if (node0.GetTrsMatrix().ComputeTransformationMatrix() != + node1.GetTrsMatrix().ComputeTransformationMatrix()) { + return false; + } + + // Check equivalence of node children. + if (node0.NumChildren() != node1.NumChildren()) { + return false; + } + for (int i = 0; i < node0.NumChildren(); i++) { + if (node0.Child(i) != node1.Child(i)) { + return false; + } + } + + // Check equivalence of node parents. + if (node0.NumParents() != node1.NumParents()) { + return false; + } + for (int i = 0; i < node0.NumParents(); i++) { + if (node0.Parent(i) != node1.Parent(i)) { + return false; + } + } + return true; +} + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED diff --git a/contrib/draco/src/draco/scene/scene_are_equivalent.h b/contrib/draco/src/draco/scene/scene_are_equivalent.h new file mode 100644 index 000000000..b309c0338 --- /dev/null +++ b/contrib/draco/src/draco/scene/scene_are_equivalent.h @@ -0,0 +1,42 @@ +// Copyright 2019 The Draco Authors. +// +// 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 DRACO_SCENE_SCENE_ARE_EQUIVALENT_H_ +#define DRACO_SCENE_SCENE_ARE_EQUIVALENT_H_ + +#include "draco/draco_features.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED +#include "draco/scene/scene.h" + +namespace draco { + +// A functor to compare two scenes for equivalency up to permutation of mesh +// vertices. +class SceneAreEquivalent { + public: + // Returns true if both scenes are equivalent up to permutation of + // the internal order of mesh vertices. This includes all attributes. + bool operator()(const Scene &scene0, const Scene &scene1); + + private: + static bool AreEquivalent(const Mesh &mesh0, const Mesh &mesh1); + static bool AreEquivalent(const SceneNode &node0, const SceneNode &node1); +}; + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED +#endif // DRACO_SCENE_SCENE_ARE_EQUIVALENT_H_ diff --git a/contrib/draco/src/draco/scene/scene_are_equivalent_test.cc b/contrib/draco/src/draco/scene/scene_are_equivalent_test.cc new file mode 100644 index 000000000..3a9edc6c3 --- /dev/null +++ b/contrib/draco/src/draco/scene/scene_are_equivalent_test.cc @@ -0,0 +1,86 @@ +// Copyright 2019 The Draco Authors. +// +// 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 "draco/scene/scene_are_equivalent.h" + +#include +#include +#include +#include + +#include "draco/core/draco_test_base.h" +#include "draco/core/draco_test_utils.h" +#include "draco/io/scene_io.h" +#include "draco/scene/scene.h" + +namespace draco { + +#ifdef DRACO_TRANSCODER_SUPPORTED +class SceneAreEquivalentTest : public ::testing::Test {}; + +TEST_F(SceneAreEquivalentTest, TestOnIndenticalScenes) { + const std::string file_name = "CesiumMilkTruck/glTF/CesiumMilkTruck.gltf"; + const std::unique_ptr scene(ReadSceneFromTestFile(file_name)); + ASSERT_NE(scene, nullptr) << "Failed to load test scene: " << file_name; + + // Add mesh feature ID set to a scene mesh. + std::unique_ptr mesh_features(new MeshFeatures()); + scene->GetMesh(MeshIndex(2)).AddMeshFeatures(std::move(mesh_features)); + + SceneAreEquivalent equiv; + ASSERT_TRUE(equiv(*scene, *scene)); +} + +TEST_F(SceneAreEquivalentTest, TestOnDifferentScenes) { + const std::string file_name0 = "CesiumMilkTruck/glTF/CesiumMilkTruck.gltf"; + const std::string file_name1 = "Lantern/glTF/Lantern.gltf"; + const std::unique_ptr scene0(ReadSceneFromTestFile(file_name0)); + const std::unique_ptr scene1(ReadSceneFromTestFile(file_name1)); + ASSERT_NE(scene0, nullptr) << "Failed to load test scene: " << file_name0; + ASSERT_NE(scene1, nullptr) << "Failed to load test scene: " << file_name1; + SceneAreEquivalent equiv; + ASSERT_FALSE(equiv(*scene0, *scene1)); +} + +TEST_F(SceneAreEquivalentTest, TestMeshFeatures) { + const std::string file_name = "CesiumMilkTruck/glTF/CesiumMilkTruck.gltf"; + const std::unique_ptr scene0(ReadSceneFromTestFile(file_name)); + const std::unique_ptr scene1(ReadSceneFromTestFile(file_name)); + ASSERT_NE(scene0, nullptr); + ASSERT_NE(scene1, nullptr); + + // Add identical mesh feature ID sets to mesh at index 0. + Mesh &mesh0 = scene0->GetMesh(MeshIndex(0)); + Mesh &mesh1 = scene1->GetMesh(MeshIndex(0)); + mesh0.AddMeshFeatures(std::unique_ptr(new MeshFeatures())); + mesh1.AddMeshFeatures(std::unique_ptr(new MeshFeatures())); + + // Empty feature sets should match. + SceneAreEquivalent equiv; + ASSERT_TRUE(equiv(*scene0, *scene1)); + + // Make mesh features different and check that the meshes are not equivalent. + mesh0.GetMeshFeatures(MeshFeaturesIndex(0)).SetFeatureCount(5); + mesh1.GetMeshFeatures(MeshFeaturesIndex(0)).SetFeatureCount(6); + ASSERT_FALSE(equiv(*scene0, *scene1)); + + // Make mesh features identical and check that the meshes are equivalent. + mesh0.GetMeshFeatures(MeshFeaturesIndex(0)).SetFeatureCount(1); + mesh1.GetMeshFeatures(MeshFeaturesIndex(0)).SetFeatureCount(1); + ASSERT_TRUE(equiv(*scene0, *scene1)); +} + +#endif // DRACO_TRANSCODER_SUPPORTED + +} // namespace draco diff --git a/contrib/draco/src/draco/scene/scene_indices.h b/contrib/draco/src/draco/scene/scene_indices.h new file mode 100644 index 000000000..7b57e3e4a --- /dev/null +++ b/contrib/draco/src/draco/scene/scene_indices.h @@ -0,0 +1,72 @@ +// Copyright 2019 The Draco Authors. +// +// 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. +// +#ifdef DRACO_TRANSCODER_SUPPORTED +#ifndef DRACO_SCENE_SCENE_INDICES_H_ +#define DRACO_SCENE_SCENE_INDICES_H_ + +#include + +#include + +#include "draco/core/draco_index_type.h" + +namespace draco { + +// Index of a mesh in a scene. +DEFINE_NEW_DRACO_INDEX_TYPE(uint32_t, MeshIndex) + +// Index of a mesh instance in a scene. +DEFINE_NEW_DRACO_INDEX_TYPE(uint32_t, MeshInstanceIndex) + +// Index of a mesh group in a scene. +DEFINE_NEW_DRACO_INDEX_TYPE(uint32_t, MeshGroupIndex) + +// Index of a node in a scene. +DEFINE_NEW_DRACO_INDEX_TYPE(uint32_t, SceneNodeIndex) + +// Index of an animation in a scene. +DEFINE_NEW_DRACO_INDEX_TYPE(uint32_t, AnimationIndex) + +// Index of a skin in a scene. +DEFINE_NEW_DRACO_INDEX_TYPE(uint32_t, SkinIndex) + +// Index of a light in a scene. +DEFINE_NEW_DRACO_INDEX_TYPE(uint32_t, LightIndex) + +// Index of a mesh group GPU instancing in a scene. +DEFINE_NEW_DRACO_INDEX_TYPE(uint32_t, InstanceArrayIndex) + +// Constants denoting invalid indices. +static constexpr MeshIndex kInvalidMeshIndex( + std::numeric_limits::max()); +static constexpr MeshInstanceIndex kInvalidMeshInstanceIndex( + std::numeric_limits::max()); +static constexpr MeshGroupIndex kInvalidMeshGroupIndex( + std::numeric_limits::max()); +static constexpr SceneNodeIndex kInvalidSceneNodeIndex( + std::numeric_limits::max()); +static constexpr AnimationIndex kInvalidAnimationIndex( + std::numeric_limits::max()); +static constexpr SkinIndex kInvalidSkinIndex( + std::numeric_limits::max()); +static constexpr LightIndex kInvalidLightIndex( + std::numeric_limits::max()); +static constexpr InstanceArrayIndex kInvalidInstanceArrayIndex( + std::numeric_limits::max()); + +} // namespace draco + +#endif // DRACO_SCENE_SCENE_INDICES_H_ +#endif // DRACO_TRANSCODER_SUPPORTED diff --git a/contrib/draco/src/draco/scene/scene_node.h b/contrib/draco/src/draco/scene/scene_node.h new file mode 100644 index 000000000..6cfe04e2e --- /dev/null +++ b/contrib/draco/src/draco/scene/scene_node.h @@ -0,0 +1,105 @@ +// Copyright 2019 The Draco Authors. +// +// 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. +// +#ifdef DRACO_TRANSCODER_SUPPORTED +#ifndef DRACO_SCENE_SCENE_NODE_H_ +#define DRACO_SCENE_SCENE_NODE_H_ + +#include "draco/scene/scene_indices.h" +#include "draco/scene/trs_matrix.h" + +namespace draco { + +// This class is used to create a scene hierarchy from meshes in their local +// space transformed into scene space. +class SceneNode { + public: + SceneNode() + : mesh_group_index_(-1), + skin_index_(-1), + light_index_(-1), + instance_array_index_(-1) {} + + void Copy(const SceneNode &sn) { + name_ = sn.name_; + trs_matrix_.Copy(sn.trs_matrix_); + mesh_group_index_ = sn.mesh_group_index_; + skin_index_ = sn.skin_index_; + parents_ = sn.parents_; + children_ = sn.children_; + light_index_ = sn.light_index_; + instance_array_index_ = sn.instance_array_index_; + } + + // Sets a name. + void SetName(const std::string &name) { name_ = name; } + + // Returns the name. + const std::string &GetName() const { return name_; } + + // Set transformation from mesh local space to scene space. + void SetTrsMatrix(const TrsMatrix &trsm) { trs_matrix_.Copy(trsm); } + const TrsMatrix &GetTrsMatrix() const { return trs_matrix_; } + + // Set the index to the mesh group in the scene. + void SetMeshGroupIndex(MeshGroupIndex index) { mesh_group_index_ = index; } + MeshGroupIndex GetMeshGroupIndex() const { return mesh_group_index_; } + + // Set the index to the skin in the scene. + void SetSkinIndex(SkinIndex index) { skin_index_ = index; } + SkinIndex GetSkinIndex() const { return skin_index_; } + + // Set the index to the light in the scene. + void SetLightIndex(LightIndex index) { light_index_ = index; } + LightIndex GetLightIndex() const { return light_index_; } + + // Set the index to the mesh group instance array in the scene. Note that + // according to EXT_mesh_gpu_instancing glTF extension there is no defined + // behavior for a node with instance array and without a mesh group. + void SetInstanceArrayIndex(InstanceArrayIndex index) { + instance_array_index_ = index; + } + InstanceArrayIndex GetInstanceArrayIndex() const { + return instance_array_index_; + } + + // Functions to set and get zero or more parent nodes of this node. + SceneNodeIndex Parent(int index) const { return parents_[index]; } + const std::vector &Parents() const { return parents_; } + void AddParentIndex(SceneNodeIndex index) { parents_.push_back(index); } + int NumParents() const { return parents_.size(); } + void RemoveAllParents() { parents_.clear(); } + + // Functions to set and get zero or more child nodes of this node. + SceneNodeIndex Child(int index) const { return children_[index]; } + const std::vector &Children() const { return children_; } + void AddChildIndex(SceneNodeIndex index) { children_.push_back(index); } + int NumChildren() const { return children_.size(); } + void RemoveAllChildren() { children_.clear(); } + + private: + std::string name_; + TrsMatrix trs_matrix_; + draco::MeshGroupIndex mesh_group_index_; + draco::SkinIndex skin_index_; + std::vector parents_; + std::vector children_; + LightIndex light_index_; + InstanceArrayIndex instance_array_index_; +}; + +} // namespace draco + +#endif // DRACO_SCENE_SCENE_NODE_H_ +#endif // DRACO_TRANSCODER_SUPPORTED diff --git a/contrib/draco/src/draco/scene/scene_test.cc b/contrib/draco/src/draco/scene/scene_test.cc new file mode 100644 index 000000000..d639614c7 --- /dev/null +++ b/contrib/draco/src/draco/scene/scene_test.cc @@ -0,0 +1,295 @@ +// Copyright 2019 The Draco Authors. +// +// 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 "draco/scene/scene.h" + +#include +#include +#include + +#include "draco/core/draco_test_base.h" +#include "draco/core/draco_test_utils.h" +#include "draco/core/status.h" +#include "draco/mesh/mesh_are_equivalent.h" +#include "draco/scene/scene_are_equivalent.h" +#include "draco/scene/scene_indices.h" + +namespace { + +#ifdef DRACO_TRANSCODER_SUPPORTED + +// Helper method for adding mesh group GPU instancing to the milk truck scene. +draco::Status AddGpuInstancingToMilkTruck(draco::Scene *scene) { + // Create an instance and set its transformation TRS vectors. + draco::InstanceArray::Instance instance_0; + instance_0.trs.SetTranslation(Eigen::Vector3d(1.0, 2.0, 3.0)); + instance_0.trs.SetRotation(Eigen::Quaterniond(4.0, 5.0, 6.0, 7.0)); + instance_0.trs.SetScale(Eigen::Vector3d(8.0, 9.0, 10.0)); + + // Create another instance. + draco::InstanceArray::Instance instance_1; + instance_1.trs.SetTranslation(Eigen::Vector3d(1.1, 2.1, 3.1)); + instance_1.trs.SetRotation(Eigen::Quaterniond(4.1, 5.1, 6.1, 7.1)); + instance_1.trs.SetScale(Eigen::Vector3d(8.1, 9.1, 10.1)); + + // Add an empty GPU instancing object to the scene. + const draco::InstanceArrayIndex index = scene->AddInstanceArray(); + draco::InstanceArray *gpu_instancing = scene->GetInstanceArray(index); + + // Add two instances to the GPU instancing object stored in the scene. + DRACO_RETURN_IF_ERROR(gpu_instancing->AddInstance(instance_0)); + DRACO_RETURN_IF_ERROR(gpu_instancing->AddInstance(instance_1)); + + // Assign the GPU instancing object to two mesh groups in two scene nodes. + scene->GetNode(draco::SceneNodeIndex(2))->SetInstanceArrayIndex(index); + scene->GetNode(draco::SceneNodeIndex(4))->SetInstanceArrayIndex(index); + + return draco::OkStatus(); +} + +TEST(SceneTest, TestCopy) { + // Test copying of scene data. + auto src_scene = + draco::ReadSceneFromTestFile("CesiumMilkTruck/glTF/CesiumMilkTruck.gltf"); + ASSERT_NE(src_scene, nullptr); + + // Add GPU instancing to the scene for testing. + DRACO_ASSERT_OK(AddGpuInstancingToMilkTruck(src_scene.get())); + ASSERT_EQ(src_scene->NumInstanceArrays(), 1); + ASSERT_EQ(src_scene->NumNodes(), 5); + ASSERT_EQ( + src_scene->GetNode(draco::SceneNodeIndex(0))->GetInstanceArrayIndex(), + draco::kInvalidInstanceArrayIndex); + ASSERT_EQ( + src_scene->GetNode(draco::SceneNodeIndex(1))->GetInstanceArrayIndex(), + draco::kInvalidInstanceArrayIndex); + ASSERT_EQ( + src_scene->GetNode(draco::SceneNodeIndex(2))->GetInstanceArrayIndex(), + draco::InstanceArrayIndex(0)); + ASSERT_EQ( + src_scene->GetNode(draco::SceneNodeIndex(3))->GetInstanceArrayIndex(), + draco::kInvalidInstanceArrayIndex); + ASSERT_EQ( + src_scene->GetNode(draco::SceneNodeIndex(4))->GetInstanceArrayIndex(), + draco::InstanceArrayIndex(0)); + + // Make a copy of the scene. + draco::Scene dst_scene; + dst_scene.Copy(*src_scene); + + ASSERT_EQ(src_scene->NumMeshes(), dst_scene.NumMeshes()); + ASSERT_EQ(src_scene->NumMeshGroups(), dst_scene.NumMeshGroups()); + ASSERT_EQ(src_scene->NumNodes(), dst_scene.NumNodes()); + ASSERT_EQ(src_scene->NumAnimations(), dst_scene.NumAnimations()); + ASSERT_EQ(src_scene->NumSkins(), dst_scene.NumSkins()); + ASSERT_EQ(src_scene->NumLights(), dst_scene.NumLights()); + ASSERT_EQ(src_scene->NumInstanceArrays(), dst_scene.NumInstanceArrays()); + + for (draco::MeshIndex i(0); i < src_scene->NumMeshes(); ++i) { + draco::MeshAreEquivalent eq; + ASSERT_TRUE(eq(src_scene->GetMesh(i), dst_scene.GetMesh(i))); + } + for (draco::MeshGroupIndex i(0); i < src_scene->NumMeshGroups(); ++i) { + ASSERT_EQ(src_scene->GetMeshGroup(i)->NumMeshInstances(), + dst_scene.GetMeshGroup(i)->NumMeshInstances()); + for (int j = 0; j < src_scene->GetMeshGroup(i)->NumMeshInstances(); ++j) { + ASSERT_EQ(src_scene->GetMeshGroup(i)->GetMeshInstance(j).mesh_index, + dst_scene.GetMeshGroup(i)->GetMeshInstance(j).mesh_index); + ASSERT_EQ(src_scene->GetMeshGroup(i)->GetMeshInstance(j).material_index, + dst_scene.GetMeshGroup(i)->GetMeshInstance(j).material_index); + ASSERT_EQ(src_scene->GetMeshGroup(i) + ->GetMeshInstance(j) + .materials_variants_mappings.size(), + dst_scene.GetMeshGroup(i) + ->GetMeshInstance(j) + .materials_variants_mappings.size()); + } + } + for (draco::SceneNodeIndex i(0); i < src_scene->NumNodes(); ++i) { + ASSERT_EQ(src_scene->GetNode(i)->NumParents(), + dst_scene.GetNode(i)->NumParents()); + for (int j = 0; j < src_scene->GetNode(i)->NumParents(); ++j) { + ASSERT_EQ(src_scene->GetNode(i)->Parent(j), + dst_scene.GetNode(i)->Parent(j)); + } + ASSERT_EQ(src_scene->GetNode(i)->NumChildren(), + dst_scene.GetNode(i)->NumChildren()); + for (int j = 0; j < src_scene->GetNode(i)->NumChildren(); ++j) { + ASSERT_EQ(src_scene->GetNode(i)->Child(j), + dst_scene.GetNode(i)->Child(j)); + } + ASSERT_EQ(src_scene->GetNode(i)->GetMeshGroupIndex(), + dst_scene.GetNode(i)->GetMeshGroupIndex()); + ASSERT_EQ(src_scene->GetNode(i)->GetSkinIndex(), + dst_scene.GetNode(i)->GetSkinIndex()); + ASSERT_EQ(src_scene->GetNode(i)->GetLightIndex(), + dst_scene.GetNode(i)->GetLightIndex()); + ASSERT_EQ(src_scene->GetNode(i)->GetInstanceArrayIndex(), + dst_scene.GetNode(i)->GetInstanceArrayIndex()); + } +} + +TEST(SceneTest, TestRemoveMesh) { + // Test that a base mesh can be removed from scene. + auto src_scene_ptr = + draco::ReadSceneFromTestFile("CesiumMilkTruck/glTF/CesiumMilkTruck.gltf"); + ASSERT_NE(src_scene_ptr, nullptr); + const draco::Scene &src_scene = *src_scene_ptr; + + // Copy scene. + draco::Scene dst_scene; + dst_scene.Copy(src_scene); + ASSERT_EQ(dst_scene.NumMeshes(), 4); + draco::MeshAreEquivalent eq; + ASSERT_TRUE(eq(dst_scene.GetMesh(draco::MeshIndex(0)), + src_scene.GetMesh(draco::MeshIndex(0)))); + ASSERT_TRUE(eq(dst_scene.GetMesh(draco::MeshIndex(1)), + src_scene.GetMesh(draco::MeshIndex(1)))); + ASSERT_TRUE(eq(dst_scene.GetMesh(draco::MeshIndex(2)), + src_scene.GetMesh(draco::MeshIndex(2)))); + ASSERT_TRUE(eq(dst_scene.GetMesh(draco::MeshIndex(3)), + src_scene.GetMesh(draco::MeshIndex(3)))); + + // Remove base mesh from scene. + dst_scene.RemoveMesh(draco::MeshIndex(2)); + ASSERT_EQ(dst_scene.NumMeshes(), 3); + ASSERT_TRUE(eq(dst_scene.GetMesh(draco::MeshIndex(0)), + src_scene.GetMesh(draco::MeshIndex(0)))); + ASSERT_TRUE(eq(dst_scene.GetMesh(draco::MeshIndex(1)), + src_scene.GetMesh(draco::MeshIndex(1)))); + ASSERT_TRUE(eq(dst_scene.GetMesh(draco::MeshIndex(2)), + src_scene.GetMesh(draco::MeshIndex(3)))); + + // Remove another base mesh from scene. + dst_scene.RemoveMesh(draco::MeshIndex(1)); + ASSERT_EQ(dst_scene.NumMeshes(), 2); + ASSERT_TRUE(eq(dst_scene.GetMesh(draco::MeshIndex(0)), + src_scene.GetMesh(draco::MeshIndex(0)))); + ASSERT_TRUE(eq(dst_scene.GetMesh(draco::MeshIndex(1)), + src_scene.GetMesh(draco::MeshIndex(3)))); +} + +TEST(SceneTest, TestRemoveMeshGroup) { + // Test that a mesh group can be removed from scene. + auto src_scene_ptr = + draco::ReadSceneFromTestFile("CesiumMilkTruck/glTF/CesiumMilkTruck.gltf"); + ASSERT_NE(src_scene_ptr, nullptr); + const draco::Scene &src_scene = *src_scene_ptr; + + // Copy scene. + draco::Scene dst_scene; + dst_scene.Copy(src_scene); + ASSERT_EQ(dst_scene.NumMeshGroups(), 2); + ASSERT_EQ(dst_scene.NumNodes(), 5); + ASSERT_EQ(dst_scene.GetNode(draco::SceneNodeIndex(0))->GetMeshGroupIndex(), + draco::MeshGroupIndex(0)); + ASSERT_EQ(dst_scene.GetNode(draco::SceneNodeIndex(2))->GetMeshGroupIndex(), + draco::MeshGroupIndex(1)); + ASSERT_EQ(dst_scene.GetNode(draco::SceneNodeIndex(4))->GetMeshGroupIndex(), + draco::MeshGroupIndex(1)); + + // Remove mesh group from scene. + dst_scene.RemoveMeshGroup(draco::MeshGroupIndex(0)); + ASSERT_EQ(dst_scene.NumMeshGroups(), 1); + ASSERT_EQ(dst_scene.NumNodes(), 5); + ASSERT_EQ(dst_scene.GetNode(draco::SceneNodeIndex(0))->GetMeshGroupIndex(), + draco::kInvalidMeshGroupIndex); + ASSERT_EQ(dst_scene.GetNode(draco::SceneNodeIndex(2))->GetMeshGroupIndex(), + draco::MeshGroupIndex(0)); + ASSERT_EQ(dst_scene.GetNode(draco::SceneNodeIndex(4))->GetMeshGroupIndex(), + draco::MeshGroupIndex(0)); + + // Remove another mesh group from scene. + dst_scene.RemoveMeshGroup(draco::MeshGroupIndex(0)); + ASSERT_EQ(dst_scene.NumMeshGroups(), 0); + ASSERT_EQ(dst_scene.GetNode(draco::SceneNodeIndex(0))->GetMeshGroupIndex(), + draco::kInvalidMeshGroupIndex); + ASSERT_EQ(dst_scene.GetNode(draco::SceneNodeIndex(2))->GetMeshGroupIndex(), + draco::kInvalidMeshGroupIndex); + ASSERT_EQ(dst_scene.GetNode(draco::SceneNodeIndex(4))->GetMeshGroupIndex(), + draco::kInvalidMeshGroupIndex); +} + +void CheckMeshMaterials(const draco::Scene &scene, + const std::vector &expected_material_indices) { + ASSERT_EQ(scene.NumMeshes(), expected_material_indices.size()); + std::vector scene_material_indices; + for (draco::MeshGroupIndex i(0); i < scene.NumMeshGroups(); i++) { + const auto mg = scene.GetMeshGroup(i); + for (int mi = 0; mi < mg->NumMeshInstances(); ++mi) { + scene_material_indices.push_back(mg->GetMeshInstance(mi).material_index); + } + } + ASSERT_EQ(scene_material_indices, expected_material_indices); +} + +TEST(SceneTest, TestRemoveMaterial) { + // Test that materials can be removed from a scene. + auto src_scene_ptr = + draco::ReadSceneFromTestFile("CesiumMilkTruck/glTF/CesiumMilkTruck.gltf"); + ASSERT_NE(src_scene_ptr, nullptr); + const draco::Scene &src_scene = *src_scene_ptr; + ASSERT_EQ(src_scene.GetMaterialLibrary().NumMaterials(), 4); + CheckMeshMaterials(src_scene, {0, 1, 2, 3}); + + // Copy scene. + draco::Scene dst_scene; + dst_scene.Copy(src_scene); + + // Check that referenced material cannot be removed from the scene. + ASSERT_FALSE(dst_scene.RemoveMaterial(2).ok()); + + // Copy scene again, since failed material removal corrupts the scene. + dst_scene.Copy(src_scene); + + // Remove base mesh from scene. Material at index 2 becomes unreferenced. + DRACO_ASSERT_OK(dst_scene.RemoveMesh(draco::MeshIndex(2))); + ASSERT_EQ(dst_scene.GetMaterialLibrary().NumMaterials(), 4); + CheckMeshMaterials(dst_scene, {0, 1, 3}); + + // Check that unreferenced material can be removed from the scene. + DRACO_ASSERT_OK(dst_scene.RemoveMaterial(2)); + ASSERT_EQ(dst_scene.GetMaterialLibrary().NumMaterials(), 3); + CheckMeshMaterials(dst_scene, {0, 1, 2}); + + // Check that material cannot be removed when material index is out of range. + ASSERT_FALSE(dst_scene.RemoveMaterial(-1).ok()); + ASSERT_FALSE(dst_scene.RemoveMaterial(3).ok()); +} + +TEST(SceneTest, TestCopyWithStructuralMetadata) { + // Tests copying of a scene with structural metadata. + auto scene_ptr = + draco::ReadSceneFromTestFile("CesiumMilkTruck/glTF/CesiumMilkTruck.gltf"); + ASSERT_NE(scene_ptr, nullptr); + draco::Scene &scene = *scene_ptr; + + // Add structural metadata to the scene. + draco::PropertyTable::Schema schema; + schema.json.SetString("Data"); + scene.GetStructuralMetadata().SetPropertyTableSchema(schema); + + // Copy the scene. + draco::Scene copy; + copy.Copy(scene); + + // Check that the structural metadata has been copied. + ASSERT_EQ( + copy.GetStructuralMetadata().GetPropertyTableSchema().json.GetString(), + "Data"); +} + +#endif // DRACO_TRANSCODER_SUPPORTED + +} // namespace diff --git a/contrib/draco/src/draco/scene/scene_utils.cc b/contrib/draco/src/draco/scene/scene_utils.cc new file mode 100644 index 000000000..a7bf1dcb9 --- /dev/null +++ b/contrib/draco/src/draco/scene/scene_utils.cc @@ -0,0 +1,962 @@ +// Copyright 2019 The Draco Authors. +// +// 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 "draco/scene/scene_utils.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED +#include +#include +#include +#include +#include +#include + +#include "draco/core/draco_index_type_vector.h" +#include "draco/core/hash_utils.h" +#include "draco/core/vector_d.h" +#include "draco/mesh/mesh_splitter.h" +#include "draco/mesh/mesh_utils.h" +#include "draco/scene/scene_indices.h" +#include "draco/texture/texture_utils.h" + +namespace draco { + +IndexTypeVector +SceneUtils::ComputeAllInstances(const Scene &scene) { + IndexTypeVector instances; + + // Traverse the scene assuming multiple root nodes. + const Eigen::Matrix4d transform = Eigen::Matrix4d::Identity(); + + struct Node { + const SceneNodeIndex scene_node_index; + Eigen::Matrix4d transform; + }; + std::vector nodes; + nodes.reserve(scene.NumRootNodes()); + for (int i = 0; i < scene.NumRootNodes(); ++i) { + nodes.push_back({scene.GetRootNodeIndex(i), transform}); + } + + while (!nodes.empty()) { + const Node node = nodes.back(); + nodes.pop_back(); + const SceneNode &scene_node = *scene.GetNode(node.scene_node_index); + const Eigen::Matrix4d combined_transform = + node.transform * + scene_node.GetTrsMatrix().ComputeTransformationMatrix(); + + // Create instances from node meshes. + const MeshGroupIndex mesh_group_index = scene_node.GetMeshGroupIndex(); + if (mesh_group_index != kInvalidMeshGroupIndex) { + const MeshGroup &mesh_group = *scene.GetMeshGroup(mesh_group_index); + for (int i = 0; i < mesh_group.NumMeshInstances(); i++) { + const MeshIndex mesh_index = mesh_group.GetMeshInstance(i).mesh_index; + if (mesh_index != kInvalidMeshIndex) { + instances.push_back( + {mesh_index, node.scene_node_index, i, combined_transform}); + } + } + } + + // Traverse children nodes. + for (int i = 0; i < scene_node.NumChildren(); i++) { + nodes.push_back({scene_node.Child(i), combined_transform}); + } + } + return instances; +} + +Eigen::Matrix4d SceneUtils::ComputeGlobalNodeTransform(const Scene &scene, + SceneNodeIndex index) { + Eigen::Matrix4d transform = Eigen::Matrix4d::Identity(); + while (index != kInvalidSceneNodeIndex) { + const SceneNode *const node = scene.GetNode(index); + transform = node->GetTrsMatrix().ComputeTransformationMatrix() * transform; + index = node->NumParents() == 1 ? node->Parent(0) : kInvalidSceneNodeIndex; + } + return transform; +} + +IndexTypeVector SceneUtils::NumMeshInstances( + const Scene &scene) { + const auto instances = ComputeAllInstances(scene); + IndexTypeVector num_mesh_instances(scene.NumMeshes(), 0); + for (MeshInstanceIndex i(0); i < instances.size(); i++) { + const MeshInstance &instance = instances[i]; + num_mesh_instances[instance.mesh_index]++; + } + return num_mesh_instances; +} + +int SceneUtils::GetMeshInstanceMaterialIndex(const Scene &scene, + const MeshInstance &instance) { + const auto *const node = scene.GetNode(instance.scene_node_index); + return scene.GetMeshGroup(node->GetMeshGroupIndex()) + ->GetMeshInstance(instance.mesh_group_mesh_index) + .material_index; +} + +int SceneUtils::NumFacesOnBaseMeshes(const Scene &scene) { + int num_faces = 0; + for (MeshIndex i(0); i < scene.NumMeshes(); ++i) { + num_faces += scene.GetMesh(i).num_faces(); + } + return num_faces; +} + +int SceneUtils::NumFacesOnInstancedMeshes(const Scene &scene) { + const auto instances = ComputeAllInstances(scene); + int num_faces = 0; + for (MeshInstanceIndex i(0); i < instances.size(); i++) { + const MeshInstance &instance = instances[i]; + num_faces += scene.GetMesh(instance.mesh_index).num_faces(); + } + return num_faces; +} + +int SceneUtils::NumPointsOnBaseMeshes(const Scene &scene) { + int num_points = 0; + for (MeshIndex i(0); i < scene.NumMeshes(); ++i) { + num_points += scene.GetMesh(i).num_points(); + } + return num_points; +} + +int SceneUtils::NumPointsOnInstancedMeshes(const Scene &scene) { + const auto instances = ComputeAllInstances(scene); + int num_points = 0; + for (MeshInstanceIndex i(0); i < instances.size(); i++) { + const MeshInstance &instance = instances[i]; + num_points += scene.GetMesh(instance.mesh_index).num_points(); + } + return num_points; +} + +int SceneUtils::NumAttEntriesOnBaseMeshes(const Scene &scene, + GeometryAttribute::Type att_type) { + int num_att_entries = 0; + for (MeshIndex i(0); i < scene.NumMeshes(); ++i) { + const Mesh &mesh = scene.GetMesh(i); + const PointAttribute *att = mesh.GetNamedAttribute(att_type); + if (att != nullptr) { + num_att_entries += att->size(); + } + } + return num_att_entries; +} + +int SceneUtils::NumAttEntriesOnInstancedMeshes( + const Scene &scene, GeometryAttribute::Type att_type) { + const auto instances = ComputeAllInstances(scene); + int num_att_entries = 0; + for (MeshInstanceIndex i(0); i < instances.size(); i++) { + const MeshInstance &instance = instances[i]; + const Mesh &mesh = scene.GetMesh(instance.mesh_index); + const PointAttribute *att = mesh.GetNamedAttribute(att_type); + if (att != nullptr) { + num_att_entries += att->size(); + } + } + return num_att_entries; +} + +BoundingBox SceneUtils::ComputeBoundingBox(const Scene &scene) { + // Compute bounding box that includes all scene mesh instances. + const auto instances = ComputeAllInstances(scene); + BoundingBox scene_bbox; + for (MeshInstanceIndex i(0); i < instances.size(); i++) { + const MeshInstance &instance = instances[i]; + const BoundingBox mesh_bbox = + ComputeMeshInstanceBoundingBox(scene, instance); + scene_bbox.Update(mesh_bbox); + } + return scene_bbox; +} + +BoundingBox SceneUtils::ComputeMeshInstanceBoundingBox( + const Scene &scene, const MeshInstance &instance) { + const Mesh &mesh = scene.GetMesh(instance.mesh_index); + BoundingBox mesh_bbox; + auto pc_att = mesh.GetNamedAttribute(GeometryAttribute::POSITION); + Eigen::Vector4d position; + position[3] = 1.0; + for (AttributeValueIndex i(0); i < pc_att->size(); ++i) { + pc_att->ConvertValue(i, &position[0]); + const Eigen::Vector4d transformed = instance.transform * position; + mesh_bbox.Update({static_cast(transformed[0]), + static_cast(transformed[1]), + static_cast(transformed[2])}); + } + return mesh_bbox; +} + +namespace { + +// Updates texture pointers in mesh features of |mesh| to texture pointers +// stored in |new_texture_library|. |texture_to_index_map| stores texture +// indices of the old texture pointers within |mesh|. +void UpdateMeshFeaturesTexturesOnMesh( + const std::unordered_map &texture_to_index_map, + TextureLibrary *new_texture_library, Mesh *mesh) { + for (MeshFeaturesIndex mfi(0); mfi < mesh->NumMeshFeatures(); ++mfi) { + mesh->UpdateMeshFeaturesTexturePointer( + texture_to_index_map, new_texture_library, &mesh->GetMeshFeatures(mfi)); + } +} + +} // namespace + +StatusOr> SceneUtils::MeshToScene( + std::unique_ptr mesh) { + const size_t num_mesh_materials = mesh->GetMaterialLibrary().NumMaterials(); + std::unique_ptr scene(new Scene()); + if (num_mesh_materials > 0) { + scene->GetMaterialLibrary().Copy(mesh->GetMaterialLibrary()); + mesh->GetMaterialLibrary().Clear(); + } else { + // Create a default material for the scene. + scene->GetMaterialLibrary().MutableMaterial(0); + } + + // Copy mesh feature textures. + scene->GetNonMaterialTextureLibrary().Copy( + mesh->GetNonMaterialTextureLibrary()); + + const auto old_texture_to_index_map = + mesh->GetNonMaterialTextureLibrary().ComputeTextureToIndexMap(); + + const SceneNodeIndex scene_node_index = scene->AddNode(); + SceneNode *const scene_node = scene->GetNode(scene_node_index); + const MeshGroupIndex mesh_group_index = scene->AddMeshGroup(); + MeshGroup *const mesh_group = scene->GetMeshGroup(mesh_group_index); + + if (num_mesh_materials <= 1) { + const MeshIndex mesh_index = scene->AddMesh(std::move(mesh)); + if (mesh_index == kInvalidMeshIndex) { + // No idea whether this can happen. It's not covered by any unit test. + return Status(Status::DRACO_ERROR, "Could not add Draco mesh to scene."); + } + mesh_group->AddMeshInstance({mesh_index, 0, {}}); + + UpdateMeshFeaturesTexturesOnMesh(old_texture_to_index_map, + &scene->GetNonMaterialTextureLibrary(), + &scene->GetMesh(mesh_index)); + + } else { + const int32_t mat_att_id = + mesh->GetNamedAttributeId(GeometryAttribute::MATERIAL); + if (mat_att_id == -1) { + // Probably dead code, not covered by any unit test. + return Status(Status::DRACO_ERROR, + "Internal error in MeshToScene: " + "GetNamedAttributeId(MATERIAL) returned -1"); + } + const PointAttribute *const mat_att = + mesh->GetNamedAttribute(GeometryAttribute::MATERIAL); + if (mat_att == nullptr) { + // Probably dead code, not covered by any unit test. + return Status(Status::DRACO_ERROR, + "Internal error in MeshToScene: " + "GetNamedAttribute(MATERIAL) returned nullptr"); + } + + MeshSplitter splitter; + DRACO_ASSIGN_OR_RETURN(MeshSplitter::MeshVector split_meshes, + splitter.SplitMesh(*mesh, mat_att_id)); + // Note: cannot clear mesh here, since mat_att points into it. + for (size_t i = 0; i < split_meshes.size(); ++i) { + if (split_meshes[i] == nullptr) { + // Probably dead code, not covered by any unit test. + continue; + } + const MeshIndex mesh_index = scene->AddMesh(std::move(split_meshes[i])); + if (mesh_index == kInvalidMeshIndex) { + // No idea whether this can happen. It's not covered by any unit test. + return Status(Status::DRACO_ERROR, + "Could not add Draco mesh to scene."); + } + + int material_index = 0; + mat_att->GetValue(AttributeValueIndex(i), &material_index); + mesh_group->AddMeshInstance({mesh_index, material_index, {}}); + + // Copy over mesh features that were associated with the |material_index|. + Mesh &scene_mesh = scene->GetMesh(mesh_index); + Mesh::CopyMeshFeaturesForMaterial(*mesh, &scene_mesh, material_index); + UpdateMeshFeaturesTexturesOnMesh(old_texture_to_index_map, + &scene->GetNonMaterialTextureLibrary(), + &scene_mesh); + } + } + + scene_node->SetMeshGroupIndex(mesh_group_index); + scene->AddRootNodeIndex(scene_node_index); + return scene; +} + +void SceneUtils::PrintInfo(const Scene &input, const Scene &simplified, + bool verbose) { + struct Printer { + Printer(const Scene &input, const Scene &simplified) + : input(input), simplified(simplified), print_instanced_info(false) { + // Info about the instanced meshes is printed if some of the meshes have + // multiple instances and also if the number of base meshes has changed. + auto input_instances = SceneUtils::NumMeshInstances(input); + auto simplified_instances = SceneUtils::NumMeshInstances(simplified); + if (input_instances.size() != simplified_instances.size()) { + print_instanced_info = true; + return; + } + for (MeshIndex i(0); i < input_instances.size(); i++) { + if (input_instances[i] != 1 || simplified_instances[i] != 1) { + print_instanced_info = true; + return; + } + } + } + + void PrintInfoHeader() const { + printf("\n"); + printf("%21s | geometry: base", ""); + if (print_instanced_info) { + printf(" instanced"); + } + printf("\n"); + } + + void PrintInfoRow(const std::string &label, int count_input_base, + int count_input_instanced, int count_simplified_base, + int count_simplified_instanced) const { + // Do not clutter the printout with empty info. + if (count_input_base == 0 && count_input_instanced == 0) { + return; + } + printf(" ----------------------------------------------"); + if (print_instanced_info) { + printf("-------------"); + } + printf("\n"); + printf("%21s | input: %12d", label.c_str(), count_input_base); + if (print_instanced_info) { + printf(" %12d", count_input_instanced); + } + printf("\n"); + printf("%21s | simplified: %12d", "", count_simplified_base); + if (print_instanced_info) { + printf(" %12d", count_simplified_instanced); + } + printf("\n"); + } + + void PrintAttInfoRow(const std::string &label, const draco::Scene &input, + const draco::Scene &simplified, + draco::GeometryAttribute::Type att_type) const { + PrintInfoRow(label, NumAttEntriesOnBaseMeshes(input, att_type), + NumAttEntriesOnInstancedMeshes(input, att_type), + NumAttEntriesOnBaseMeshes(simplified, att_type), + NumAttEntriesOnInstancedMeshes(simplified, att_type)); + } + + const Scene &input; + const Scene &simplified; + bool print_instanced_info; + }; + + // Print information about input and simplified scenes. + const Printer printer(input, simplified); + printer.PrintInfoHeader(); + if (verbose) { + const int num_meshes_input_base = input.NumMeshes(); + const int num_meshes_simplified_base = simplified.NumMeshes(); + const int num_meshes_input_instanced = ComputeAllInstances(input).size(); + const int num_meshes_simplified_instanced = + ComputeAllInstances(simplified).size(); + printer.PrintInfoRow("Number of meshes", num_meshes_input_base, + num_meshes_input_instanced, num_meshes_simplified_base, + num_meshes_simplified_instanced); + } + printer.PrintInfoRow("Number of faces", NumFacesOnBaseMeshes(input), + NumFacesOnInstancedMeshes(input), + NumFacesOnBaseMeshes(simplified), + NumFacesOnInstancedMeshes(simplified)); + if (verbose) { + printer.PrintInfoRow("Number of points", NumPointsOnBaseMeshes(input), + NumPointsOnInstancedMeshes(input), + NumPointsOnBaseMeshes(simplified), + NumPointsOnInstancedMeshes(simplified)); + printer.PrintAttInfoRow("Number of positions", input, simplified, + draco::GeometryAttribute::POSITION); + printer.PrintAttInfoRow("Number of normals", input, simplified, + draco::GeometryAttribute::NORMAL); + printer.PrintAttInfoRow("Number of colors", input, simplified, + draco::GeometryAttribute::COLOR); + printer.PrintInfoRow("Number of materials", + input.GetMaterialLibrary().NumMaterials(), + simplified.GetMaterialLibrary().NumMaterials(), + input.GetMaterialLibrary().NumMaterials(), + simplified.GetMaterialLibrary().NumMaterials()); + } +} + +StatusOr> SceneUtils::InstantiateMesh( + const Scene &scene, const MeshInstance &instance) { + // Check if the |scene| has base mesh corresponding to mesh |instance|. + if (scene.NumMeshes() <= instance.mesh_index.value()) { + Status(Status::DRACO_ERROR, "Scene has no corresponding base mesh."); + } + + // Check that mesh has valid positions. + const Mesh &base_mesh = scene.GetMesh(instance.mesh_index); + const int32_t pos_id = + base_mesh.GetNamedAttributeId(GeometryAttribute::POSITION); + const PointAttribute *const pos_att = base_mesh.attribute(pos_id); + if (pos_att == nullptr) { + return Status(Status::DRACO_ERROR, "Mesh has no positions."); + } + if (pos_att->data_type() != DT_FLOAT32 || pos_att->num_components() != 3) { + return Status(Status::DRACO_ERROR, "Mesh has invalid positions."); + } + + // Copy the base mesh from |scene|. + std::unique_ptr mesh(new Mesh()); + mesh->Copy(base_mesh); + + // Apply transformation to mesh unless transformation is identity. + if (instance.transform != Eigen::Matrix4d::Identity()) { + MeshUtils::TransformMesh(instance.transform, mesh.get()); + } + return mesh; +} + +namespace { + +// Helper class for deleting unused nodes from the scene. +class SceneUnusedNodeRemover { + public: + // Removes unused nodes from the |scene|. + void RemoveUnusedNodes(Scene *scene) { + // Finds all unused nodes and initializes |node_map_| that maps old node + // indices to new node indices. + const int num_unused_nodes = FindUnusedNodes(*scene); + if (num_unused_nodes == 0) { + return; // All nodes are used. + } + + // Update indices of all scene elements accounting for nodes that are going + // to be removed from the scene. + UpdateNodeIndices(scene); + RemoveUnusedNodesFromScene(scene); + } + + private: + // Returns the number of unused nodes. + int FindUnusedNodes(const Scene &scene) { + // First all nodes are considered unused (mapped to invalid index). + // Initially if a node is used, we just map it to its own index. The final + // mapping will be updated once we know all used nodes. + node_map_.resize(scene.NumNodes(), kInvalidSceneNodeIndex); + for (SceneNodeIndex sni(0); sni < scene.NumNodes(); ++sni) { + // If the scene node has a valid mesh group, mark it as used. + if (scene.GetNode(sni)->GetMeshGroupIndex() != kInvalidMeshGroupIndex) { + node_map_[sni] = sni; + } + } + + // Preserve nodes used by animations. + for (AnimationIndex i(0); i < scene.NumAnimations(); i++) { + const Animation &animation = *scene.GetAnimation(i); + for (int channel_i = 0; channel_i < animation.NumChannels(); + channel_i++) { + const SceneNodeIndex node_index( + animation.GetChannel(channel_i)->target_index); + node_map_[node_index] = node_index; + } + } + for (SkinIndex i(0); i < scene.NumSkins(); i++) { + const Skin &skin = *scene.GetSkin(i); + for (int j = 0; j < skin.NumJoints(); j++) { + const SceneNodeIndex node_index = skin.GetJoint(j); + node_map_[node_index] = node_index; + } + const SceneNodeIndex root_index = skin.GetJointRoot(); + if (root_index != kInvalidSceneNodeIndex) { + node_map_[root_index] = root_index; + } + } + + // Ensure that "unused" nodes with used child nodes are marked as used + // (a node can't be deleted as long as it has a used child node). + for (int r = 0; r < scene.NumRootNodes(); ++r) { + UpdateUsedNodesFromSceneGraph(scene, scene.GetRootNodeIndex(r)); + } + + // All used / unused nodes are known. Find new indices for all scene nodes. + int num_valid_nodes = 0; + for (SceneNodeIndex sni(0); sni < scene.NumNodes(); ++sni) { + if (node_map_[sni] != kInvalidSceneNodeIndex) { + node_map_[sni] = SceneNodeIndex(num_valid_nodes++); + } + } + // Return the number of nodes that were unused. + return scene.NumNodes() - num_valid_nodes; + } + + // Recursively traverse node |sni| and mark it as used as long as it has a + // used child node. The function returns true when |sni| is a used node. + bool UpdateUsedNodesFromSceneGraph(const Scene &scene, SceneNodeIndex sni) { + const auto &node = scene.GetNode(sni); + bool is_any_child_node_used = false; + for (int c = 0; c < node->NumChildren(); ++c) { + const SceneNodeIndex cni = node->Child(c); + // Check if the child node is used. + const bool is_c_used = UpdateUsedNodesFromSceneGraph(scene, cni); + if (is_c_used) { + is_any_child_node_used = true; + } + } + if (is_any_child_node_used) { + // The node must be used even if it was previously marked as unused. + node_map_[sni] = sni; + } + // Returns whether this node is used or not. + return node_map_[sni] != kInvalidSceneNodeIndex; + } + + // Remaps existing node indices at various scene elements to new node indices + // defined by |node_map_|. + void UpdateNodeIndices(Scene *scene) const { + // Update node indices on child / parent nodes. + std::vector indices; + for (SceneNodeIndex sni(0); sni < scene->NumNodes(); ++sni) { + indices = scene->GetNode(sni)->Children(); + scene->GetNode(sni)->RemoveAllChildren(); + for (int j = 0; j < indices.size(); ++j) { + const SceneNodeIndex new_sni = node_map_[indices[j]]; + if (new_sni != kInvalidSceneNodeIndex) { + scene->GetNode(sni)->AddChildIndex(new_sni); + } + } + indices = scene->GetNode(sni)->Parents(); + scene->GetNode(sni)->RemoveAllParents(); + for (int j = 0; j < indices.size(); ++j) { + const SceneNodeIndex new_sni = node_map_[indices[j]]; + if (new_sni != kInvalidSceneNodeIndex) { + scene->GetNode(sni)->AddParentIndex(new_sni); + } + } + } + + // Update root node indices. + indices = scene->GetRootNodeIndices(); + scene->RemoveAllRootNodeIndices(); + for (int ri = 0; ri < indices.size(); ++ri) { + const SceneNodeIndex new_rni = node_map_[indices[ri]]; + if (new_rni != kInvalidSceneNodeIndex) { + scene->AddRootNodeIndex(new_rni); + } + } + + // Update node indices used by animations. + for (AnimationIndex i(0); i < scene->NumAnimations(); i++) { + Animation &animation = *scene->GetAnimation(i); + for (int i = 0; i < animation.NumChannels(); i++) { + const SceneNodeIndex node_index(animation.GetChannel(i)->target_index); + animation.GetChannel(i)->target_index = node_map_[node_index].value(); + } + } + for (SkinIndex i(0); i < scene->NumSkins(); i++) { + Skin &skin = *scene->GetSkin(i); + for (int j = 0; j < skin.NumJoints(); j++) { + const SceneNodeIndex node_index = skin.GetJoint(j); + skin.GetJoint(j) = node_map_[node_index]; + } + const SceneNodeIndex root_index = skin.GetJointRoot(); + if (root_index != kInvalidSceneNodeIndex) { + skin.SetJointRoot(node_map_[root_index]); + } + } + } + + // Removes all unused nodes from the scene. + void RemoveUnusedNodesFromScene(Scene *scene) const { + int num_valid_nodes = 0; + // Copy over nodes to their new position in the nodes array. + for (SceneNodeIndex sni(0); sni < scene->NumNodes(); ++sni) { + const SceneNodeIndex new_sni = node_map_[sni]; + if (new_sni == kInvalidSceneNodeIndex) { + continue; + } + num_valid_nodes++; + if (sni != new_sni) { + // Copy over the |sni| node to the new location (|new_sni| is lower than + // |sni|). + scene->GetNode(new_sni)->Copy(*scene->GetNode(sni)); + } + } + // Resize the nodes in the scene to account for the unused ones. This will + // delete all unused nodes. + scene->ResizeNodes(num_valid_nodes); + } + + IndexTypeVector node_map_; +}; + +} // namespace + +void SceneUtils::Cleanup(Scene *scene) { Cleanup(scene, CleanupOptions()); } + +void SceneUtils::Cleanup(Scene *scene, const CleanupOptions &options) { + // Remove invalid mesh indices from mesh groups. + if (options.remove_invalid_mesh_instances) { + for (MeshGroupIndex i(0); i < scene->NumMeshGroups(); i++) { + scene->GetMeshGroup(i)->RemoveMeshInstances(kInvalidMeshIndex); + } + } + + // Find references to mesh groups. + std::vector is_mesh_group_referenced(scene->NumMeshGroups(), false); + for (SceneNodeIndex i(0); i < scene->NumNodes(); i++) { + const SceneNode &node = *scene->GetNode(i); + const MeshGroupIndex mesh_group_index = node.GetMeshGroupIndex(); + if (mesh_group_index != kInvalidMeshGroupIndex) { + is_mesh_group_referenced[mesh_group_index.value()] = true; + } + } + + // Find references to base meshes from referenced mesh groups and find mesh + // groups that have no valid references to base meshes. + std::vector is_base_mesh_referenced(scene->NumMeshes(), false); + std::vector is_mesh_group_empty(scene->NumMeshGroups(), false); + for (MeshGroupIndex i(0); i < scene->NumMeshGroups(); i++) { + if (!is_mesh_group_referenced[i.value()]) { + continue; + } + const MeshGroup &mesh_group = *scene->GetMeshGroup(i); + bool mesh_group_is_empty = true; + for (int j = 0; j < mesh_group.NumMeshInstances(); j++) { + const MeshIndex mesh_index = mesh_group.GetMeshInstance(j).mesh_index; + mesh_group_is_empty = false; + is_base_mesh_referenced[mesh_index.value()] = true; + } + if (mesh_group_is_empty) { + is_mesh_group_empty[i.value()] = true; + } + } + + if (options.remove_unused_meshes) { + // Remove base meshes with no references to them. + for (int i = scene->NumMeshes() - 1; i >= 0; i--) { + const MeshIndex mi(i); + if (!is_base_mesh_referenced[mi.value()]) { + scene->RemoveMesh(mi); + } + } + } + + if (options.remove_unused_mesh_groups) { + // Remove empty mesh groups with no geometry or no references to them. + for (int i = scene->NumMeshGroups() - 1; i >= 0; i--) { + const MeshGroupIndex mgi(i); + if (is_mesh_group_empty[mgi.value()] || + !is_mesh_group_referenced[mgi.value()]) { + scene->RemoveMeshGroup(mgi); + } + } + } + + // Find materials that reference a texture. + MaterialLibrary &material_library = scene->GetMaterialLibrary(); + std::vector materials_with_textures(material_library.NumMaterials(), + false); + for (int i = 0; i < material_library.NumMaterials(); ++i) { + if (material_library.GetMaterial(i)->NumTextureMaps() > 0) { + materials_with_textures[i] = true; + } + } + + // Maps material index to a set of meshes that use that material. + std::vector> material_meshes( + material_library.NumMaterials()); + + // Maps mesh index to a set of materials used by that mesh. + IndexTypeVector> mesh_materials( + scene->NumMeshes()); + + // Maps mesh index to a set of tex coord indices referenced by materials. + IndexTypeVector> tex_coord_referenced( + scene->NumMeshes()); + + // Populate the maps that will be used to remove unused texture coordinates. + for (int mgi = 0; mgi < scene->NumMeshGroups(); ++mgi) { + const MeshGroup *const mesh_group = + scene->GetMeshGroup(MeshGroupIndex(mgi)); + for (int mi = 0; mi < mesh_group->NumMeshInstances(); ++mi) { + const MeshIndex mesh_index = mesh_group->GetMeshInstance(mi).mesh_index; + const int material_index = mesh_group->GetMeshInstance(mi).material_index; + if (material_index == -1) { + continue; + } + + // Populate mesh-material mapping. + material_meshes[material_index].insert(mesh_index); + mesh_materials[mesh_index].insert(material_index); + + // Populate texture coordinate indices referenced by material textures. + const auto material = material_library.GetMaterial(material_index); + for (int i = 0; i < material->NumTextureMaps(); i++) { + const TextureMap *const texture_map = material->GetTextureMapByIndex(i); + const int tex_coord_index = texture_map->tex_coord_index(); + tex_coord_referenced[mesh_index].insert(tex_coord_index); + } + } + } + + // From each mesh, remove texture coordinate attributes that are not + // referenced by any materials and decrement texture coordinate indices in + // texture maps of the mesh materials accordingly. + if (options.remove_unused_tex_coords) { + for (MeshIndex mi(0); mi < scene->NumMeshes(); ++mi) { + // Do not remove unreferenced texture coordinates when the mesh materials + // are used by any other meshes to avoid corrupting those other meshes. + // TODO(vytyaz): Consider removing this limitation. + bool remove_tex_coord = true; + for (const int material_index : mesh_materials[mi]) { + if (material_meshes[material_index].size() != 1) { + // Materials of this mesh are used by other meshes. + remove_tex_coord = false; + break; + } + } + if (!remove_tex_coord) { + continue; + } + + // Remove unreferenced texture coordinate sets from this mesh. + Mesh &mesh = scene->GetMesh(mi); + const int tex_coord_count = + mesh.NumNamedAttributes(GeometryAttribute::TEX_COORD); + for (int tci = tex_coord_count - 1; tci >= 0; tci--) { + if (tex_coord_referenced[mi].count(tci) != 0) { + // Texture coordinate set is referenced. + continue; + } + mesh.DeleteAttribute( + mesh.GetNamedAttributeId(GeometryAttribute::TEX_COORD, tci)); + + // Decrement texture coordinate indices in all materials of this mesh. + for (const int material_index : mesh_materials[mi]) { + auto material = material_library.MutableMaterial(material_index); + for (int i = 0; i < material->NumTextureMaps(); i++) { + auto texture_map = material->GetTextureMapByIndex(i); + // Decrement the indices that are greater than the removed index. + if (texture_map->tex_coord_index() > tci) { + texture_map->SetProperties(texture_map->type(), + texture_map->tex_coord_index() - 1); + } + } + } + } + } + } + + if (options.remove_unused_materials) { + // Remove materials that are not used by any mesh. + for (int i = material_library.NumMaterials() - 1; i >= 0; --i) { + if (material_meshes[i].empty()) { + // Material |i| is not used. + scene->RemoveMaterial(i); + } + } + } + + if (options.remove_unused_nodes) { + SceneUnusedNodeRemover node_remover; + node_remover.RemoveUnusedNodes(scene); + } +} + +void SceneUtils::RemoveMeshInstances(const std::vector &instances, + Scene *scene) { + // Remove mesh instances from the scene. + for (const SceneUtils::MeshInstance &instance : instances) { + const MeshGroupIndex mgi = + scene->GetNode(instance.scene_node_index)->GetMeshGroupIndex(); + + // Create a new mesh group with removed instance (we can't just delete the + // instance from the mesh group directly, because the same mesh group may + // be used by multiple scene nodes). + const MeshGroupIndex new_mesh_group_index = scene->AddMeshGroup(); + MeshGroup &new_mesh_group = *scene->GetMeshGroup(new_mesh_group_index); + + new_mesh_group.Copy(*scene->GetMeshGroup(mgi)); + new_mesh_group.RemoveMeshInstances(instance.mesh_index); + + // Assign the new mesh group to the scene node. Unused mesh groups will be + // automatically removed later during a scene cleanup operation. + scene->GetNode(instance.scene_node_index) + ->SetMeshGroupIndex(new_mesh_group_index); + } + + // Remove duplicate mesh groups that may have been created during the instance + // removal process. + DeduplicateMeshGroups(scene); +} + +void SceneUtils::DeduplicateMeshGroups(Scene *scene) { + if (scene->NumMeshGroups() <= 1) { + return; + } + + // Signature of a mesh group used for detecting duplicates. + struct MeshGroupSignature { + const MeshGroupIndex mesh_group_index; + const MeshGroup &mesh_group; + MeshGroupSignature(MeshGroupIndex mgi, const MeshGroup &mesh_group) + : mesh_group_index(mgi), mesh_group(mesh_group) {} + + bool operator==(const MeshGroupSignature &signature) const { + if (mesh_group.GetName() != signature.mesh_group.GetName()) { + return false; + } + if (mesh_group.NumMeshInstances() != + signature.mesh_group.NumMeshInstances()) { + return false; + } + // TODO(ostava): We may consider sorting meshes within a mesh group to + // make the order of meshes irrelevant. This should be done only for + // meshes with opaque materials though, because for transparent + // geometries, the order matters. + for (int i = 0; i < mesh_group.NumMeshInstances(); ++i) { + if (mesh_group.GetMeshInstance(i) != + signature.mesh_group.GetMeshInstance(i)) { + return false; + } + } + return true; + } + struct Hash { + size_t operator()(const MeshGroupSignature &signature) const { + size_t hash = 79; // Magic number. + const MeshGroup &group = signature.mesh_group; + hash = HashCombine(group.GetName(), hash); + hash = HashCombine(group.NumMeshInstances(), hash); + for (int i = 0; i < group.NumMeshInstances(); ++i) { + const MeshGroup::MeshInstance &instance = group.GetMeshInstance(i); + hash = HashCombine(instance.mesh_index, hash); + hash = HashCombine(instance.material_index, hash); + hash = HashCombine(instance.materials_variants_mappings.size(), hash); + for (const MeshGroup::MaterialsVariantsMapping &mapping : + instance.materials_variants_mappings) { + hash = HashCombine(mapping.material, hash); + hash = HashCombine(mapping.variants.size(), hash); + for (const int &variant : mapping.variants) { + hash = HashCombine(variant, hash); + } + } + } + return hash; + } + }; + }; + + // Set holding unique mesh groups. + std::unordered_set + unique_mesh_groups; + IndexTypeVector parent_mesh_group( + scene->NumMeshGroups()); + for (MeshGroupIndex mgi(0); mgi < scene->NumMeshGroups(); ++mgi) { + const MeshGroup *mg = scene->GetMeshGroup(mgi); + const MeshGroupSignature signature(mgi, *mg); + auto it = unique_mesh_groups.find(signature); + if (it != unique_mesh_groups.end()) { + parent_mesh_group[mgi] = it->mesh_group_index; + } else { + parent_mesh_group[mgi] = kInvalidMeshGroupIndex; + unique_mesh_groups.insert(signature); + } + } + + // Go over all nodes and update mesh groups if needed. + for (SceneNodeIndex sni(0); sni < scene->NumNodes(); ++sni) { + const MeshGroupIndex mgi = scene->GetNode(sni)->GetMeshGroupIndex(); + if (mgi == kInvalidMeshGroupIndex || + parent_mesh_group[mgi] == kInvalidMeshGroupIndex) { + continue; // Nothing to update. + } + scene->GetNode(sni)->SetMeshGroupIndex(parent_mesh_group[mgi]); + } + + // Remove any unused mesh groups. + Cleanup(scene); +} + +void SceneUtils::SetDracoCompressionOptions( + const DracoCompressionOptions *options, Scene *scene) { + for (MeshIndex i(0); i < scene->NumMeshes(); ++i) { + Mesh &mesh = scene->GetMesh(i); + if (options == nullptr) { + mesh.SetCompressionEnabled(false); + } else { + mesh.SetCompressionEnabled(true); + mesh.SetCompressionOptions(*options); + } + } +} + +bool SceneUtils::IsDracoCompressionEnabled(const Scene &scene) { + for (MeshIndex i(0); i < scene.NumMeshes(); ++i) { + if (scene.GetMesh(i).IsCompressionEnabled()) { + return true; + } + } + return false; +} + +IndexTypeVector +SceneUtils::FindLargestBaseMeshTransforms(const Scene &scene) { + IndexTypeVector transforms( + scene.NumMeshes(), Eigen::Matrix4d::Identity()); + + // In case a mesh has multiple instances we want to use the instance with + // the largest scale. + IndexTypeVector transform_scale(scene.NumMeshes(), 0.f); + + const auto instances = SceneUtils::ComputeAllInstances(scene); + for (MeshInstanceIndex i(0); i < instances.size(); ++i) { + const auto &instance = instances[i]; + + // Compute the scale of the transform. + const Vector3f scale_vec(instance.transform.col(0).norm(), + instance.transform.col(1).norm(), + instance.transform.col(2).norm()); + + // In our framework we support uniform scale only. For now, just take the + // maximum scale across all axes. + // TODO(ostava): Investigate how to properly support non-uniform scaling. + const float max_scale = scale_vec.MaxCoeff(); + + if (transform_scale[instance.mesh_index] < max_scale) { + transform_scale[instance.mesh_index] = max_scale; + transforms[instance.mesh_index] = instance.transform; + } + } + + return transforms; +} + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED diff --git a/contrib/draco/src/draco/scene/scene_utils.h b/contrib/draco/src/draco/scene/scene_utils.h new file mode 100644 index 000000000..5b978c3c5 --- /dev/null +++ b/contrib/draco/src/draco/scene/scene_utils.h @@ -0,0 +1,150 @@ +// Copyright 2019 The Draco Authors. +// +// 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 DRACO_SCENE_SCENE_UTILS_H_ +#define DRACO_SCENE_SCENE_UTILS_H_ + +#include "draco/draco_features.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED +#include "draco/attributes/geometry_attribute.h" +#include "draco/scene/scene.h" + +namespace draco { + +// Helper class containing various utility functions operating on draco::Scene. +class SceneUtils { + public: + // Helper struct holding instanced meshes and their transformations. + struct MeshInstance { + // Index of the parent mesh in the draco::Scene. + MeshIndex mesh_index; + // Index of the node in the draco::Scene. + SceneNodeIndex scene_node_index; + // Index of the mesh in the mesh group. + int mesh_group_mesh_index; + // Transform of the instance from the mesh local space to the global space + // of the scene. + Eigen::Matrix4d transform; + }; + + // Computes all mesh instances in the |scene|. + static IndexTypeVector ComputeAllInstances( + const Scene &scene); + + // Computes global transform matrix of a |scene| node given by its |index|. + static Eigen::Matrix4d ComputeGlobalNodeTransform(const Scene &scene, + SceneNodeIndex index); + + // Returns a vector of mesh instance counts for all base meshes. + static IndexTypeVector NumMeshInstances(const Scene &scene); + + // Returns the material index of the given |instance| or -1 if the mesh + // |instance| has a default material. + static int GetMeshInstanceMaterialIndex(const Scene &scene, + const MeshInstance &instance); + + // Returns the total number of faces on all base meshes of the scene (not + // counting instances). + static int NumFacesOnBaseMeshes(const Scene &scene); + + // Returns the total number of faces on all meshes of the scenes, including + // all instances of the same mesh. + static int NumFacesOnInstancedMeshes(const Scene &scene); + + // Returns the total number of points on all base meshes of the scene (not + // counting instances). + static int NumPointsOnBaseMeshes(const Scene &scene); + + // Returns the total number of points on all meshes of the scenes, including + // all instances of the same mesh. + static int NumPointsOnInstancedMeshes(const Scene &scene); + + // Returns the total number of attribute entries on all base meshes of the + // scene (not counting instances) for the first attribute of |att_type|. + static int NumAttEntriesOnBaseMeshes(const Scene &scene, + GeometryAttribute::Type att_type); + + // Returns the total number of attribute ent on all meshes of the scenes, + // including all instances of the same mesh for the first attribute of + // |att_type|. + static int NumAttEntriesOnInstancedMeshes(const Scene &scene, + GeometryAttribute::Type att_type); + + // Returns the bounding box of the scene. + static BoundingBox ComputeBoundingBox(const Scene &scene); + + // Returns the bounding box of a mesh instance. + static BoundingBox ComputeMeshInstanceBoundingBox( + const Scene &scene, const MeshInstance &instance); + + // Prints info about input and simplified scenes. + static void PrintInfo(const Scene &input, const Scene &simplified, + bool verbose); + + // Converts a draco::Mesh into a draco::Scene. If the passed-in `mesh` has + // multiple materials, the returned scene will contain multiple meshes, one + // for each of the source mesh's materials; if `mesh` has no material, one + // will be created for it. + static StatusOr> MeshToScene( + std::unique_ptr mesh); + + // Creates a mesh according to mesh |instance| in |scene|. Error is returned + // if there is no corresponding base mesh in the |scene| or the base mesh has + // no valid positions. + static StatusOr> InstantiateMesh( + const Scene &scene, const MeshInstance &instance); + + // Cleans up a |scene| by removing unused base meshes, unused and empty mesh + // groups, unused materials, unused texture coordinates and unused scene + // nodes. The actual behavior of the cleanup operation can be controller via + // the user provided |options|. + struct CleanupOptions { + bool remove_invalid_mesh_instances = true; + bool remove_unused_mesh_groups = true; + bool remove_unused_meshes = true; + bool remove_unused_nodes = false; + bool remove_unused_tex_coords = false; + bool remove_unused_materials = true; + }; + static void Cleanup(Scene *scene); + static void Cleanup(Scene *scene, const CleanupOptions &options); + + // Removes mesh |instances| from |scene|. + static void RemoveMeshInstances(const std::vector &instances, + Scene *scene); + + // Removes duplicate mesh groups that have the same name and that contain + // exactly the same meshes and materials. + static void DeduplicateMeshGroups(Scene *scene); + + // Enables geometry compression and sets compression |options| to all meshes + // in the |scene|. If |options| is nullptr then geometry compression is + // disabled for all meshes in the |scene|. + static void SetDracoCompressionOptions(const DracoCompressionOptions *options, + Scene *scene); + + // Returns true if geometry compression is eabled for any of |scene| meshes. + static bool IsDracoCompressionEnabled(const Scene &scene); + + // Returns a single tranformation matrix for each base mesh of the |scene| + // corresponding to the instance with the maximum scale. + static IndexTypeVector + FindLargestBaseMeshTransforms(const Scene &scene); +}; + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED +#endif // DRACO_SCENE_SCENE_UTILS_H_ diff --git a/contrib/draco/src/draco/scene/scene_utils_test.cc b/contrib/draco/src/draco/scene/scene_utils_test.cc new file mode 100644 index 000000000..4d6bd731d --- /dev/null +++ b/contrib/draco/src/draco/scene/scene_utils_test.cc @@ -0,0 +1,763 @@ +// Copyright 2019 The Draco Authors. +// +// 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 "draco/scene/scene_utils.h" + +#include +#include + +#ifdef DRACO_TRANSCODER_SUPPORTED +#include "draco/core/bounding_box.h" +#include "draco/core/draco_test_base.h" +#include "draco/core/draco_test_utils.h" +#include "draco/io/texture_io.h" +#include "draco/scene/scene_indices.h" + +namespace { + +using draco::MeshIndex; +using draco::MeshInstanceIndex; + +void AssertMatrixNear(const Eigen::Matrix4d &a, const Eigen::Matrix4d &b, + float tolerance) { + Eigen::Matrix4d diff = a - b; + ASSERT_NEAR(diff.norm(), 0.f, tolerance) << a << " vs " << b; +} + +// TODO(fgalligan): Re-factor this code with gltf_encoder_test. +void CompareScenes(const draco::Scene *scene0, const draco::Scene *scene1) { + ASSERT_EQ(scene0->NumMeshGroups(), scene1->NumMeshGroups()); + ASSERT_EQ(scene0->NumMeshes(), scene1->NumMeshes()); + ASSERT_EQ(scene0->GetMaterialLibrary().NumMaterials(), + scene1->GetMaterialLibrary().NumMaterials()); + ASSERT_EQ(scene0->NumAnimations(), scene1->NumAnimations()); + ASSERT_EQ(scene0->NumSkins(), scene1->NumSkins()); + for (draco::AnimationIndex i(0); i < scene0->NumAnimations(); ++i) { + const draco::Animation *const animation0 = scene0->GetAnimation(i); + const draco::Animation *const animation1 = scene1->GetAnimation(i); + ASSERT_NE(animation0, nullptr); + ASSERT_NE(animation1, nullptr); + ASSERT_EQ(animation0->NumSamplers(), animation1->NumSamplers()); + ASSERT_EQ(animation0->NumChannels(), animation1->NumChannels()); + ASSERT_EQ(animation0->NumNodeAnimationData(), + animation1->NumNodeAnimationData()); + } +} + +TEST(SceneUtilsTest, TestComputeAllInstances) { + // Tests that we can compute all instances in an input scene along with their + // transformations. + + auto scene = + draco::ReadSceneFromTestFile("CesiumMilkTruck/glTF/CesiumMilkTruck.gltf"); + ASSERT_NE(scene, nullptr); + ASSERT_EQ(scene->NumMeshes(), 4); + + // Compute mesh instances. + const auto instances = draco::SceneUtils::ComputeAllInstances(*scene); + ASSERT_EQ(instances.size(), 5); + + // Check base mesh indices. + ASSERT_EQ(instances[MeshInstanceIndex(0)].mesh_index, 0); + ASSERT_EQ(instances[MeshInstanceIndex(1)].mesh_index, 1); + ASSERT_EQ(instances[MeshInstanceIndex(2)].mesh_index, 2); + ASSERT_EQ(instances[MeshInstanceIndex(3)].mesh_index, 3); + ASSERT_EQ(instances[MeshInstanceIndex(4)].mesh_index, 3); + + // Check scene node indices. + ASSERT_EQ(instances[MeshInstanceIndex(0)].scene_node_index, 0); + ASSERT_EQ(instances[MeshInstanceIndex(1)].scene_node_index, 0); + ASSERT_EQ(instances[MeshInstanceIndex(2)].scene_node_index, 0); + ASSERT_EQ(instances[MeshInstanceIndex(3)].scene_node_index, 4); + ASSERT_EQ(instances[MeshInstanceIndex(4)].scene_node_index, 2); + + // Check indices of meshes in mesh group. + ASSERT_EQ(instances[MeshInstanceIndex(0)].mesh_group_mesh_index, 0); + ASSERT_EQ(instances[MeshInstanceIndex(1)].mesh_group_mesh_index, 1); + ASSERT_EQ(instances[MeshInstanceIndex(2)].mesh_group_mesh_index, 2); + ASSERT_EQ(instances[MeshInstanceIndex(3)].mesh_group_mesh_index, 0); + ASSERT_EQ(instances[MeshInstanceIndex(4)].mesh_group_mesh_index, 0); + + // The first three instances should have identity transformation. + for (MeshInstanceIndex i(0); i < 3; ++i) { + AssertMatrixNear(instances[i].transform, Eigen::Matrix4d::Identity(), + 1e-6f); + } + + // Fourth and fifth instances are transformed. + Eigen::Matrix4d expected_transform = Eigen::Matrix4d::Identity(); + // Expected translation. + expected_transform(0, 3) = -1.352329969406128; + expected_transform(1, 3) = 0.4277220070362091; + expected_transform(2, 3) = -2.98022992950564e-8; + + // Expected rotation. + Eigen::Matrix4d expected_rotation = Eigen::Matrix4d::Identity(); + expected_rotation.block<3, 3>(0, 0) = + Eigen::Quaterniond(-0.9960774183273317, -0.0, -0.0, 0.08848590403795243) + .normalized() + .toRotationMatrix(); + expected_transform = expected_transform * expected_rotation; + + AssertMatrixNear(instances[MeshInstanceIndex(3)].transform, + expected_transform, 1e-6f); + + // Last instance differs only in the translation part in X axis. + expected_transform(0, 3) = 1.432669997215271; + + AssertMatrixNear(instances[MeshInstanceIndex(4)].transform, + expected_transform, 1e-6f); +} + +TEST(SceneUtilsTest, TestComputeAllInstancesWithShiftedGeometryRoot) { + // Tests that we can compute all instances in an input scene along with their + // transformations. This scene has light and camera nodes before the geometry + // node. + auto scene = draco::ReadSceneFromTestFile( + "SphereWithCircleTexture/sphere_with_circle_texture.gltf"); + ASSERT_NE(scene, nullptr); + + // There is one base mesh. + ASSERT_EQ(scene->NumMeshes(), 1); + + // There is a single mesh instance. + const auto instances = draco::SceneUtils::ComputeAllInstances(*scene); + ASSERT_EQ(instances.size(), 1); + ASSERT_EQ(instances[MeshInstanceIndex(0)].mesh_index, 0); + + // There is no transformation. + AssertMatrixNear(instances[MeshInstanceIndex(0)].transform, + Eigen::Matrix4d::Identity(), 1e-6); +} + +TEST(SceneUtilsTest, TestNumMeshInstances) { + // Tests that we can compute mesh instance counts for all base meshes in an + // input scene. + + auto scene = + draco::ReadSceneFromTestFile("CesiumMilkTruck/glTF/CesiumMilkTruck.gltf"); + ASSERT_NE(scene, nullptr); + ASSERT_EQ(scene->NumMeshes(), 4); + + const auto num_mesh_instances = draco::SceneUtils::NumMeshInstances(*scene); + ASSERT_EQ(num_mesh_instances.size(), 4); + ASSERT_EQ(num_mesh_instances[draco::MeshIndex(0)], 1); + ASSERT_EQ(num_mesh_instances[draco::MeshIndex(1)], 1); + ASSERT_EQ(num_mesh_instances[draco::MeshIndex(2)], 1); + ASSERT_EQ(num_mesh_instances[draco::MeshIndex(3)], 2); +} + +TEST(SceneUtilsTest, TestNumFacesOnScene) { + auto scene = + draco::ReadSceneFromTestFile("CesiumMilkTruck/glTF/CesiumMilkTruck.gltf"); + ASSERT_NE(scene, nullptr); + ASSERT_EQ(draco::SceneUtils::NumFacesOnBaseMeshes(*scene), 2856); + ASSERT_EQ(draco::SceneUtils::NumFacesOnInstancedMeshes(*scene), 3624); +} + +TEST(SceneUtilsTest, TestNumPointsOnScene) { + auto scene = + draco::ReadSceneFromTestFile("CesiumMilkTruck/glTF/CesiumMilkTruck.gltf"); + ASSERT_NE(scene, nullptr); + ASSERT_EQ(draco::SceneUtils::NumPointsOnBaseMeshes(*scene), 2978); + ASSERT_EQ(draco::SceneUtils::NumPointsOnInstancedMeshes(*scene), 3564); +} + +TEST(SceneUtilsTest, TestNumPositionsOnScene) { + auto scene = + draco::ReadSceneFromTestFile("CesiumMilkTruck/glTF/CesiumMilkTruck.gltf"); + ASSERT_NE(scene, nullptr); + ASSERT_EQ(draco::SceneUtils::NumAttEntriesOnBaseMeshes( + *scene, draco::GeometryAttribute::POSITION), + 1572); + ASSERT_EQ(draco::SceneUtils::NumAttEntriesOnInstancedMeshes( + *scene, draco::GeometryAttribute::POSITION), + 1960); +} + +TEST(SceneUtilsTest, TestNumNormalsOnScene) { + auto scene = + draco::ReadSceneFromTestFile("CesiumMilkTruck/glTF/CesiumMilkTruck.gltf"); + ASSERT_NE(scene, nullptr); + ASSERT_EQ(draco::SceneUtils::NumAttEntriesOnBaseMeshes( + *scene, draco::GeometryAttribute::NORMAL), + 1252); + ASSERT_EQ(draco::SceneUtils::NumAttEntriesOnInstancedMeshes( + *scene, draco::GeometryAttribute::NORMAL), + 1612); +} + +TEST(SceneUtilsTest, TestNumColorsOnScene) { + auto scene = + draco::ReadSceneFromTestFile("CesiumMilkTruck/glTF/CesiumMilkTruck.gltf"); + ASSERT_NE(scene, nullptr); + ASSERT_EQ(draco::SceneUtils::NumAttEntriesOnBaseMeshes( + *scene, draco::GeometryAttribute::COLOR), + 0); + ASSERT_EQ(draco::SceneUtils::NumAttEntriesOnInstancedMeshes( + *scene, draco::GeometryAttribute::COLOR), + 0); +} + +TEST(SceneUtilsTest, TestComputeBoundingBox) { + auto scene = + draco::ReadSceneFromTestFile("CesiumMilkTruck/glTF/CesiumMilkTruck.gltf"); + ASSERT_NE(scene, nullptr); + const draco::BoundingBox bbox = draco::SceneUtils::ComputeBoundingBox(*scene); + const draco::Vector3f min_point = bbox.GetMinPoint(); + const draco::Vector3f max_point = bbox.GetMaxPoint(); + constexpr float tolerance = 1e-4f; + EXPECT_NEAR(min_point[0], -2.43091, tolerance); + EXPECT_NEAR(min_point[1], +0.00145, tolerance); + EXPECT_NEAR(min_point[2], -1.39600, tolerance); + EXPECT_NEAR(max_point[0], +2.43800, tolerance); + EXPECT_NEAR(max_point[1], +2.58437, tolerance); + EXPECT_NEAR(max_point[2], +1.39600, tolerance); +} + +TEST(SceneUtilsTest, TestComputeMeshInstanceBoundingBox) { + auto scene = draco::ReadSceneFromTestFile( + "SphereWithCircleTexture/sphere_with_circle_texture.gltf"); + ASSERT_NE(scene, nullptr); + const draco::BoundingBox scene_bbox = + draco::SceneUtils::ComputeBoundingBox(*scene); + const auto instances = draco::SceneUtils::ComputeAllInstances(*scene); + ASSERT_EQ(instances.size(), 1); + const draco::BoundingBox mesh_bbox = + draco::SceneUtils::ComputeMeshInstanceBoundingBox( + *scene, instances[draco::MeshInstanceIndex(0)]); + ASSERT_EQ(scene_bbox.GetMinPoint(), mesh_bbox.GetMinPoint()); + ASSERT_EQ(scene_bbox.GetMaxPoint(), mesh_bbox.GetMaxPoint()); +} + +TEST(SceneUtilsTest, TestMeshToSceneZeroMaterials) { + const std::string filename = "cube_att.obj"; + std::unique_ptr mesh = draco::ReadMeshFromTestFile(filename); + ASSERT_NE(mesh, nullptr); + ASSERT_EQ(mesh->GetMaterialLibrary().NumMaterials(), 0); + + DRACO_ASSIGN_OR_ASSERT(const std::unique_ptr scene_from_mesh, + draco::SceneUtils::MeshToScene(std::move(mesh))); + ASSERT_NE(scene_from_mesh, nullptr); + ASSERT_EQ(scene_from_mesh->NumMeshes(), 1); + ASSERT_EQ(scene_from_mesh->GetMaterialLibrary().NumMaterials(), 1); + ASSERT_EQ(scene_from_mesh->NumMeshGroups(), 1); + const draco::MeshGroup *const mesh_group = + scene_from_mesh->GetMeshGroup(draco::MeshGroupIndex(0)); + ASSERT_EQ(mesh_group->NumMeshInstances(), 1); +} + +TEST(SceneUtilsTest, TestMeshToSceneOneMaterial) { + const std::string filename = + "SphereWithCircleTexture/sphere_with_circle_texture.gltf"; + auto scene = draco::ReadSceneFromTestFile(filename); + ASSERT_NE(scene, nullptr); + ASSERT_EQ(scene->GetMaterialLibrary().NumMaterials(), 1); + + std::unique_ptr mesh = draco::ReadMeshFromTestFile(filename); + ASSERT_NE(mesh, nullptr); + ASSERT_EQ(mesh->GetMaterialLibrary().NumMaterials(), 1); + + DRACO_ASSIGN_OR_ASSERT(const std::unique_ptr scene_from_mesh, + draco::SceneUtils::MeshToScene(std::move(mesh))); + ASSERT_NE(scene_from_mesh, nullptr); + ASSERT_EQ(scene_from_mesh->NumMeshes(), 1); + ASSERT_EQ(scene_from_mesh->GetMaterialLibrary().NumMaterials(), 1); + ASSERT_EQ(scene_from_mesh->NumMeshGroups(), 1); + const draco::MeshGroup *const mesh_group = + scene_from_mesh->GetMeshGroup(draco::MeshGroupIndex(0)); + ASSERT_EQ(mesh_group->NumMeshInstances(), 1); + + CompareScenes(scene.get(), scene_from_mesh.get()); +} + +TEST(SceneUtilsTest, TestMeshToSceneMultipleMaterials) { + const std::string filename = "CesiumMilkTruck/glTF/CesiumMilkTruck.gltf"; + auto scene = draco::ReadSceneFromTestFile(filename); + ASSERT_NE(scene, nullptr); + + std::unique_ptr mesh = draco::ReadMeshFromTestFile(filename); + ASSERT_NE(mesh, nullptr); + ASSERT_EQ(mesh->GetMaterialLibrary().NumMaterials(), 4); + + DRACO_ASSIGN_OR_ASSERT(const std::unique_ptr scene_from_mesh, + draco::SceneUtils::MeshToScene(std::move(mesh))); + ASSERT_NE(scene_from_mesh, nullptr); + ASSERT_EQ(scene_from_mesh->NumMeshes(), 4); + ASSERT_EQ(scene_from_mesh->GetMaterialLibrary().NumMaterials(), 4); + ASSERT_EQ(scene_from_mesh->NumMeshGroups(), 1); + const draco::MeshGroup *const mesh_group = + scene_from_mesh->GetMeshGroup(draco::MeshGroupIndex(0)); + ASSERT_EQ(mesh_group->NumMeshInstances(), 4); + + // Unfortunately we can't CompareScenes(scene.get(), scene_from_mesh.get()), + // because scene has two mesh groups and scene_from_mesh has only one. +} + +TEST(SceneUtilsTest, TestMeshToSceneMultipleMeshFeatures) { + const std::string filename = "BoxesMeta/glTF/BoxesMeta.gltf"; + std::unique_ptr scene = draco::ReadSceneFromTestFile(filename); + ASSERT_NE(scene, nullptr); + std::unique_ptr mesh = draco::ReadMeshFromTestFile(filename); + ASSERT_NE(mesh, nullptr); + ASSERT_EQ(mesh->GetMaterialLibrary().NumMaterials(), 2); + ASSERT_EQ(mesh->NumMeshFeatures(), 5); + + DRACO_ASSIGN_OR_ASSERT(const std::unique_ptr scene_from_mesh, + draco::SceneUtils::MeshToScene(std::move(mesh))); + ASSERT_NE(scene_from_mesh, nullptr); + ASSERT_EQ(scene_from_mesh->NumMeshes(), 2); + ASSERT_EQ(scene_from_mesh->GetMaterialLibrary().NumMaterials(), 2); + ASSERT_EQ(scene_from_mesh->NumMeshGroups(), 1); + const draco::MeshGroup *const mesh_group = + scene_from_mesh->GetMeshGroup(draco::MeshGroupIndex(0)); + ASSERT_EQ(mesh_group->NumMeshInstances(), 2); + + // Meshes of the new scene should have the same properties as meshes loaded + // directly into |scene|. + for (draco::MeshIndex mi(0); mi < scene->NumMeshes(); ++mi) { + ASSERT_EQ(scene->GetMesh(mi).NumMeshFeatures(), + scene_from_mesh->GetMesh(mi).NumMeshFeatures()); + for (draco::MeshFeaturesIndex mfi(0); + mfi < scene->GetMesh(mi).NumMeshFeatures(); ++mfi) { + const auto &scene_mf = scene->GetMesh(mi).GetMeshFeatures(mfi); + const auto &scene_from_mesh_mf = + scene_from_mesh->GetMesh(mi).GetMeshFeatures(mfi); + ASSERT_EQ(scene_mf.GetAttributeIndex(), + scene_from_mesh_mf.GetAttributeIndex()); + ASSERT_EQ(scene_mf.GetPropertyTableIndex(), + scene_from_mesh_mf.GetPropertyTableIndex()); + ASSERT_EQ(scene_mf.GetLabel(), scene_from_mesh_mf.GetLabel()); + ASSERT_EQ(scene_mf.GetNullFeatureId(), + scene_from_mesh_mf.GetNullFeatureId()); + ASSERT_EQ(scene_mf.GetFeatureCount(), + scene_from_mesh_mf.GetFeatureCount()); + ASSERT_EQ(scene_mf.GetTextureChannels(), + scene_from_mesh_mf.GetTextureChannels()); + ASSERT_EQ(scene_mf.GetTextureMap().texture() != nullptr, + scene_from_mesh_mf.GetTextureMap().texture() != nullptr); + } + } +} + +TEST(SceneUtilsTest, TestInstantiateMeshWithIdentityTransformation) { + auto scene = + draco::ReadSceneFromTestFile("CesiumMilkTruck/glTF/CesiumMilkTruck.gltf"); + ASSERT_NE(scene, nullptr); + + // Compute scene mesh instances. + const auto instances = draco::SceneUtils::ComputeAllInstances(*scene); + ASSERT_EQ(instances.size(), 5); + + // Check instantiation of mesh with identity transformation. + const draco::SceneUtils::MeshInstance instance = + instances[MeshInstanceIndex(0)]; + ASSERT_EQ(instance.transform, Eigen::Matrix4d::Identity()); + + // Instantiate this mesh instance. + DRACO_ASSIGN_OR_ASSERT(auto mesh, + draco::SceneUtils::InstantiateMesh(*scene, instance)); + const draco::Mesh &base_mesh = scene->GetMesh(instance.mesh_index); + + // Check that bounding box of the instanced mesh is same as box of base mesh. + const draco::BoundingBox instanced_bbox = mesh->ComputeBoundingBox(); + const draco::BoundingBox base_bbox = base_mesh.ComputeBoundingBox(); + ASSERT_EQ(instanced_bbox.GetMinPoint()[0], base_bbox.GetMinPoint()[0]); + ASSERT_EQ(instanced_bbox.GetMinPoint()[1], base_bbox.GetMinPoint()[1]); + ASSERT_EQ(instanced_bbox.GetMinPoint()[2], base_bbox.GetMinPoint()[2]); + ASSERT_EQ(instanced_bbox.GetMaxPoint()[0], base_bbox.GetMaxPoint()[0]); + ASSERT_EQ(instanced_bbox.GetMaxPoint()[1], base_bbox.GetMaxPoint()[1]); + ASSERT_EQ(instanced_bbox.GetMaxPoint()[2], base_bbox.GetMaxPoint()[2]); +} + +TEST(SceneUtilsTest, TestInstantiateMesh) { + auto scene = + draco::ReadSceneFromTestFile("CesiumMilkTruck/glTF/CesiumMilkTruck.gltf"); + ASSERT_NE(scene, nullptr); + + // Compute scene mesh instances. + const auto instances = draco::SceneUtils::ComputeAllInstances(*scene); + ASSERT_EQ(instances.size(), 5); + + // Check instantiation of mesh with identity transformation. + const draco::SceneUtils::MeshInstance instance = + instances[MeshInstanceIndex(3)]; + ASSERT_NE(instance.transform, Eigen::Matrix4d::Identity()); + + // Instantiate this mesh instance. + DRACO_ASSIGN_OR_ASSERT(auto mesh, + draco::SceneUtils::InstantiateMesh(*scene, instance)); + const draco::Mesh &base_mesh = scene->GetMesh(instance.mesh_index); + + // Check bounding box of the base mesh. + constexpr float tolerance = 1e-4f; + const draco::BoundingBox base_bbox = base_mesh.ComputeBoundingBox(); + EXPECT_NEAR(base_bbox.GetMinPoint()[0], -0.42780, tolerance); + EXPECT_NEAR(base_bbox.GetMinPoint()[1], -0.42780, tolerance); + EXPECT_NEAR(base_bbox.GetMinPoint()[2], -1.05800, tolerance); + EXPECT_NEAR(base_bbox.GetMaxPoint()[0], +0.42780, tolerance); + EXPECT_NEAR(base_bbox.GetMaxPoint()[1], +0.42780, tolerance); + EXPECT_NEAR(base_bbox.GetMaxPoint()[2], +1.05800, tolerance); + + // Check bounding box of the instanced mesh. It should differ. + const draco::BoundingBox instanced_bbox = mesh->ComputeBoundingBox(); + EXPECT_NEAR(instanced_bbox.GetMinPoint()[0], -1.77860, tolerance); + EXPECT_NEAR(instanced_bbox.GetMinPoint()[1], +0.00145, tolerance); + EXPECT_NEAR(instanced_bbox.GetMinPoint()[2], -1.05800, tolerance); + EXPECT_NEAR(instanced_bbox.GetMaxPoint()[0], -0.92606, tolerance); + EXPECT_NEAR(instanced_bbox.GetMaxPoint()[1], +0.85399, tolerance); + EXPECT_NEAR(instanced_bbox.GetMaxPoint()[2], +1.05800, tolerance); +} + +TEST(SceneUtilsTest, TestCleanupEmptyMeshGroup) { + auto scene = + draco::ReadSceneFromTestFile("CesiumMilkTruck/glTF/CesiumMilkTruck.gltf"); + ASSERT_NE(scene, nullptr); + ASSERT_EQ(scene->NumMeshes(), 4); + ASSERT_EQ(scene->NumMeshGroups(), 2); + ASSERT_EQ(draco::SceneUtils::ComputeAllInstances(*scene).size(), 5); + ASSERT_EQ(scene->GetNode(draco::SceneNodeIndex(0))->GetMeshGroupIndex(), + draco::MeshGroupIndex(0)); + + // Invalidate references to the three truck body parts in mesh group. + draco::MeshGroup &mesh_group = *scene->GetMeshGroup(draco::MeshGroupIndex(0)); + mesh_group.SetMeshInstance(0, {draco::kInvalidMeshIndex, 0}); + mesh_group.SetMeshInstance(1, {draco::kInvalidMeshIndex, 0}); + mesh_group.SetMeshInstance(2, {draco::kInvalidMeshIndex, 0}); + + // Cleanup scene. + draco::SceneUtils::Cleanup(scene.get()); + + // Check cleaned up scene. + ASSERT_EQ(scene->NumMeshes(), 1); + ASSERT_EQ(scene->NumMeshGroups(), 1); + ASSERT_EQ(draco::SceneUtils::ComputeAllInstances(*scene).size(), 2); + ASSERT_EQ(scene->GetNode(draco::SceneNodeIndex(0))->GetMeshGroupIndex(), + draco::kInvalidMeshGroupIndex); +} + +TEST(SceneUtilsTest, TestCleanupUnreferencedMeshGroup) { + auto scene = + draco::ReadSceneFromTestFile("CesiumMilkTruck/glTF/CesiumMilkTruck.gltf"); + ASSERT_NE(scene, nullptr); + ASSERT_EQ(scene->NumMeshes(), 4); + ASSERT_EQ(scene->NumMeshGroups(), 2); + ASSERT_EQ(draco::SceneUtils::ComputeAllInstances(*scene).size(), 5); + + // Invalidate references to truck axle mesh group. + scene->GetNode(draco::SceneNodeIndex(2)) + ->SetMeshGroupIndex(draco::kInvalidMeshGroupIndex); + scene->GetNode(draco::SceneNodeIndex(4)) + ->SetMeshGroupIndex(draco::kInvalidMeshGroupIndex); + + // Cleanup scene. + draco::SceneUtils::Cleanup(scene.get()); + + // Check cleaned up scene. + ASSERT_EQ(scene->NumMeshes(), 3); + ASSERT_EQ(scene->NumMeshGroups(), 1); + ASSERT_EQ(draco::SceneUtils::ComputeAllInstances(*scene).size(), 3); +} + +TEST(SceneUtilsTest, TestCleanupInvalidMeshIndex) { + auto scene = + draco::ReadSceneFromTestFile("CesiumMilkTruck/glTF/CesiumMilkTruck.gltf"); + ASSERT_NE(scene, nullptr); + ASSERT_EQ(scene->NumMeshes(), 4); + ASSERT_EQ(scene->NumMeshGroups(), 2); + ASSERT_EQ(draco::SceneUtils::ComputeAllInstances(*scene).size(), 5); + ASSERT_EQ(scene->GetNode(draco::SceneNodeIndex(0))->GetMeshGroupIndex(), + draco::MeshGroupIndex(0)); + + // Invalidate references to two truck body parts in mesh group. + draco::MeshGroup &mesh_group = *scene->GetMeshGroup(draco::MeshGroupIndex(0)); + ASSERT_EQ(mesh_group.NumMeshInstances(), 3); + mesh_group.SetMeshInstance(0, {draco::kInvalidMeshIndex, 0}); + mesh_group.SetMeshInstance(2, {draco::kInvalidMeshIndex, 0}); + + // Cleanup scene. + draco::SceneUtils::Cleanup(scene.get()); + + // Check cleaned up scene. + ASSERT_EQ(scene->NumMeshes(), 2); + ASSERT_EQ(scene->NumMeshGroups(), 2); + ASSERT_EQ(draco::SceneUtils::ComputeAllInstances(*scene).size(), 3); + ASSERT_EQ(scene->GetMeshGroup(draco::MeshGroupIndex(0))->NumMeshInstances(), + 1); +} + +TEST(SceneUtilsTest, TestCleanupUnusedNodes) { + auto scene = + draco::ReadSceneFromTestFile("CesiumMilkTruck/glTF/CesiumMilkTruck.gltf"); + ASSERT_NE(scene, nullptr); + ASSERT_EQ(scene->NumNodes(), 5); + + draco::SceneUtils::CleanupOptions options; + options.remove_unused_nodes = true; + + // Delete mesh on node 2 and try to remove unused nodes. + // Node 2 is connected to node 1 that has no mesh as well. But node 2 is also + // used in an animation so we don't actually expect anything to be deleted. + scene->GetNode(draco::SceneNodeIndex(2)) + ->SetMeshGroupIndex(draco::kInvalidMeshGroupIndex); + draco::SceneUtils::Cleanup(scene.get(), options); + + ASSERT_EQ(scene->NumNodes(), 5); + + // Now remove the animation channel that used the node and try it again. This + // time, we expect two nodes to be deleted (node 1 and node 2). Node 1 will be + // deleted because it doesn't contain a mesh and all its children are unused. + ASSERT_EQ(scene->GetAnimation(draco::AnimationIndex(0)) + ->GetChannel(0) + ->target_index, + 2); + // Change the mapped node to node 4 (we can't actually remove channel as of + // the time this test was written). + scene->GetAnimation(draco::AnimationIndex(0))->GetChannel(0)->target_index = + 4; + + // Cleanup again. + draco::SceneUtils::Cleanup(scene.get(), options); + ASSERT_EQ(scene->NumNodes(), 3); // Two nodes should be deleted. + + // Ensure all node indices are remapped to the new values. + for (draco::SceneNodeIndex sni(0); sni < scene->NumNodes(); ++sni) { + const auto *node = scene->GetNode(sni); + for (int i = 0; i < node->NumChildren(); ++i) { + ASSERT_LT(node->Child(i).value(), 3); + } + for (int i = 0; i < node->NumParents(); ++i) { + ASSERT_LT(node->Parent(i).value(), 3); + } + } + + // Ensure the animation channels are mapped to the updated node indices (node + // 4 should be new node 2 because two nodes were removed). + ASSERT_EQ(scene->GetAnimation(draco::AnimationIndex(0)) + ->GetChannel(0) + ->target_index, + 2); +} + +TEST(SceneUtilsTest, TestDeduplicateMeshGroups) { + // Input scene has four different mesh groups but only two of them should + // contain unique set of meshes. + auto scene = + draco::ReadSceneFromTestFile("DuplicateMeshes/duplicate_meshes.gltf"); + ASSERT_NE(scene, nullptr); + ASSERT_EQ(scene->NumMeshes(), 1); + ASSERT_EQ(scene->NumMeshGroups(), 4); + ASSERT_EQ(draco::SceneUtils::ComputeAllInstances(*scene).size(), 7); + + draco::SceneUtils::DeduplicateMeshGroups(scene.get()); + + // Check deduplicated scene. + ASSERT_EQ(scene->NumMeshes(), 1); + ASSERT_EQ(scene->NumMeshGroups(), 2); + ASSERT_EQ(draco::SceneUtils::ComputeAllInstances(*scene).size(), 7); +} + +TEST(SceneUtilsTest, TestCleanupUnusedTexCoordsNoTextures) { + // The glTF file has two tex coords that are unused because the materials do + // not reference any textures. + auto scene = draco::ReadSceneFromTestFile("UnusedTexCoords/NoTextures.gltf"); + ASSERT_NE(scene, nullptr); + ASSERT_EQ(scene->GetMesh(draco::MeshIndex(0)) + .NumNamedAttributes(draco::GeometryAttribute::TEX_COORD), + 2); + + // Cleanup scene and check that unused UV are not removed by default. + draco::SceneUtils::Cleanup(scene.get()); + ASSERT_EQ(scene->GetMesh(draco::MeshIndex(0)) + .NumNamedAttributes(draco::GeometryAttribute::TEX_COORD), + 2); + + // Cleanup scene and check that unused UV are removed when requested. + draco::SceneUtils::CleanupOptions options; + options.remove_unused_tex_coords = true; + draco::SceneUtils::Cleanup(scene.get(), options); + ASSERT_EQ(scene->GetMesh(draco::MeshIndex(0)) + .NumNamedAttributes(draco::GeometryAttribute::TEX_COORD), + 0); +} + +TEST(SceneUtilsTest, TestCleanupUnusedTexCoords0NoReferences) { + auto scene = draco::ReadSceneFromTestFile( + "UnusedTexCoords/TexCoord0InvalidTexCoord1Valid.gltf"); + ASSERT_NE(scene, nullptr); + typedef draco::GeometryAttribute Att; + + draco::Mesh &mesh = scene->GetMesh(draco::MeshIndex(0)); + ASSERT_EQ(mesh.NumNamedAttributes(Att::TEX_COORD), 2); + ASSERT_EQ(mesh.GetNamedAttribute(Att::TEX_COORD, 0)->size(), 14); + ASSERT_EQ(mesh.GetNamedAttribute(Att::TEX_COORD, 1)->size(), 4); + auto &ml = scene->GetMaterialLibrary(); + ASSERT_EQ(ml.NumMaterials(), 1); + ASSERT_EQ(ml.GetMaterial(0)->NumTextureMaps(), 1); + ASSERT_EQ(ml.GetMaterial(0)->GetTextureMapByIndex(0)->tex_coord_index(), 1); + + // Cleanup unused texture coordinate attributes. + draco::SceneUtils::CleanupOptions options; + options.remove_unused_tex_coords = true; + draco::SceneUtils::Cleanup(scene.get(), options); + + // Check that the unreferenced attribute was removed. + ASSERT_EQ(mesh.NumNamedAttributes(Att::TEX_COORD), 1); + ASSERT_EQ(mesh.GetNamedAttribute(Att::TEX_COORD, 0)->size(), 4); + ASSERT_EQ(ml.NumMaterials(), 1); + ASSERT_EQ(ml.GetMaterial(0)->NumTextureMaps(), 1); + ASSERT_EQ(ml.GetMaterial(0)->GetTextureMapByIndex(0)->tex_coord_index(), 0); +} + +TEST(SceneUtilsTest, TestCleanupUnusedTexCoords1NoReferences) { + auto scene = draco::ReadSceneFromTestFile( + "UnusedTexCoords/TexCoord0ValidTexCoord1Invalid.gltf"); + ASSERT_NE(scene, nullptr); + typedef draco::GeometryAttribute Att; + + draco::Mesh &mesh = scene->GetMesh(draco::MeshIndex(0)); + ASSERT_EQ(mesh.NumNamedAttributes(Att::TEX_COORD), 2); + ASSERT_EQ(mesh.GetNamedAttribute(Att::TEX_COORD, 0)->size(), 14); + ASSERT_EQ(mesh.GetNamedAttribute(Att::TEX_COORD, 1)->size(), 4); + auto &ml = scene->GetMaterialLibrary(); + ASSERT_EQ(ml.NumMaterials(), 1); + ASSERT_EQ(ml.GetMaterial(0)->NumTextureMaps(), 1); + ASSERT_EQ(ml.GetMaterial(0)->GetTextureMapByIndex(0)->tex_coord_index(), 0); + + // Cleanup unused texture coordinate attributes. + draco::SceneUtils::CleanupOptions options; + options.remove_unused_tex_coords = true; + draco::SceneUtils::Cleanup(scene.get(), options); + + // Check that the unreferenced attribute was removed. + ASSERT_EQ(mesh.NumNamedAttributes(Att::TEX_COORD), 1); + ASSERT_EQ(mesh.GetNamedAttribute(Att::TEX_COORD, 0)->size(), 14); + ASSERT_EQ(ml.NumMaterials(), 1); + ASSERT_EQ(ml.GetMaterial(0)->NumTextureMaps(), 1); + ASSERT_EQ(ml.GetMaterial(0)->GetTextureMapByIndex(0)->tex_coord_index(), 0); +} + +TEST(SceneUtilsTest, TestComputeGlobalNodeTransform) { + // Tests that we can compute global transformation of scene nodes. + + auto scene = draco::ReadSceneFromTestFile("simple_skin.gltf"); + ASSERT_NE(scene, nullptr); + ASSERT_EQ(scene->NumNodes(), 3); + + // Compute and check global node transforms. + constexpr float kTolerance = 1e-6; + // clang-format off + AssertMatrixNear(draco::SceneUtils::ComputeGlobalNodeTransform( + *scene, draco::SceneNodeIndex(0)), + Eigen::Matrix4d::Identity(), + kTolerance); + AssertMatrixNear(draco::SceneUtils::ComputeGlobalNodeTransform( + *scene, draco::SceneNodeIndex(1)), + Eigen::Matrix4d{{1.0, 0.0, 0.0, 0.0}, + {0.0, 1.0, 0.0, 1.0}, + {0.0, 0.0, 1.0, 0.0}, + {0.0, 0.0, 0.0, 1.0}}, + kTolerance); + AssertMatrixNear(draco::SceneUtils::ComputeGlobalNodeTransform( + *scene, draco::SceneNodeIndex(2)), + Eigen::Matrix4d{{1.0, 0.0, 0.0, 0.0}, + {0.0, 1.0, 0.0, 1.0}, + {0.0, 0.0, 1.0, 0.0}, + {0.0, 0.0, 0.0, 1.0}}, + kTolerance); + // clang-format on +} + +TEST(SceneUtilsTest, TestIsDracoCompressionEnabled) { + // Tests that we can determine whether any of the scene meshes have geometry + // compression enabled. + const std::string file = "CesiumMilkTruck/glTF/CesiumMilkTruck.gltf"; + auto scene = draco::ReadSceneFromTestFile(file); + ASSERT_NE(scene, nullptr); + ASSERT_EQ(scene->NumMeshes(), 4); + + // Check that the scene has geometry compression disabled by default. + ASSERT_FALSE(draco::SceneUtils::IsDracoCompressionEnabled(*scene)); + + // Check that geometry compression can be enabled. + scene->GetMesh(MeshIndex(2)).SetCompressionEnabled(true); + ASSERT_TRUE(draco::SceneUtils::IsDracoCompressionEnabled(*scene)); +} + +TEST(SceneUtilsTest, TestSetDracoCompressionOptions) { + // Tests that geometry compression settings can be set for all scene meshes. + const std::string file = "CesiumMilkTruck/glTF/CesiumMilkTruck.gltf"; + auto scene = draco::ReadSceneFromTestFile(file); + ASSERT_NE(scene, nullptr); + ASSERT_EQ(scene->NumMeshes(), 4); + + // Check that compression is initially disabled for all scene meshes. + ASSERT_FALSE(scene->GetMesh(MeshIndex(0)).IsCompressionEnabled()); + ASSERT_FALSE(scene->GetMesh(MeshIndex(1)).IsCompressionEnabled()); + ASSERT_FALSE(scene->GetMesh(MeshIndex(2)).IsCompressionEnabled()); + ASSERT_FALSE(scene->GetMesh(MeshIndex(3)).IsCompressionEnabled()); + + // Check that initially all scene meshes have default compression options. + draco::DracoCompressionOptions defaults; + ASSERT_EQ(scene->GetMesh(MeshIndex(0)).GetCompressionOptions(), defaults); + ASSERT_EQ(scene->GetMesh(MeshIndex(1)).GetCompressionOptions(), defaults); + ASSERT_EQ(scene->GetMesh(MeshIndex(2)).GetCompressionOptions(), defaults); + ASSERT_EQ(scene->GetMesh(MeshIndex(3)).GetCompressionOptions(), defaults); + + // Check geometry compression options can be set to all scene meshes and that + // this also enables compression for all scnene meshes. + draco::DracoCompressionOptions options; + options.compression_level = 10; + options.quantization_bits_normal = 12; + draco::SceneUtils::SetDracoCompressionOptions(&options, scene.get()); + ASSERT_TRUE(scene->GetMesh(MeshIndex(0)).IsCompressionEnabled()); + ASSERT_TRUE(scene->GetMesh(MeshIndex(1)).IsCompressionEnabled()); + ASSERT_TRUE(scene->GetMesh(MeshIndex(2)).IsCompressionEnabled()); + ASSERT_TRUE(scene->GetMesh(MeshIndex(3)).IsCompressionEnabled()); + ASSERT_EQ(scene->GetMesh(MeshIndex(0)).GetCompressionOptions(), options); + ASSERT_EQ(scene->GetMesh(MeshIndex(1)).GetCompressionOptions(), options); + ASSERT_EQ(scene->GetMesh(MeshIndex(2)).GetCompressionOptions(), options); + ASSERT_EQ(scene->GetMesh(MeshIndex(3)).GetCompressionOptions(), options); + + // Check that geometry compression can be disabled for all scene meshes. + draco::SceneUtils::SetDracoCompressionOptions(nullptr, scene.get()); + ASSERT_FALSE(scene->GetMesh(MeshIndex(0)).IsCompressionEnabled()); + ASSERT_FALSE(scene->GetMesh(MeshIndex(1)).IsCompressionEnabled()); + ASSERT_FALSE(scene->GetMesh(MeshIndex(2)).IsCompressionEnabled()); + ASSERT_FALSE(scene->GetMesh(MeshIndex(3)).IsCompressionEnabled()); +} + +TEST(SceneUtilsTest, TestFindLargestBaseMeshTransforms) { + // Tests that FindLargestBaseMeshTransforms() works as expected. + auto scene = + draco::ReadSceneFromTestFile("CubeScaledInstances/glTF/cube_att.gltf"); + ASSERT_NE(scene, nullptr); + + // There should be one base mesh with four instances. + ASSERT_EQ(scene->NumMeshes(), 1); + ASSERT_EQ(draco::SceneUtils::ComputeAllInstances(*scene).size(), 4); + + const auto transforms = + draco::SceneUtils::FindLargestBaseMeshTransforms(*scene); + + ASSERT_EQ(transforms.size(), 1); // One transform for the single base mesh. + + // The largest instance should have a uniform scale 4. + const draco::MeshIndex mi(0); + ASSERT_EQ(transforms[mi].diagonal(), Eigen::Vector4d(4, 4, 4, 1)); +} + +} // namespace + +#endif // DRACO_TRANSCODER_SUPPORTED diff --git a/contrib/draco/src/draco/scene/trs_matrix.cc b/contrib/draco/src/draco/scene/trs_matrix.cc new file mode 100644 index 000000000..6e6dac251 --- /dev/null +++ b/contrib/draco/src/draco/scene/trs_matrix.cc @@ -0,0 +1,102 @@ +// Copyright 2019 The Draco Authors. +// +// 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 "draco/scene/trs_matrix.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED + +namespace draco { + +void TrsMatrix::Copy(const TrsMatrix &tm) { + matrix_ = tm.matrix_; + translation_ = tm.translation_; + rotation_ = tm.rotation_; + scale_ = tm.scale_; + matrix_set_ = tm.matrix_set_; + translation_set_ = tm.translation_set_; + rotation_set_ = tm.rotation_set_; + scale_set_ = tm.scale_set_; +} + +Eigen::Matrix4d TrsMatrix::ComputeTransformationMatrix() const { + // Return transformation matrix if it has been set. + if (matrix_set_) { + return matrix_; + } + + // Populate translation matrix. + Eigen::Matrix4d translation_matrix = Eigen::Matrix4d::Identity(); + translation_matrix(0, 3) = translation_[0]; + translation_matrix(1, 3) = translation_[1]; + translation_matrix(2, 3) = translation_[2]; + + // Populate rotation matrix using rotation quaternion. + Eigen::Matrix3d rotation_matrix_3 = rotation_.normalized().toRotationMatrix(); + + // Convert the 3x3 matrix to a 4x4 matrix that can be multiplied with the + // other TRS matrices. + Eigen::Matrix4d rotation_matrix = Eigen::Matrix4d::Identity(); + rotation_matrix.block<3, 3>(0, 0) = rotation_matrix_3; + + // Populate scale matrix. + const Eigen::Matrix4d scale_matrix( + Eigen::Vector4d(scale_.x(), scale_.y(), scale_.z(), 1.0).asDiagonal()); + + // Return transformation matrix computed by combining TRS matrices. + return translation_matrix * rotation_matrix * scale_matrix; +} + +bool TrsMatrix::IsMatrixIdentity() const { + if (!matrix_set_) { + return true; + } + return matrix_ == Eigen::Matrix4d::Identity(); +} + +bool TrsMatrix::IsMatrixTranslationOnly() const { + if (!matrix_set_) { + return false; + } + Eigen::Matrix4d translation_check = matrix_; + translation_check(0, 3) = 0.0; + translation_check(1, 3) = 0.0; + translation_check(2, 3) = 0.0; + return translation_check == Eigen::Matrix4d::Identity(); +} + +bool TrsMatrix::operator==(const TrsMatrix &trs_matrix) const { + if (matrix_set_ != trs_matrix.matrix_set_ || + translation_set_ != trs_matrix.translation_set_ || + rotation_set_ != trs_matrix.rotation_set_ || + scale_set_ != trs_matrix.scale_set_) { + return false; + } + if (matrix_set_ && matrix_ != trs_matrix.matrix_) { + return false; + } + if (translation_set_ && translation_ != trs_matrix.translation_) { + return false; + } + if (rotation_set_ && rotation_ != trs_matrix.rotation_) { + return false; + } + if (scale_set_ && scale_set_ != trs_matrix.scale_set_) { + return false; + } + return true; +} + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED diff --git a/contrib/draco/src/draco/scene/trs_matrix.h b/contrib/draco/src/draco/scene/trs_matrix.h new file mode 100644 index 000000000..6c2ab7388 --- /dev/null +++ b/contrib/draco/src/draco/scene/trs_matrix.h @@ -0,0 +1,124 @@ +// Copyright 2019 The Draco Authors. +// +// 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 DRACO_SCENE_TRS_MATRIX_H_ +#define DRACO_SCENE_TRS_MATRIX_H_ + +#include "draco/draco_features.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED +#include "Eigen/Geometry" +#include "draco/core/status_or.h" + +namespace draco { + +// This class is used to store one or more of a translation, rotation, scale +// vectors or a transformation matrix. +class TrsMatrix { + public: + TrsMatrix() + : matrix_(Eigen::Matrix4d::Identity()), + translation_(0.0, 0.0, 0.0), + rotation_(1.0, 0.0, 0.0, 0.0), // (w, x, y, z) + scale_(1.0, 1.0, 1.0), + matrix_set_(false), + translation_set_(false), + rotation_set_(false), + scale_set_(false) {} + + void Copy(const TrsMatrix &tm); + + void SetMatrix(const Eigen::Matrix4d &matrix) { + matrix_ = matrix; + matrix_set_ = true; + } + bool MatrixSet() const { return matrix_set_; } + StatusOr Matrix() const { + if (!matrix_set_) { + return Status(Status::DRACO_ERROR, "Matrix is not set."); + } + return matrix_; + } + + void SetTranslation(const Eigen::Vector3d &translation) { + translation_ = translation; + translation_set_ = true; + } + bool TranslationSet() const { return translation_set_; } + StatusOr Translation() const { + if (!translation_set_) { + return Status(Status::DRACO_ERROR, "Translation is not set."); + } + return translation_; + } + + void SetRotation(const Eigen::Quaterniond &rotation) { + rotation_ = rotation; + rotation_set_ = true; + } + bool RotationSet() const { return rotation_set_; } + StatusOr Rotation() const { + if (!rotation_set_) { + return Status(Status::DRACO_ERROR, "Rotation is not set."); + } + return rotation_; + } + + void SetScale(const Eigen::Vector3d &scale) { + scale_ = scale; + scale_set_ = true; + } + bool ScaleSet() const { return scale_set_; } + StatusOr Scale() const { + if (!scale_set_) { + return Status(Status::DRACO_ERROR, "Scale is not set."); + } + return scale_; + } + + // Returns true if the matrix is not set or if matrix is set and is equal to + // identity. + bool IsMatrixIdentity() const; + + // Returns true if matrix is set and only the translation elements may differ + // from identity. Returns false if matrix is not set. + bool IsMatrixTranslationOnly() const; + + // Returns transformation matrix if it has been set. Otherwise, computes + // transformation matrix from TRS vectors and returns it. + Eigen::Matrix4d ComputeTransformationMatrix() const; + + // Returns a boolean indicating whether any of the transforms have been set. + // Can be used to check whether this object represents a default transform. + bool TransformSet() const { + return matrix_set_ || translation_set_ || rotation_set_ || scale_set_; + } + + bool operator==(const TrsMatrix &trs_matrix) const; + + private: + Eigen::Matrix4d matrix_; + Eigen::Vector3d translation_; + Eigen::Quaterniond rotation_; + Eigen::Vector3d scale_; + bool matrix_set_; + bool translation_set_; + bool rotation_set_; + bool scale_set_; +}; + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED +#endif // DRACO_SCENE_TRS_MATRIX_H_ diff --git a/contrib/draco/src/draco/scene/trs_matrix_test.cc b/contrib/draco/src/draco/scene/trs_matrix_test.cc new file mode 100644 index 000000000..d7938e974 --- /dev/null +++ b/contrib/draco/src/draco/scene/trs_matrix_test.cc @@ -0,0 +1,79 @@ +// Copyright 2019 The Draco Authors. +// +// 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 "draco/scene/trs_matrix.h" + +#include "draco/core/draco_test_base.h" +#include "draco/core/draco_test_utils.h" + +namespace { + +#ifdef DRACO_TRANSCODER_SUPPORTED + +TEST(TrsMatrixTest, TestIsMatrixIdentity) { + draco::TrsMatrix trs; + ASSERT_EQ(trs.MatrixSet(), false); + ASSERT_EQ(trs.IsMatrixIdentity(), true); + + // clang-format off + Eigen::Matrix4d matrix; + matrix << 1, 2, 3, 4, + 5, 6, 7, 8, + 9, 10, 11, 12, + 13, 14, 15, 16; + // clang-format on + trs.SetMatrix(matrix); + ASSERT_EQ(trs.MatrixSet(), true); + ASSERT_EQ(trs.IsMatrixIdentity(), false); + + trs.SetMatrix(Eigen::Matrix4d::Identity()); + ASSERT_EQ(trs.MatrixSet(), true); + ASSERT_EQ(trs.IsMatrixIdentity(), true); +} + +TEST(TrsMatrixTest, TestIsMatrixTranslationOnly) { + draco::TrsMatrix trs; + ASSERT_EQ(trs.MatrixSet(), false); + ASSERT_EQ(trs.IsMatrixTranslationOnly(), false); + + trs.SetMatrix(Eigen::Matrix4d::Identity()); + ASSERT_EQ(trs.MatrixSet(), true); + ASSERT_EQ(trs.IsMatrixTranslationOnly(), true); + + // clang-format off + Eigen::Matrix4d matrix; + matrix << 1, 2, 3, 4, + 5, 6, 7, 8, + 9, 10, 11, 12, + 13, 14, 15, 16; + // clang-format on + trs.SetMatrix(matrix); + ASSERT_EQ(trs.MatrixSet(), true); + ASSERT_EQ(trs.IsMatrixTranslationOnly(), false); + + // clang-format off + Eigen::Matrix4d translation; + translation << 1, 0, 0, 1, + 0, 1, 0, 2, + 0, 0, 1, 3, + 0, 0, 0, 1; + // clang-format on + trs.SetMatrix(translation); + ASSERT_EQ(trs.MatrixSet(), true); + ASSERT_EQ(trs.IsMatrixTranslationOnly(), true); +} + +#endif // DRACO_TRANSCODER_SUPPORTED + +} // namespace diff --git a/contrib/draco/src/draco/texture/source_image.cc b/contrib/draco/src/draco/texture/source_image.cc new file mode 100644 index 000000000..b4d493250 --- /dev/null +++ b/contrib/draco/src/draco/texture/source_image.cc @@ -0,0 +1,29 @@ +// Copyright 2021 The Draco Authors. +// +// 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 "draco/texture/source_image.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED + +namespace draco { + +void SourceImage::Copy(const SourceImage &src) { + mime_type_ = src.mime_type_; + filename_ = src.filename_; + encoded_data_ = src.encoded_data_; +} + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED diff --git a/contrib/draco/src/draco/texture/source_image.h b/contrib/draco/src/draco/texture/source_image.h new file mode 100644 index 000000000..5827918e4 --- /dev/null +++ b/contrib/draco/src/draco/texture/source_image.h @@ -0,0 +1,72 @@ +// Copyright 2021 The Draco Authors. +// +// 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 DRACO_TEXTURE_SOURCE_IMAGE_H_ +#define DRACO_TEXTURE_SOURCE_IMAGE_H_ + +#include "draco/draco_features.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED +#include +#include +#include + +#include "draco/core/status.h" + +namespace draco { + +// This class is used to hold the encoded and decoded data and characteristics +// for an image. In order for the image to contain "valid" encoded data, either +// the |filename_| must point to a valid image file or the |mime_type_| and +// |encoded_data_| must contain valid image data. +class SourceImage { + public: + SourceImage() {} + + // No copy constructors. + SourceImage(const SourceImage &) = delete; + SourceImage &operator=(const SourceImage &) = delete; + // No move constructors. + SourceImage(SourceImage &&) = delete; + SourceImage &operator=(SourceImage &&) = delete; + + void Copy(const SourceImage &src); + + // Sets the name of the source image file. + void set_filename(const std::string &filename) { filename_ = filename; } + const std::string &filename() const { return filename_; } + + void set_mime_type(const std::string &mime_type) { mime_type_ = mime_type; } + const std::string &mime_type() const { return mime_type_; } + + std::vector &MutableEncodedData() { return encoded_data_; } + const std::vector &encoded_data() const { return encoded_data_; } + + private: + // The filename of the image. This field can be empty as long as |mime_type_| + // and |encoded_data_| is not empty. + std::string filename_; + + // The mimetype of the |encoded_data_|. + std::string mime_type_; + + // The encoded data of the image. This field can be empty as long as + // |filename_| is not empty. + std::vector encoded_data_; +}; + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED +#endif // DRACO_TEXTURE_SOURCE_IMAGE_H_ diff --git a/contrib/draco/src/draco/texture/texture.h b/contrib/draco/src/draco/texture/texture.h new file mode 100644 index 000000000..1d3b6e382 --- /dev/null +++ b/contrib/draco/src/draco/texture/texture.h @@ -0,0 +1,46 @@ +// Copyright 2021 The Draco Authors. +// +// 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 DRACO_TEXTURE_TEXTURE_H_ +#define DRACO_TEXTURE_TEXTURE_H_ + +#include "draco/draco_features.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED +#include +#include + +#include "draco/io/image_compression_options.h" +#include "draco/texture/source_image.h" + +namespace draco { + +// Texture class storing the source image data. +class Texture { + public: + void Copy(Texture &other) { source_image_.Copy(other.source_image_); } + + void set_source_image(const SourceImage &image) { source_image_.Copy(image); } + const SourceImage &source_image() const { return source_image_; } + SourceImage &source_image() { return source_image_; } + + private: + // If set this is the image that this texture is based from. + SourceImage source_image_; +}; + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED +#endif // DRACO_TEXTURE_TEXTURE_H_ diff --git a/contrib/draco/src/draco/texture/texture_library.cc b/contrib/draco/src/draco/texture/texture_library.cc new file mode 100644 index 000000000..221ff28d4 --- /dev/null +++ b/contrib/draco/src/draco/texture/texture_library.cc @@ -0,0 +1,61 @@ +// Copyright 2019 The Draco Authors. +// +// 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 "draco/texture/texture_library.h" + +#include + +#ifdef DRACO_TRANSCODER_SUPPORTED + +namespace draco { + +void TextureLibrary::Copy(const TextureLibrary &src) { + Clear(); + Append(src); +} + +void TextureLibrary::Append(const TextureLibrary &src) { + const size_t old_num_textures = textures_.size(); + textures_.resize(old_num_textures + src.textures_.size()); + for (int i = 0; i < src.textures_.size(); ++i) { + textures_[old_num_textures + i] = std::unique_ptr(new Texture()); + textures_[old_num_textures + i]->Copy(*src.textures_[i]); + } +} + +void TextureLibrary::Clear() { textures_.clear(); } + +int TextureLibrary::PushTexture(std::unique_ptr texture) { + textures_.push_back(std::move(texture)); + return textures_.size() - 1; +} + +std::unordered_map +TextureLibrary::ComputeTextureToIndexMap() const { + std::unordered_map ret; + for (int i = 0; i < textures_.size(); ++i) { + ret[textures_[i].get()] = i; + } + return ret; +} + +std::unique_ptr TextureLibrary::RemoveTexture(int index) { + std::unique_ptr ret = std::move(textures_[index]); + textures_.erase(textures_.begin() + index); + return ret; +} + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED diff --git a/contrib/draco/src/draco/texture/texture_library.h b/contrib/draco/src/draco/texture/texture_library.h new file mode 100644 index 000000000..a377d8fbc --- /dev/null +++ b/contrib/draco/src/draco/texture/texture_library.h @@ -0,0 +1,67 @@ +// Copyright 2019 The Draco Authors. +// +// 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 DRACO_TEXTURE_TEXTURE_LIBRARY_H_ +#define DRACO_TEXTURE_TEXTURE_LIBRARY_H_ + +#include "draco/draco_features.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED +#include +#include +#include + +#include "draco/texture/texture.h" + +namespace draco { + +// Container class for storing draco::Texture objects in an indexed list. +class TextureLibrary { + public: + // Copies textures from the source library to this library. Order of the + // copied textures is preserved. + void Copy(const TextureLibrary &src); + + // Appends all textures from the source library to this library. All textures + // are copied over. + void Append(const TextureLibrary &src); + + // Removes all textures from the library. + void Clear(); + + // Pushes a new texture into the library. Returns an index of the newly + // inserted texture. + int PushTexture(std::unique_ptr texture); + + size_t NumTextures() const { return textures_.size(); } + + Texture *GetTexture(int index) { return textures_[index].get(); } + const Texture *GetTexture(int index) const { return textures_[index].get(); } + + // Returns a map from texture pointer to texture index for all textures. + std::unordered_map ComputeTextureToIndexMap() const; + + // Removes and returns a texture from the library. The returned texture can be + // either used by the caller or ignored in which case it would be + // automatically deleted. + std::unique_ptr RemoveTexture(int index); + + private: + std::vector> textures_; +}; + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED +#endif // DRACO_TEXTURE_TEXTURE_LIBRARY_H_ diff --git a/contrib/draco/src/draco/texture/texture_library_test.cc b/contrib/draco/src/draco/texture/texture_library_test.cc new file mode 100644 index 000000000..4d681fdd2 --- /dev/null +++ b/contrib/draco/src/draco/texture/texture_library_test.cc @@ -0,0 +1,22 @@ +// Copyright 2019 The Draco Authors. +// +// 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 "draco/texture/texture_library.h" + +#include + +#include "draco/core/draco_test_utils.h" +#include "draco/io/texture_io.h" + +namespace {} // namespace diff --git a/contrib/draco/src/draco/texture/texture_map.cc b/contrib/draco/src/draco/texture/texture_map.cc new file mode 100644 index 000000000..459d3f600 --- /dev/null +++ b/contrib/draco/src/draco/texture/texture_map.cc @@ -0,0 +1,86 @@ +// Copyright 2021 The Draco Authors. +// +// 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 "draco/texture/texture_map.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED + +namespace draco { + +TextureMap::TextureMap() + : type_(TextureMap::GENERIC), + wrapping_mode_(CLAMP_TO_EDGE), + tex_coord_index_(-1), + min_filter_(UNSPECIFIED), + mag_filter_(UNSPECIFIED), + texture_(nullptr) {} + +void TextureMap::Copy(const TextureMap &src) { + type_ = src.type_; + wrapping_mode_ = src.wrapping_mode_; + tex_coord_index_ = src.tex_coord_index_; + min_filter_ = src.min_filter_; + mag_filter_ = src.mag_filter_; + if (src.owned_texture_ == nullptr) { + owned_texture_ = nullptr; + texture_ = src.texture_; + } else { + std::unique_ptr new_texture(new Texture()); + new_texture->Copy(*src.owned_texture_); + owned_texture_ = std::move(new_texture); + texture_ = owned_texture_.get(); + } + texture_transform_.Copy(src.texture_transform_); +} + +void TextureMap::SetProperties(Type type) { + SetProperties(type, WrappingMode(CLAMP_TO_EDGE), 0); +} + +void TextureMap::SetProperties(TextureMap::Type type, int tex_coord_index) { + SetProperties(type, WrappingMode(CLAMP_TO_EDGE), tex_coord_index); +} + +void TextureMap::SetProperties(Type type, WrappingMode wrapping_mode, + int tex_coord_index) { + SetProperties(type, wrapping_mode, tex_coord_index, UNSPECIFIED, UNSPECIFIED); +} + +void TextureMap::SetProperties(Type type, WrappingMode wrapping_mode, + int tex_coord_index, FilterType min_filter, + FilterType mag_filter) { + type_ = type; + wrapping_mode_ = wrapping_mode; + tex_coord_index_ = tex_coord_index; + min_filter_ = min_filter; + mag_filter_ = mag_filter; +} + +void TextureMap::SetTexture(std::unique_ptr texture) { + owned_texture_ = std::move(texture); + texture_ = owned_texture_.get(); +} + +void TextureMap::SetTexture(Texture *texture) { + owned_texture_ = nullptr; + texture_ = texture; +} + +void TextureMap::SetTransform(const TextureTransform &transform) { + texture_transform_.Copy(transform); +} + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED diff --git a/contrib/draco/src/draco/texture/texture_map.h b/contrib/draco/src/draco/texture/texture_map.h new file mode 100644 index 000000000..f3a95b501 --- /dev/null +++ b/contrib/draco/src/draco/texture/texture_map.h @@ -0,0 +1,175 @@ +// Copyright 2021 The Draco Authors. +// +// 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 DRACO_TEXTURE_TEXTURE_MAP_H_ +#define DRACO_TEXTURE_TEXTURE_MAP_H_ + +#include "draco/draco_features.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED +#include + +#include "draco/texture/texture.h" +#include "draco/texture/texture_transform.h" + +namespace draco { + +// Class representing mapping of one texture to a mesh. A texture map +// specifies the mesh attribute that contains texture coordinates used by the +// texture. The class also defines an intended use of the texture as a so called +// mapping type (COLOR, NORMAL_TANGENT_SPACE, etc..). Mapping types are roughly +// based on GLTF 2.0 material spec that describes a metallic-roughness PBR +// material model. More details can be found here: +// https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#materials +class TextureMap { + public: + enum Type { + // Generic purpose texture (not GLTF compliant). + GENERIC = 0, + // Color data with optional alpha channel for transparency (GLTF compliant). + COLOR = 1, + // Dedicated texture for storing transparency (not GLTF compliant). + OPACITY = 2, + // Dedicated texture for storing metallic property (not GLTF compliant). + METALLIC = 3, + // Dedicated texture for storing roughness property (not GLTF compliant). + ROUGHNESS = 4, + // Combined texture for storing metallic and roughness properties. + // B == metallic, G == roughness (GLTF compliant). + METALLIC_ROUGHNESS = 5, + // Normal map defined in the object space of the mesh (not GLTF compliant). + NORMAL_OBJECT_SPACE = 6, + // Normal map defined in the tangent space of the mesh (GLTF compliant). + NORMAL_TANGENT_SPACE = 7, + // Precomputed ambient occlusion on the surface (GLTF compliant). + AMBIENT_OCCLUSION = 8, + // Emissive color (GLTF compliant). + EMISSIVE = 9, + // Texture types of glTF material extension KHR_materials_sheen. + SHEEN_COLOR = 10, + SHEEN_ROUGHNESS = 11, + // Texture types of glTF material extension KHR_materials_transmission. + TRANSMISSION = 12, + // Texture types of glTF material extension KHR_materials_clearcoat. + CLEARCOAT = 13, + CLEARCOAT_ROUGHNESS = 14, + CLEARCOAT_NORMAL = 15, + // Texture types of glTF material extension KHR_materials_volume. + THICKNESS = 16, + // Texture types of glTF material extension KHR_materials_specular. + SPECULAR = 17, + SPECULAR_COLOR = 18, + // The number of texture types. + TEXTURE_TYPES_COUNT + }; + + enum AxisWrappingMode { + // Out of bounds access along a texture axis should be clamped to the + // nearest edge (default). + CLAMP_TO_EDGE = 0, + // Texture is repeated along a texture axis in a mirrored pattern. + MIRRORED_REPEAT, + // Texture is repeated along a texture axis (tiled textures). + REPEAT + }; + + struct WrappingMode { + explicit WrappingMode(AxisWrappingMode mode) : WrappingMode(mode, mode) {} + WrappingMode(AxisWrappingMode s, AxisWrappingMode t) : s(s), t(t) {} + AxisWrappingMode s; + AxisWrappingMode t; + }; + + // Filter types are roughly based on glTF 2.0 samplers spec. + // https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#samplers + enum FilterType { + UNSPECIFIED = 0, + NEAREST, + LINEAR, + NEAREST_MIPMAP_NEAREST, + LINEAR_MIPMAP_NEAREST, + NEAREST_MIPMAP_LINEAR, + LINEAR_MIPMAP_LINEAR + }; + + TextureMap(); + TextureMap(TextureMap &&) = default; + + // Copies texture map data from the |src| texture map to this texture map. + void Copy(const TextureMap &src); + + // Sets the mapping information between the texture and the target mesh. + // |tex_coord_index| is the local index of the texture coordinates that is + // used to map the texture on the mesh. + void SetProperties(Type type); + void SetProperties(Type type, int tex_coord_index); + void SetProperties(Type type, WrappingMode wrapping_mode, + int tex_coord_index); + void SetProperties(Type type, WrappingMode wrapping_mode, int tex_coord_index, + FilterType min_filter, FilterType mag_filter); + + // Set texture and transfer its ownership to the TextureMap object. + // + // Note that this should not be used if this TextureMap is part of a + // MaterialLibrary. For such cases, the TextureMap's texture should refer to + // an entry in the MaterialLibrary's TextureLibrary. + void SetTexture(std::unique_ptr texture); + + // Set texture and without transferring the ownership. The caller needs to + // ensure the texture is valid during the lifetime of the TextureMap object. + void SetTexture(Texture *texture); + + void SetTransform(const TextureTransform &transform); + const TextureTransform &texture_transform() const { + return texture_transform_; + } + + const Texture *texture() const { return texture_; } + Texture *texture() { return texture_; } + Type type() const { return type_; } + WrappingMode wrapping_mode() const { return wrapping_mode_; } + int tex_coord_index() const { return tex_coord_index_; } + FilterType min_filter() const { return min_filter_; } + FilterType mag_filter() const { return mag_filter_; } + + TextureMap &operator=(TextureMap &&) = default; + + private: + Type type_; + WrappingMode wrapping_mode_; + + // Local index of the texture coordinates that is used to map the texture on + // the mesh. For example, |tex_coord_index_ == 0| would correspond to the + // first TEX_COORD attribute of the mesh. + int tex_coord_index_; + + FilterType min_filter_; + FilterType mag_filter_; + + // Used when the texture object is owned by TextureMap, otherwise set to + // nullptr. + std::unique_ptr owned_texture_; + + // Either raw pointer owned by |owned_texture_| or a pointer to a user + // specified texture in case |owned_texture_| is nullptr. + Texture *texture_; + + // Transformation values of the texture map. + TextureTransform texture_transform_; +}; + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED +#endif // DRACO_TEXTURE_TEXTURE_MAP_H_ diff --git a/contrib/draco/src/draco/texture/texture_map_test.cc b/contrib/draco/src/draco/texture/texture_map_test.cc new file mode 100644 index 000000000..e69de29bb diff --git a/contrib/draco/src/draco/texture/texture_transform.cc b/contrib/draco/src/draco/texture/texture_transform.cc new file mode 100644 index 000000000..ccb00d59f --- /dev/null +++ b/contrib/draco/src/draco/texture/texture_transform.cc @@ -0,0 +1,79 @@ +// Copyright 2021 The Draco Authors. +// +// 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 "draco/texture/texture_transform.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED + +namespace draco { + +TextureTransform::TextureTransform() { Clear(); } + +void TextureTransform::Clear() { + offset_ = TextureTransform::GetDefaultOffset(); + rotation_ = TextureTransform::GetDefaultRotation(); + scale_ = TextureTransform::GetDefaultScale(); + tex_coord_ = TextureTransform::GetDefaultTexCoord(); +} + +void TextureTransform::Copy(const TextureTransform &src) { + offset_ = src.offset_; + rotation_ = src.rotation_; + scale_ = src.scale_; + tex_coord_ = src.tex_coord_; +} + +bool TextureTransform::IsDefault(const TextureTransform &tt) { + const TextureTransform defaults; + if (tt == defaults) { + return true; + } + return false; +} + +bool TextureTransform::IsOffsetSet() const { + return offset_ != TextureTransform::GetDefaultOffset(); +} + +bool TextureTransform::IsRotationSet() const { + return rotation_ != TextureTransform::GetDefaultRotation(); +} + +bool TextureTransform::IsScaleSet() const { + return scale_ != TextureTransform::GetDefaultScale(); +} + +bool TextureTransform::IsTexCoordSet() const { + return tex_coord_ != TextureTransform::GetDefaultTexCoord(); +} + +bool TextureTransform::operator==(const TextureTransform &tt) const { + if (tex_coord_ != tt.tex_coord_) { + return false; + } + if (rotation_ != tt.rotation_) { + return false; + } + if (offset_ != tt.offset_) { + return false; + } + if (scale_ != tt.scale_) { + return false; + } + return true; +} + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED diff --git a/contrib/draco/src/draco/texture/texture_transform.h b/contrib/draco/src/draco/texture/texture_transform.h new file mode 100644 index 000000000..b2ec47f2e --- /dev/null +++ b/contrib/draco/src/draco/texture/texture_transform.h @@ -0,0 +1,75 @@ +// Copyright 2021 The Draco Authors. +// +// 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 DRACO_TEXTURE_TEXTURE_TRANSFORM_H_ +#define DRACO_TEXTURE_TEXTURE_TRANSFORM_H_ + +#include "draco/draco_features.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED +#include +#include + +namespace draco { + +// Class to hold texture transformations. Parameters are based on the glTF 2.0 +// extension KHR_texture_transform: +// https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_texture_transform +class TextureTransform { + public: + TextureTransform(); + + // Resets the values back to defaults. + void Clear(); + + // Copies texture transform data from the |src| texture transform to this + // texture transform. + void Copy(const TextureTransform &src); + + // Returns true if |tt| contains all default values. + static bool IsDefault(const TextureTransform &tt); + + bool IsOffsetSet() const; + bool IsRotationSet() const; + bool IsScaleSet() const; + bool IsTexCoordSet() const; + + void set_offset(const std::array &offset) { offset_ = offset; } + const std::array &offset() const { return offset_; } + void set_scale(const std::array &scale) { scale_ = scale; } + const std::array &scale() const { return scale_; } + + void set_rotation(double rotation) { rotation_ = rotation; } + double rotation() const { return rotation_; } + void set_tex_coord(int tex_coord) { tex_coord_ = tex_coord; } + int tex_coord() const { return tex_coord_; } + + bool operator==(const TextureTransform &tt) const; + + private: + static std::array GetDefaultOffset() { return {0.0, 0.0}; } + static float GetDefaultRotation() { return 0.0; } + static std::array GetDefaultScale() { return {0.0, 0.0}; } + static int GetDefaultTexCoord() { return -1; } + + std::array offset_; + double rotation_; + std::array scale_; + int tex_coord_; +}; + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED +#endif // DRACO_TEXTURE_TEXTURE_TRANSFORM_H_ diff --git a/contrib/draco/src/draco/texture/texture_transform_test.cc b/contrib/draco/src/draco/texture/texture_transform_test.cc new file mode 100644 index 000000000..e69de29bb diff --git a/contrib/draco/src/draco/texture/texture_utils.cc b/contrib/draco/src/draco/texture/texture_utils.cc new file mode 100644 index 000000000..7598ac780 --- /dev/null +++ b/contrib/draco/src/draco/texture/texture_utils.cc @@ -0,0 +1,144 @@ +// Copyright 2019 The Draco Authors. +// +// 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 "draco/texture/texture_utils.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED +#include +#include +#include +#include +#include +#include +#include + +namespace draco { + +std::string TextureUtils::GetTargetStem(const Texture &texture) { + // Return stem of the source image if there is one. + if (!texture.source_image().filename().empty()) { + const std::string &full_path = texture.source_image().filename(); + std::string folder_path; + std::string filename; + SplitPath(full_path, &folder_path, &filename); + return RemoveFileExtension(filename); + } + + // Return an empty stem. + return ""; +} + +std::string TextureUtils::GetOrGenerateTargetStem(const Texture &texture, + int index, + const std::string &suffix) { + // Return target stem from |texture| if there is one. + const std::string name = GetTargetStem(texture); + if (!name.empty()) { + return name; + } + + // Return target stem generated from |index| and |suffix|. + return "Texture" + std::to_string(index) + suffix; +} + +ImageFormat TextureUtils::GetTargetFormat(const Texture &texture) { + // Return format based on source image mime type. + return GetSourceFormat(texture); +} + +std::string TextureUtils::GetTargetExtension(const Texture &texture) { + return GetExtension(GetTargetFormat(texture)); +} + +ImageFormat TextureUtils::GetSourceFormat(const Texture &texture) { + // Try to get the extension based on source image mime type. + std::string extension = + LowercaseMimeTypeExtension(texture.source_image().mime_type()); + if (extension.empty() && !texture.source_image().filename().empty()) { + // Try to get the extension from the source image filename. + extension = LowercaseFileExtension(texture.source_image().filename()); + } + if (extension.empty()) { + // Default to png. + extension = "png"; + } + return GetFormat(extension); +} + +ImageFormat TextureUtils::GetFormat(const std::string &extension) { + if (extension == "png") { + return ImageFormat::PNG; + } else if (extension == "jpg" || extension == "jpeg") { + return ImageFormat::JPEG; + } else if (extension == "basis" || extension == "ktx2") { + return ImageFormat::BASIS; + } else if (extension == "webp") { + return ImageFormat::WEBP; + } + return ImageFormat::NONE; +} + +std::string TextureUtils::GetExtension(ImageFormat format) { + switch (format) { + case ImageFormat::PNG: + return "png"; + case ImageFormat::JPEG: + return "jpg"; + case ImageFormat::BASIS: + return "ktx2"; + case ImageFormat::WEBP: + return "webp"; + case ImageFormat::NONE: + default: + return ""; + } +} + +int TextureUtils::ComputeRequiredNumChannels( + const Texture &texture, const MaterialLibrary &material_library) { + // TODO(vytyaz): Consider a case where |texture| is not only used in OMR but + // also in other texture map types. + const auto mr_textures = TextureUtils::FindTextures( + TextureMap::METALLIC_ROUGHNESS, &material_library); + if (std::find(mr_textures.begin(), mr_textures.end(), &texture) == + mr_textures.end()) { + // Occlusion-only texture. + return 1; + } + // Occlusion-metallic-roughness texture. + return 3; +} + +std::vector TextureUtils::FindTextures( + const TextureMap::Type texture_type, + const MaterialLibrary *material_library) { + // Find textures with no duplicates. + std::unordered_set textures; + for (int i = 0; i < material_library->NumMaterials(); ++i) { + const TextureMap *const texture_map = + material_library->GetMaterial(i)->GetTextureMapByType(texture_type); + if (texture_map != nullptr && texture_map->texture() != nullptr) { + textures.insert(texture_map->texture()); + } + } + + // Return the textures as a vector. + std::vector result; + result.insert(result.end(), textures.begin(), textures.end()); + return result; +} + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED diff --git a/contrib/draco/src/draco/texture/texture_utils.h b/contrib/draco/src/draco/texture/texture_utils.h new file mode 100644 index 000000000..18d29950a --- /dev/null +++ b/contrib/draco/src/draco/texture/texture_utils.h @@ -0,0 +1,78 @@ +// Copyright 2019 The Draco Authors. +// +// 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 DRACO_TEXTURE_TEXTURE_UTILS_H_ +#define DRACO_TEXTURE_TEXTURE_UTILS_H_ + +#include "draco/draco_features.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED +#include "draco/core/status.h" +#include "draco/core/status_or.h" +#include "draco/io/file_utils.h" +#include "draco/material/material_library.h" +#include "draco/texture/texture_library.h" +#include "draco/texture/texture_map.h" + +namespace draco { + +// Helper class implementing various utilities operating on draco::Texture. +class TextureUtils { + public: + // Returns |texture| image stem (file basename without extension) based on the + // source image filename or an empty string when source image is not set. + static std::string GetTargetStem(const Texture &texture); + + // Returns |texture| image stem (file basename without extension) based on the + // source image filename or a name generated from |index| and |suffix| like + // "Texture5_BaseColor" when source image is not set. + static std::string GetOrGenerateTargetStem(const Texture &texture, int index, + const std::string &suffix); + + // Returns |texture| format based on compression settings, the source image + // mime type or the source image filename. + static ImageFormat GetTargetFormat(const Texture &texture); + + // Returns |texture| image file extension based on compression settings, the + // source image mime type or the source image filename. + static std::string GetTargetExtension(const Texture &texture); + + // Returns |texture| format based on source image mime type or the source + // image filename. + static ImageFormat GetSourceFormat(const Texture &texture); + + // Returns image format corresponding to a given image file |extension|. NONE + // is returned when |extension| is empty or unknown. + static ImageFormat GetFormat(const std::string &extension); + + // Returns image file extension corresponding to a given image |format|. Empty + // extension is returned when the |format| is NONE. + static std::string GetExtension(ImageFormat format); + + // Returns the number of channels required for encoding a |texture| from a + // given |material_library|, taking into account texture opacity and assuming + // that occlusion and metallic-roughness texture maps may share a texture. + // TODO(vytyaz): Move this and FindTextures() to MaterialLibrary class. + static int ComputeRequiredNumChannels( + const Texture &texture, const MaterialLibrary &material_library); + + static std::vector FindTextures( + const TextureMap::Type texture_type, + const MaterialLibrary *material_library); +}; + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED +#endif // DRACO_TEXTURE_TEXTURE_UTILS_H_ diff --git a/contrib/draco/src/draco/texture/texture_utils_test.cc b/contrib/draco/src/draco/texture/texture_utils_test.cc new file mode 100644 index 000000000..45873ea7a --- /dev/null +++ b/contrib/draco/src/draco/texture/texture_utils_test.cc @@ -0,0 +1,163 @@ +// Copyright 2019 The Draco Authors. +// +// 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 "draco/texture/texture_utils.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED +#include + +#include "draco/core/draco_test_utils.h" +#include "draco/io/texture_io.h" +#include "draco/texture/color_utils.h" + +namespace { + +TEST(TextureUtilsTest, TestGetTargetNameForTextureLoadedFromFile) { + // Tests that correct target stem and format are returned by texture utils for + // texture loaded from image file (stem and format from source file). + std::unique_ptr texture = + draco::ReadTextureFromFile(draco::GetTestFileFullPath("fast.jpg")) + .value(); + ASSERT_NE(texture, nullptr); + ASSERT_EQ(draco::TextureUtils::GetTargetStem(*texture), "fast"); + ASSERT_EQ(draco::TextureUtils::GetTargetExtension(*texture), "jpg"); + ASSERT_EQ(draco::TextureUtils::GetTargetFormat(*texture), + draco::ImageFormat::JPEG); + ASSERT_EQ(draco::TextureUtils::GetOrGenerateTargetStem(*texture, 5, "_Color"), + "fast"); +} + +TEST(TextureUtilsTest, TestGetTargetNameForNewTexture) { + // Tests that correct target stem and format are returned by texture utils for + // a newly created texture (empty stem and PNG image type by default). + std::unique_ptr texture(new draco::Texture()); + ASSERT_NE(texture, nullptr); + ASSERT_EQ(draco::TextureUtils::GetTargetStem(*texture), ""); + ASSERT_EQ(draco::TextureUtils::GetOrGenerateTargetStem(*texture, 5, "_Color"), + "Texture5_Color"); + ASSERT_EQ(draco::TextureUtils::GetTargetExtension(*texture), "png"); + ASSERT_EQ(draco::TextureUtils::GetTargetFormat(*texture), + draco::ImageFormat::PNG); +} + +TEST(TextureUtilsTest, TestGetSourceFormat) { + // Tests that the source format is determined correctly for new textures and + // for textures loaded from file. + std::unique_ptr new_texture(new draco::Texture()); + DRACO_ASSIGN_OR_ASSERT( + std::unique_ptr png_texture, + draco::ReadTextureFromFile(draco::GetTestFileFullPath("test.png"))); + DRACO_ASSIGN_OR_ASSERT( + std::unique_ptr jpg_texture, + draco::ReadTextureFromFile(draco::GetTestFileFullPath("fast.jpg"))); + + // Check source formats. + ASSERT_EQ(draco::TextureUtils::GetSourceFormat(*new_texture), + draco::ImageFormat::PNG); + ASSERT_EQ(draco::TextureUtils::GetSourceFormat(*png_texture), + draco::ImageFormat::PNG); + ASSERT_EQ(draco::TextureUtils::GetSourceFormat(*jpg_texture), + draco::ImageFormat::JPEG); + + // Remove the mime-type from the jpeg texture and ensure the source format is + // still detected properly based on the filename. + jpg_texture->source_image().set_mime_type(""); + ASSERT_EQ(draco::TextureUtils::GetSourceFormat(*jpg_texture), + draco::ImageFormat::JPEG); +} + +TEST(TextureUtilsTest, TestGetFormat) { + typedef draco::ImageFormat ImageFormat; + ASSERT_EQ(draco::TextureUtils::GetFormat("png"), ImageFormat::PNG); + ASSERT_EQ(draco::TextureUtils::GetFormat("jpg"), ImageFormat::JPEG); + ASSERT_EQ(draco::TextureUtils::GetFormat("jpeg"), ImageFormat::JPEG); + ASSERT_EQ(draco::TextureUtils::GetFormat("basis"), ImageFormat::BASIS); + ASSERT_EQ(draco::TextureUtils::GetFormat("ktx2"), ImageFormat::BASIS); + ASSERT_EQ(draco::TextureUtils::GetFormat("webp"), ImageFormat::WEBP); + ASSERT_EQ(draco::TextureUtils::GetFormat(""), ImageFormat::NONE); + ASSERT_EQ(draco::TextureUtils::GetFormat("bmp"), ImageFormat::NONE); +} + +TEST(TextureUtilsTest, TestGetExtension) { + typedef draco::ImageFormat ImageFormat; + ASSERT_EQ(draco::TextureUtils::GetExtension(ImageFormat::PNG), "png"); + ASSERT_EQ(draco::TextureUtils::GetExtension(ImageFormat::JPEG), "jpg"); + ASSERT_EQ(draco::TextureUtils::GetExtension(ImageFormat::BASIS), "ktx2"); + ASSERT_EQ(draco::TextureUtils::GetExtension(ImageFormat::WEBP), "webp"); + ASSERT_EQ(draco::TextureUtils::GetExtension(ImageFormat::NONE), ""); +} + +TEST(TextureUtilsTest, TestComputeRequiredNumChannels) { + // Tests that the number of texture channels can be computed. Material library + // under test is created programmatically. + + // Load textures. + DRACO_ASSIGN_OR_ASSERT( + auto texture0, draco::ReadTextureFromFile( + draco::GetTestFileFullPath("fully_transparent.png"))); + ASSERT_NE(texture0, nullptr); + draco::Texture *texture0_ptr = texture0.get(); + DRACO_ASSIGN_OR_ASSERT( + auto texture1, + draco::ReadTextureFromFile(draco::GetTestFileFullPath("squares.png"))); + ASSERT_NE(texture1, nullptr); + const draco::Texture *texture1_ptr = texture1.get(); + DRACO_ASSIGN_OR_ASSERT( + auto texture2, draco::ReadTextureFromFile( + draco::GetTestFileFullPath("fully_transparent.png"))); + ASSERT_NE(texture2, nullptr); + const draco::Texture *texture2_ptr = texture2.get(); + + // Compute number of channels for occlusion-only texture. + draco::MaterialLibrary library; + draco::Material *const material0 = library.MutableMaterial(0); + material0->SetTextureMap(std::move(texture0), + draco::TextureMap::AMBIENT_OCCLUSION, 0); + ASSERT_EQ( + draco::TextureUtils::ComputeRequiredNumChannels(*texture0_ptr, library), + 1); + + // Compute number of channels for occlusion-only texture with MR present but + // not using the same texture. + draco::Material *const material1 = library.MutableMaterial(1); + material1->SetTextureMap(std::move(texture1), + draco::TextureMap::METALLIC_ROUGHNESS, 0); + ASSERT_EQ( + draco::TextureUtils::ComputeRequiredNumChannels(*texture0_ptr, library), + 1); + + // Compute number of channels for metallic-roughness texture. + ASSERT_EQ( + draco::TextureUtils::ComputeRequiredNumChannels(*texture1_ptr, library), + 3); + + // Compute number of channels texture that is used for occlusin map in one + // material and also shared with metallic-roughness map in another material. + draco::Material *const material2 = library.MutableMaterial(2); + DRACO_ASSERT_OK(material2->SetTextureMap( + texture0_ptr, draco::TextureMap::METALLIC_ROUGHNESS, 0)); + ASSERT_EQ( + draco::TextureUtils::ComputeRequiredNumChannels(*texture0_ptr, library), + 3); + + // Compute number of channels for non-opaque texture. + material0->SetTextureMap(std::move(texture2), draco::TextureMap::COLOR, 0); + ASSERT_EQ( + draco::TextureUtils::ComputeRequiredNumChannels(*texture2_ptr, library), + 4); +} + +} // namespace + +#endif // DRACO_TRANSCODER_SUPPORTED diff --git a/contrib/draco/src/draco/tools/draco_decoder.cc b/contrib/draco/src/draco/tools/draco_decoder.cc index 610709d62..cf5f18094 100644 --- a/contrib/draco/src/draco/tools/draco_decoder.cc +++ b/contrib/draco/src/draco/tools/draco_decoder.cc @@ -20,6 +20,7 @@ #include "draco/io/obj_encoder.h" #include "draco/io/parser_utils.h" #include "draco/io/ply_encoder.h" +#include "draco/io/stl_encoder.h" namespace { @@ -126,7 +127,6 @@ int main(int argc, char **argv) { } // Save the decoded geometry into a file. - // TODO(fgalligan): Change extension code to look for '.'. const std::string extension = draco::parser::ToLower( options.output.size() >= 4 ? options.output.substr(options.output.size() - 4) @@ -140,7 +140,7 @@ int main(int argc, char **argv) { return -1; } } else { - if (!obj_encoder.EncodeToFile(*pc.get(), options.output)) { + if (!obj_encoder.EncodeToFile(*pc, options.output)) { printf("Failed to store the decoded point cloud as OBJ.\n"); return -1; } @@ -153,13 +153,25 @@ int main(int argc, char **argv) { return -1; } } else { - if (!ply_encoder.EncodeToFile(*pc.get(), options.output)) { + if (!ply_encoder.EncodeToFile(*pc, options.output)) { printf("Failed to store the decoded point cloud as PLY.\n"); return -1; } } + } else if (extension == ".stl") { + draco::StlEncoder stl_encoder; + if (mesh) { + draco::Status s = stl_encoder.EncodeToFile(*mesh, options.output); + if (s.code() != draco::Status::OK) { + printf("Failed to store the decoded mesh as STL.\n"); + return -1; + } + } else { + printf("Can't store a point cloud as STL.\n"); + return -1; + } } else { - printf("Invalid extension of the output file. Use either .ply or .obj.\n"); + printf("Invalid output file extension. Use .obj .ply or .stl.\n"); return -1; } printf("Decoded geometry saved to %s (%" PRId64 " ms to decode)\n", diff --git a/contrib/draco/src/draco/tools/draco_encoder.cc b/contrib/draco/src/draco/tools/draco_encoder.cc index 7e3632d7d..ec639453b 100644 --- a/contrib/draco/src/draco/tools/draco_encoder.cc +++ b/contrib/draco/src/draco/tools/draco_encoder.cc @@ -15,7 +15,9 @@ #include #include +#include "draco/compression/config/compression_shared.h" #include "draco/compression/encode.h" +#include "draco/compression/expert_encode.h" #include "draco/core/cycle_timer.h" #include "draco/io/file_utils.h" #include "draco/io/mesh_io.h" @@ -35,6 +37,7 @@ struct Options { int generic_quantization_bits; bool generic_deleted; int compression_level; + bool preserve_polygons; bool use_metadata; std::string input; std::string output; @@ -50,6 +53,7 @@ Options::Options() generic_quantization_bits(8), generic_deleted(false), compression_level(7), + preserve_polygons(false), use_metadata(false) {} void Usage() { @@ -83,6 +87,11 @@ void Usage() { printf( " --metadata use metadata to encode extra information in " "mesh files.\n"); + // Mesh with polygonal faces loaded from OBJ format is converted to triangular + // mesh and polygon reconstruction information is encoded into a new generic + // attribute. + printf(" -preserve_polygons encode polygon info as an attribute.\n"); + printf( "\nUse negative quantization values to skip the specified attribute\n"); } @@ -138,12 +147,12 @@ void PrintOptions(const draco::PointCloud &pc, const Options &options) { } int EncodePointCloudToFile(const draco::PointCloud &pc, const std::string &file, - draco::Encoder *encoder) { + draco::ExpertEncoder *encoder) { draco::CycleTimer timer; // Encode the geometry. draco::EncoderBuffer buffer; timer.Start(); - const draco::Status status = encoder->EncodePointCloudToBuffer(pc, &buffer); + const draco::Status status = encoder->EncodeToBuffer(&buffer); if (!status.ok()) { printf("Failed to encode the point cloud.\n"); printf("%s\n", status.error_msg()); @@ -162,12 +171,12 @@ int EncodePointCloudToFile(const draco::PointCloud &pc, const std::string &file, } int EncodeMeshToFile(const draco::Mesh &mesh, const std::string &file, - draco::Encoder *encoder) { + draco::ExpertEncoder *encoder) { draco::CycleTimer timer; // Encode the geometry. draco::EncoderBuffer buffer; timer.Start(); - const draco::Status status = encoder->EncodeMeshToBuffer(mesh, &buffer); + const draco::Status status = encoder->EncodeToBuffer(&buffer); if (!status.ok()) { printf("Failed to encode the mesh.\n"); printf("%s\n", status.error_msg()); @@ -249,6 +258,8 @@ int main(int argc, char **argv) { ++i; } else if (!strcmp("--metadata", argv[i])) { options.use_metadata = true; + } else if (!strcmp("-preserve_polygons", argv[i])) { + options.preserve_polygons = true; } } if (argc < 3 || options.input.empty()) { @@ -259,8 +270,10 @@ int main(int argc, char **argv) { std::unique_ptr pc; draco::Mesh *mesh = nullptr; if (!options.is_point_cloud) { - auto maybe_mesh = - draco::ReadMeshFromFile(options.input, options.use_metadata); + draco::Options load_options; + load_options.SetBool("use_metadata", options.use_metadata); + load_options.SetBool("preserve_polygons", options.preserve_polygons); + auto maybe_mesh = draco::ReadMeshFromFile(options.input, load_options); if (!maybe_mesh.ok()) { printf("Failed loading the input mesh: %s.\n", maybe_mesh.status().error_msg()); @@ -350,14 +363,36 @@ int main(int argc, char **argv) { options.output = options.input + ".drc"; } - PrintOptions(*pc.get(), options); + PrintOptions(*pc, options); + + const bool input_is_mesh = mesh && mesh->num_faces() > 0; + + // Convert to ExpertEncoder that allows us to set per-attribute options. + std::unique_ptr expert_encoder; + if (input_is_mesh) { + expert_encoder.reset(new draco::ExpertEncoder(*mesh)); + } else { + expert_encoder.reset(new draco::ExpertEncoder(*pc)); + } + expert_encoder->Reset(encoder.CreateExpertEncoderOptions(*pc)); + + // Check if there is an attribute that stores polygon edges. If so, we disable + // the default prediction scheme for the attribute as it actually makes the + // compression worse. + const int poly_att_id = + pc->GetAttributeIdByMetadataEntry("name", "added_edges"); + if (poly_att_id != -1) { + expert_encoder->SetAttributePredictionScheme( + poly_att_id, draco::PredictionSchemeMethod::PREDICTION_NONE); + } int ret = -1; - const bool input_is_mesh = mesh && mesh->num_faces() > 0; - if (input_is_mesh) - ret = EncodeMeshToFile(*mesh, options.output, &encoder); - else - ret = EncodePointCloudToFile(*pc.get(), options.output, &encoder); + + if (input_is_mesh) { + ret = EncodeMeshToFile(*mesh, options.output, expert_encoder.get()); + } else { + ret = EncodePointCloudToFile(*pc, options.output, expert_encoder.get()); + } if (ret != -1 && options.compression_level < 10) { printf( diff --git a/contrib/draco/src/draco/tools/draco_transcoder.cc b/contrib/draco/src/draco/tools/draco_transcoder.cc new file mode 100644 index 000000000..ad0f27714 --- /dev/null +++ b/contrib/draco/src/draco/tools/draco_transcoder.cc @@ -0,0 +1,130 @@ +// Copyright 2019 The Draco Authors. +// +// 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 "draco/core/cycle_timer.h" +#include "draco/core/status.h" +#include "draco/draco_features.h" +#include "draco/texture/texture_utils.h" +#include "draco/tools/draco_transcoder_lib.h" + +namespace { + +// TODO(fgalligan): Add support for no compression to the transcoder lib. +void Usage() { + // TODO(b/204212351): Revisit using a raw string literal here for readability. + printf("Usage: draco_transcoder [options] -i input -o output\n\n"); + printf("Main options:\n"); + printf(" -h | -? show help.\n"); + printf(" -i input file name.\n"); + printf(" -o output file name.\n"); + printf(" -qp quantization bits for the position attribute, "); + printf("default=11.\n"); + printf(" -qt quantization bits for the texture coordinate "); + printf("attribute, default=10.\n"); + printf(" -qn quantization bits for the normal vector attribute"); + printf(", default=8.\n"); + printf(" -qc quantization bits for the color attribute, "); + printf("default=8.\n"); + printf(" -qtg quantization bits for the tangent attribute, "); + printf("default=8.\n"); + printf(" -qw quantization bits for the weight attribute, "); + printf("default=8.\n"); + printf(" -qg quantization bits for any generic attribute, "); + printf("default=8.\n"); + + printf("\nBoolean options may be negated by prefixing 'no'.\n"); +} + +int StringToInt(const std::string &s) { + char *end; + return strtol(s.c_str(), &end, 10); // NOLINT +} + +bool MatchesBooleanOption(const std::string &option, const std::string &value) { + const std::string opt = "-" + option; + const std::string noopt = "-no" + option; + return value == opt || value == noopt; +} + +draco::Status TranscodeFile( + const draco::DracoTranscoder::FileOptions &file_options, + const draco::DracoTranscodingOptions &transcode_options) { + draco::CycleTimer timer; + timer.Start(); + DRACO_ASSIGN_OR_RETURN(std::unique_ptr dt, + draco::DracoTranscoder::Create(transcode_options)); + + DRACO_RETURN_IF_ERROR(dt->Transcode(file_options)); + timer.Stop(); + printf("Transcode\t%s\t%" PRId64 "\n", file_options.input_filename.c_str(), + timer.GetInMs()); + + return draco::OkStatus(); +} + +} // anonymous namespace + +int main(int argc, char **argv) { + draco::DracoTranscoder::FileOptions file_options; + draco::DracoTranscodingOptions transcode_options; + const int argc_check = argc - 1; + + for (int i = 1; i < argc; ++i) { + if (!strcmp("-h", argv[i]) || !strcmp("-?", argv[i])) { + Usage(); + return 0; + } else if (!strcmp("-i", argv[i]) && i < argc_check) { + file_options.input_filename = argv[++i]; + } else if (!strcmp("-o", argv[i]) && i < argc_check) { + file_options.output_filename = argv[++i]; + } else if (!strcmp("-qp", argv[i]) && i < argc_check) { + transcode_options.geometry.quantization_position.SetQuantizationBits( + StringToInt(argv[++i])); + } else if (!strcmp("-qt", argv[i]) && i < argc_check) { + transcode_options.geometry.quantization_bits_tex_coord = + StringToInt(argv[++i]); + } else if (!strcmp("-qn", argv[i]) && i < argc_check) { + transcode_options.geometry.quantization_bits_normal = + StringToInt(argv[++i]); + } else if (!strcmp("-qc", argv[i]) && i < argc_check) { + transcode_options.geometry.quantization_bits_color = + StringToInt(argv[++i]); + } else if (!strcmp("-qtg", argv[i]) && i < argc_check) { + transcode_options.geometry.quantization_bits_tangent = + StringToInt(argv[++i]); + } else if (!strcmp("-qw", argv[i]) && i < argc_check) { + transcode_options.geometry.quantization_bits_weight = + StringToInt(argv[++i]); + } else if (!strcmp("-qg", argv[i]) && i < argc_check) { + transcode_options.geometry.quantization_bits_generic = + StringToInt(argv[++i]); + } + } + if (argc < 3 || file_options.input_filename.empty() || + file_options.output_filename.empty()) { + Usage(); + return -1; + } + + const draco::Status status = TranscodeFile(file_options, transcode_options); + if (!status.ok()) { + printf("Failed\t%s\t%s\n", file_options.input_filename.c_str(), + status.error_msg()); + return -1; + } + return 0; +} diff --git a/contrib/draco/src/draco/tools/draco_transcoder_lib.cc b/contrib/draco/src/draco/tools/draco_transcoder_lib.cc new file mode 100644 index 000000000..b0bc43843 --- /dev/null +++ b/contrib/draco/src/draco/tools/draco_transcoder_lib.cc @@ -0,0 +1,86 @@ +// Copyright 2021 The Draco Authors. +// +// 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 "draco/tools/draco_transcoder_lib.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED +#include "draco/core/status_or.h" +#include "draco/io/file_utils.h" +#include "draco/io/scene_io.h" +#include "draco/scene/scene_utils.h" +#include "draco/texture/texture_utils.h" + +namespace draco { + +DracoTranscoder::DracoTranscoder() {} + +StatusOr> DracoTranscoder::Create( + const DracoTranscodingOptions &options) { + DRACO_RETURN_IF_ERROR(options.geometry.Check()); + std::unique_ptr dt(new DracoTranscoder()); + dt->transcoding_options_ = options; + return dt; +} + +StatusOr> DracoTranscoder::Create( + const DracoCompressionOptions &options) { + DracoTranscodingOptions new_options; + new_options.geometry = options; + return Create(new_options); +} + +Status DracoTranscoder::Transcode(const FileOptions &file_options) { + DRACO_RETURN_IF_ERROR(ReadScene(file_options)); + DRACO_RETURN_IF_ERROR(CompressScene()); + DRACO_RETURN_IF_ERROR(WriteScene(file_options)); + return OkStatus(); +} + +Status DracoTranscoder::ReadScene(const FileOptions &file_options) { + if (file_options.input_filename.empty()) { + return Status(Status::DRACO_ERROR, "Input filename is empty."); + } else if (file_options.output_filename.empty()) { + return Status(Status::DRACO_ERROR, "Output filename is empty."); + } + DRACO_ASSIGN_OR_RETURN(scene_, + ReadSceneFromFile(file_options.input_filename)); + return OkStatus(); +} + +Status DracoTranscoder::WriteScene(const FileOptions &file_options) { + if (!file_options.output_bin_filename.empty() && + !file_options.output_resource_directory.empty()) { + DRACO_RETURN_IF_ERROR(gltf_encoder_.EncodeFile( + *scene_, file_options.output_filename, file_options.output_bin_filename, + file_options.output_resource_directory)); + } else if (!file_options.output_bin_filename.empty()) { + DRACO_RETURN_IF_ERROR( + gltf_encoder_.EncodeFile(*scene_, file_options.output_filename, + file_options.output_bin_filename)); + } else { + DRACO_RETURN_IF_ERROR( + gltf_encoder_.EncodeFile(*scene_, file_options.output_filename)); + } + return OkStatus(); +} + +Status DracoTranscoder::CompressScene() { + // Apply geometry compression settings to all scene meshes. + SceneUtils::SetDracoCompressionOptions(&transcoding_options_.geometry, + scene_.get()); + return OkStatus(); +} + +} // namespace draco +#endif // DRACO_TRANSCODER_SUPPORTED diff --git a/contrib/draco/src/draco/tools/draco_transcoder_lib.h b/contrib/draco/src/draco/tools/draco_transcoder_lib.h new file mode 100644 index 000000000..c94d19de7 --- /dev/null +++ b/contrib/draco/src/draco/tools/draco_transcoder_lib.h @@ -0,0 +1,103 @@ +// Copyright 2021 The Draco Authors. +// +// 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 DRACO_TOOLS_DRACO_TRANSCODER_LIB_H_ +#define DRACO_TOOLS_DRACO_TRANSCODER_LIB_H_ + +#include "draco/draco_features.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED +#include + +#include "draco/compression/draco_compression_options.h" +#include "draco/core/options.h" +#include "draco/io/gltf_encoder.h" +#include "draco/io/image_compression_options.h" + +namespace draco { + +// Struct to hold Draco transcoding options. +struct DracoTranscodingOptions { + DracoTranscodingOptions() {} + + // Options used when geometry compression optimization is disabled. + DracoCompressionOptions geometry; +}; + +// Class that supports input of glTF (and some simple USD) files, encodes +// them with Draco compression, and outputs glTF Draco compressed files. +// +// glTF supported extensions: +// Input and Output: +// KHR_draco_mesh_compression. http://shortn/_L5tPQqdwWf +// KHR_materials_unlit. http://shortn/_3eaDLoIGam +// KHR_texture_transform. http://shortn/_PORWgVTEe8 +// +// glTF unsupported features: +// Input and Output: +// Morph targets. http://shortn/_zE5DLw8a9B +// Sparse accessors. http://shortn/_h3FwbzQl4f +// KHR_lights_punctual. http://shortn/_nzGk80wKtK +// KHR_materials_pbrSpecularGlossiness. http://shortn/_iz0VC6dIKe +// All vendor extensions. +class DracoTranscoder { + public: + struct FileOptions { + std::string input_filename; // Must be non-empty. + std::string output_filename; // Must be non-empty. + std::string output_bin_filename = ""; + std::string output_resource_directory = ""; + }; + + DracoTranscoder(); + + // Creates a DracoTranscoder object. |options| sets the compression options + // used in the Encode function. + static StatusOr> Create( + const DracoTranscodingOptions &options); + + // Deprecated. + // TODO(fgalligan): Remove when function is not being used anymore. + static StatusOr> Create( + const DracoCompressionOptions &options); + + // Encodes the input with Draco compression using the compression options + // passed in the Create function. The recommended use case is to create a + // transcoder once and call Transcode for multiple files. + Status Transcode(const FileOptions &file_options); + + private: + // Read scene from file. + Status ReadScene(const FileOptions &file_options); + + // Write scene to file. + Status WriteScene(const FileOptions &file_options); + + // Apply compression settings to the scene. + Status CompressScene(); + + private: + GltfEncoder gltf_encoder_; + + // The scene being transcoded. + std::unique_ptr scene_; + + // Copy of the transcoding options passed into the Create function. + DracoTranscodingOptions transcoding_options_; +}; + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED +#endif // DRACO_TOOLS_DRACO_TRANSCODER_LIB_H_ diff --git a/contrib/draco/src/draco/tools/draco_transcoder_lib_test.cc b/contrib/draco/src/draco/tools/draco_transcoder_lib_test.cc new file mode 100644 index 000000000..a87a158e0 --- /dev/null +++ b/contrib/draco/src/draco/tools/draco_transcoder_lib_test.cc @@ -0,0 +1,172 @@ +// Copyright 2021 The Draco Authors. +// +// 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. +// +#ifdef DRACO_TRANSCODER_SUPPORTED + +#include "draco/tools/draco_transcoder_lib.h" + +#include "draco/core/draco_test_base.h" +#include "draco/core/draco_test_utils.h" +#include "draco/io/file_utils.h" + +// Tests encoding a .gltf file with default Draco compression. +TEST(DracoTranscoderTest, DefaultDracoCompression) { + const std::string input_name = "sphere.gltf"; + const std::string input_filename = draco::GetTestFileFullPath(input_name); + const std::string output_filename = + draco::GetTestTempFileFullPath("test.gltf"); + + const draco::DracoTranscodingOptions options; + DRACO_ASSIGN_OR_ASSERT(std::unique_ptr dt, + draco::DracoTranscoder::Create(options)); + + draco::DracoTranscoder::FileOptions file_options; + file_options.input_filename = input_filename; + file_options.output_filename = output_filename; + DRACO_ASSERT_OK(dt->Transcode(file_options)); + + const std::string output_bin_filename = + draco::GetTestTempFileFullPath("test.bin"); + const size_t output_bin_size = draco::GetFileSize(output_bin_filename); + ASSERT_GT(output_bin_size, 0); +} + +// Tests setting the output glTF .bin name. +TEST(DracoTranscoderTest, TestBinName) { + const std::string input_name = "sphere.gltf"; + const std::string input_filename = draco::GetTestFileFullPath(input_name); + const std::string output_filename = + draco::GetTestTempFileFullPath("test.gltf"); + const std::string output_bin_filename = + draco::GetTestTempFileFullPath("different_name.bin"); + + const draco::DracoTranscodingOptions options; + DRACO_ASSIGN_OR_ASSERT(std::unique_ptr dt, + draco::DracoTranscoder::Create(options)); + + draco::DracoTranscoder::FileOptions file_options; + file_options.input_filename = input_filename; + file_options.output_filename = output_filename; + file_options.output_bin_filename = output_bin_filename; + DRACO_ASSERT_OK(dt->Transcode(file_options)); + + const size_t output_bin_size = draco::GetFileSize(output_bin_filename); + ASSERT_GT(output_bin_size, 0); +} + +// Tests setting the output glTF resource directory. +TEST(DracoTranscoderTest, TestResourceDirName) { + const std::string input_name = "sphere.gltf"; + const std::string input_filename = draco::GetTestFileFullPath(input_name); + const std::string output_filename = + draco::GetTestTempFileFullPath("test.gltf"); + const std::string output_bin_filename = + draco::GetTestTempFileFullPath("another_name.bin"); + const std::string output_resource_directory = + draco::GetTestTempFileFullPath("res/other_files"); + + const draco::DracoTranscodingOptions options; + DRACO_ASSIGN_OR_ASSERT(std::unique_ptr dt, + draco::DracoTranscoder::Create(options)); + + draco::DracoTranscoder::FileOptions file_options; + file_options.input_filename = input_filename; + file_options.output_filename = output_filename; + file_options.output_bin_filename = output_bin_filename; + file_options.output_resource_directory = output_resource_directory; + DRACO_ASSERT_OK(dt->Transcode(file_options)); + + const size_t output_bin_size = draco::GetFileSize(output_bin_filename); + ASSERT_GT(output_bin_size, 0); + + const std::string res_dir_png_filename = draco::GetTestTempFileFullPath( + "res/other_files/sphere_Texture0_Normal.png"); + const size_t output_png_size = draco::GetFileSize(res_dir_png_filename); + ASSERT_GT(output_png_size, 0); +} + +// Tests creating one transcoder to encode multiple files. +TEST(DracoTranscoderTest, EncodeMultipleFiles) { + const draco::DracoTranscodingOptions options; + DRACO_ASSIGN_OR_ASSERT(std::unique_ptr dt, + draco::DracoTranscoder::Create(options)); + + draco::DracoTranscoder::FileOptions file_options; + file_options.input_filename = draco::GetTestFileFullPath("sphere.gltf"); + file_options.output_filename = draco::GetTestTempFileFullPath("first.gltf"); + DRACO_ASSERT_OK(dt->Transcode(file_options)); + const size_t first_bin_size = + draco::GetFileSize(draco::GetTestTempFileFullPath("first.bin")); + ASSERT_GT(first_bin_size, 0); + + file_options.input_filename = + draco::GetTestFileFullPath("CesiumMan/glTF/CesiumMan.gltf"); + file_options.output_filename = draco::GetTestTempFileFullPath("second.gltf"); + DRACO_ASSERT_OK(dt->Transcode(file_options)); + const size_t second_bin_size = + draco::GetFileSize(draco::GetTestTempFileFullPath("second.bin")); + ASSERT_GT(second_bin_size, 0); +} + +// Tests using glTF binary as input. +TEST(DracoTranscoderTest, SimpleGlbInput) { + const std::string input_name = "Box/glTF_Binary/Box.glb"; + const std::string input_filename = draco::GetTestFileFullPath(input_name); + const std::string output_filename = + draco::GetTestTempFileFullPath("test.gltf"); + + const draco::DracoTranscodingOptions options; + DRACO_ASSIGN_OR_ASSERT(std::unique_ptr dt, + draco::DracoTranscoder::Create(options)); + + draco::DracoTranscoder::FileOptions file_options; + file_options.input_filename = input_filename; + file_options.output_filename = output_filename; + DRACO_ASSERT_OK(dt->Transcode(file_options)); + + const std::string output_bin_filename = + draco::GetTestTempFileFullPath("test.bin"); + const size_t output_bin_size = draco::GetFileSize(output_bin_filename); + ASSERT_GT(output_bin_size, 0); +} + +// Simple test to check glb input and setting smaller position quantizations +// outputs a smaller file overall. +TEST(DracoTranscoderTest, TestPositionQuantization) { + const std::string input_name = + "KhronosSampleModels/Duck/glTF_Binary/Duck.glb"; + const std::string input_filename = draco::GetTestFileFullPath(input_name); + + draco::DracoTranscodingOptions options; + DRACO_ASSIGN_OR_ASSERT(std::unique_ptr dt, + draco::DracoTranscoder::Create(options)); + + draco::DracoTranscoder::FileOptions file_options; + file_options.input_filename = input_filename; + file_options.output_filename = draco::GetTestTempFileFullPath("first.glb"); + DRACO_ASSERT_OK(dt->Transcode(file_options)); + const size_t first_glb_size = + draco::GetFileSize(draco::GetTestTempFileFullPath("first.glb")); + + options.geometry.quantization_position.SetQuantizationBits(10); + DRACO_ASSIGN_OR_ASSERT(std::unique_ptr dt2, + draco::DracoTranscoder::Create(options)); + file_options.output_filename = draco::GetTestTempFileFullPath("second.glb"); + DRACO_ASSERT_OK(dt2->Transcode(file_options)); + const size_t second_glb_size = + draco::GetFileSize(draco::GetTestTempFileFullPath("second.glb")); + ASSERT_GT(first_glb_size, second_glb_size); +} + +#endif // DRACO_TRANSCODER_SUPPORTED diff --git a/contrib/draco/src/draco/tools/fuzz/build.sh b/contrib/draco/src/draco/tools/fuzz/build.sh index bbeb10591..3e48409fc 100644 --- a/contrib/draco/src/draco/tools/fuzz/build.sh +++ b/contrib/draco/src/draco/tools/fuzz/build.sh @@ -19,7 +19,7 @@ cmake $SRC/draco # The draco_decoder and draco_encoder binaries don't build nicely with OSS-Fuzz # options, so just build the Draco shared libraries. -make -j$(nproc) draco +make -j$(nproc) # build fuzzers for fuzzer in $(find $SRC/draco/src/draco/tools/fuzz -name '*.cc'); do diff --git a/contrib/draco/src/draco/tools/install_test/CMakeLists.txt b/contrib/draco/src/draco/tools/install_test/CMakeLists.txt new file mode 100644 index 000000000..800dc9c9b --- /dev/null +++ b/contrib/draco/src/draco/tools/install_test/CMakeLists.txt @@ -0,0 +1,25 @@ +# Copyright 2022 The Draco Authors +# +# 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. + +cmake_minimum_required(VERSION 3.12) +project(install_test C CXX) + +include(GNUInstallDirs) + +find_package(draco REQUIRED CONFIG) + +add_executable(install_check main.cc) +target_link_libraries(install_check PRIVATE draco::draco) + +install(TARGETS install_check DESTINATION ${CMAKE_INSTALL_BINDIR}) diff --git a/contrib/draco/src/draco/tools/install_test/main.cc b/contrib/draco/src/draco/tools/install_test/main.cc new file mode 100644 index 000000000..e76793b64 --- /dev/null +++ b/contrib/draco/src/draco/tools/install_test/main.cc @@ -0,0 +1,44 @@ +// Copyright 2022 The Draco Authors. +// +// 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. +// + +// This program is used to test the installed version of Draco. It does just +// enough to confirm that an application using Draco can compile and link +// against an installed version of Draco without errors. It does not perform +// any sort of library tests. + +#include +#include + +#include "draco/core/decoder_buffer.h" + +#if defined DRACO_TRANSCODER_SUPPORTED +#include "draco/scene/scene.h" +#include "draco/scene/scene_utils.h" +#endif + +int main(int /*argc*/, char** /*argv*/) { + std::vector empty_buffer; + draco::DecoderBuffer buffer; + buffer.Init(empty_buffer.data(), empty_buffer.size()); + +#if defined DRACO_TRANSCODER_SUPPORTED + draco::Scene empty_scene; + const int num_meshes = empty_scene.NumMeshes(); + (void)num_meshes; +#endif + + printf("Partial sanity test passed.\n"); + return 0; +} diff --git a/contrib/draco/src/draco/tools/install_test/test.py b/contrib/draco/src/draco/tools/install_test/test.py new file mode 100644 index 000000000..8612e70b5 --- /dev/null +++ b/contrib/draco/src/draco/tools/install_test/test.py @@ -0,0 +1,456 @@ +#!/usr/bin/python3 +# +# Copyright 2022 The Draco Authors +# +# 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. +"""Tests installations of the Draco library. + +Builds the library in shared and static configurations on the current host +system, and then confirms that a simple test application can link in both +configurations. +""" + +import argparse +import multiprocessing +import os +import pathlib +import shlex +import shutil +import subprocess +import sys + +# CMake executable. +CMAKE = shutil.which('cmake') + +# List of generators available in the current CMake executable. +CMAKE_AVAILABLE_GENERATORS = [] + +# List of variable defs to be passed through to CMake via its -D argument. +CMAKE_DEFINES = [] + +# CMake builds use the specified generator. +CMAKE_GENERATOR = None + +# Enable the transcoder before running tests (sets DRACO_TRANSCODER_SUPPORTED +# and builds transcoder support dependencies). +ENABLE_TRANSCODER = False + +# The Draco tree that this script uses. +DRACO_SOURCES_PATH = os.path.abspath(os.path.join('..', '..', '..', '..')) + +# Path to this script and the rest of the test project files. +TEST_SOURCES_PATH = os.path.dirname(os.path.abspath(__file__)) + +# The Draco build directories. +DRACO_SHARED_BUILD_PATH = os.path.join(TEST_SOURCES_PATH, '_draco_build_shared') +DRACO_STATIC_BUILD_PATH = os.path.join(TEST_SOURCES_PATH, '_draco_build_static') + +# The Draco install roots. +DRACO_SHARED_INSTALL_PATH = os.path.join(TEST_SOURCES_PATH, + '_draco_install_shared') +DRACO_STATIC_INSTALL_PATH = os.path.join(TEST_SOURCES_PATH, + '_draco_install_static') + +DRACO_SHARED_INSTALL_BIN_PATH = os.path.join(DRACO_SHARED_INSTALL_PATH, 'bin') + +DRACO_SHARED_INSTALL_LIB_PATH = os.path.join(DRACO_SHARED_INSTALL_PATH, 'lib') + +# Argument for -j when using make, or -m when using Visual Studio. Number of +# build jobs. +NUM_PROCESSES = multiprocessing.cpu_count() - 1 + +# The test project build directories. +TEST_SHARED_BUILD_PATH = os.path.join(TEST_SOURCES_PATH, '_test_build_shared') +TEST_STATIC_BUILD_PATH = os.path.join(TEST_SOURCES_PATH, '_test_build_static') + +# The test project install directories. +TEST_SHARED_INSTALL_PATH = os.path.join(TEST_SOURCES_PATH, + '_test_install_shared') +TEST_STATIC_INSTALL_PATH = os.path.join(TEST_SOURCES_PATH, + '_test_install_static') + +# Show configuration and build output. +VERBOSE = False + + +def cmake_get_available_generators(): + """Returns list of generators available in current CMake executable.""" + result = run_process_and_capture_output(f'{CMAKE} --help') + + if result[0] != 0: + raise Exception(f'cmake --help failed, exit code: {result[0]}\n{result[1]}') + + help_text = result[1].splitlines() + generators_start_index = help_text.index('Generators') + 3 + generators_text = help_text[generators_start_index::] + + generators = [] + for gen in generators_text: + gen = gen.split('=')[0].strip().replace('* ', '') + + if gen and gen[0] != '=': + generators.append(gen) + + return generators + + +def cmake_get_generator(): + """Returns the CMake generator from CMakeCache.txt in the current dir.""" + cmake_cache_file_path = os.path.join(os.getcwd(), 'CMakeCache.txt') + cmake_cache_text = '' + with open(cmake_cache_file_path, 'r') as cmake_cache_file: + cmake_cache_text = cmake_cache_file.read() + + if not cmake_cache_text: + raise FileNotFoundError(f'{cmake_cache_file_path} missing or empty.') + + generator = '' + for line in cmake_cache_text.splitlines(): + if line.startswith('CMAKE_GENERATOR:INTERNAL='): + generator = line.split('=')[1] + + return generator + + +def run_process_and_capture_output(cmd, env=None): + """Runs |cmd| as a child process. + + Returns process exit code and output. Streams process output to stdout when + VERBOSE is true. + + Args: + cmd: String containing the command to execute. + env: Optional dict of environment variables. + + Returns: + Tuple of exit code and output. + """ + if not cmd: + raise ValueError('run_process_and_capture_output requires cmd argument.') + + if os.name == 'posix': + # On posix systems subprocess.Popen will treat |cmd| as the program name + # when it is passed as a string. Unconditionally split the command so + # callers don't need to care about this detail. + cmd = shlex.split(cmd) + + proc = subprocess.Popen( + cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env=env) + + if VERBOSE: + print('COMMAND output:') + + stdout = '' + for line in iter(proc.stdout.readline, b''): + decoded_line = line.decode('utf-8') + if VERBOSE: + sys.stdout.write(decoded_line) + sys.stdout.flush() + stdout += decoded_line + + # Wait for the process to exit so that the exit code is available. + proc.wait() + return [proc.returncode, stdout] + + +def create_output_directories(): + """Creates the build output directores for the test.""" + pathlib.Path(DRACO_SHARED_BUILD_PATH).mkdir(parents=True, exist_ok=True) + pathlib.Path(DRACO_STATIC_BUILD_PATH).mkdir(parents=True, exist_ok=True) + pathlib.Path(TEST_SHARED_BUILD_PATH).mkdir(parents=True, exist_ok=True) + pathlib.Path(TEST_STATIC_BUILD_PATH).mkdir(parents=True, exist_ok=True) + + +def cleanup(): + """Removes the build output directories from the test.""" + shutil.rmtree(DRACO_SHARED_BUILD_PATH) + shutil.rmtree(DRACO_STATIC_BUILD_PATH) + shutil.rmtree(DRACO_SHARED_INSTALL_PATH) + shutil.rmtree(DRACO_STATIC_INSTALL_PATH) + shutil.rmtree(TEST_SHARED_BUILD_PATH) + shutil.rmtree(TEST_STATIC_BUILD_PATH) + shutil.rmtree(TEST_SHARED_INSTALL_PATH) + shutil.rmtree(TEST_STATIC_INSTALL_PATH) + + +def cmake_configure(source_path, cmake_args=None): + """Configures a CMake build.""" + command = f'{CMAKE} {source_path}' + + if CMAKE_GENERATOR: + if ' ' in CMAKE_GENERATOR: + command += f' -G "{CMAKE_GENERATOR}"' + else: + command += f' -G {CMAKE_GENERATOR}' + + if cmake_args: + for arg in cmake_args: + command += f' {arg}' + + if CMAKE_DEFINES: + for arg in CMAKE_DEFINES: + command += f' -D{arg}' + + if VERBOSE: + print(f'CONFIGURE command:\n{command}') + + result = run_process_and_capture_output(command) + + if result[0] != 0: + raise Exception(f'CONFIGURE failed!\nexit_code: {result[0]}\n{result[1]}') + + +def cmake_build(cmake_args=None, build_args=None): + """Runs a CMake build.""" + command = f'{CMAKE} --build .' + + if cmake_args: + for arg in cmake_args: + command += f' {arg}' + + if not build_args: + build_args = [] + + generator = cmake_get_generator() + if generator.endswith('Makefiles'): + build_args.append(f'-j {NUM_PROCESSES}') + elif generator.startswith('Visual'): + build_args.append(f'-m:{NUM_PROCESSES}') + + if build_args: + command += ' --' + for arg in build_args: + command += f' {arg}' + + if VERBOSE: + print(f'BUILD command:\n{command}') + + result = run_process_and_capture_output(f'{command}') + + if result[0] != 0: + raise Exception(f'BUILD failed!\nexit_code: {result[0]}\n{result[1]}') + + +def run_install_check(install_path): + """Runs the install_check program.""" + cmd = os.path.join(install_path, 'bin', 'install_check') + if VERBOSE: + print(f'RUN command: {cmd}') + + result = run_process_and_capture_output( + cmd, + # On Windows, add location of draco.dll into PATH env var + {'PATH': DRACO_SHARED_INSTALL_BIN_PATH + os.pathsep + os.environ['PATH']}, + ) + if result[0] != 0: + raise Exception( + f'install_check run failed!\nexit_code: {result[0]}\n{result[1]}') + + +def build_and_install_transcoder_dependencies(): + """Builds and installs Draco dependencies for transcoder enabled builds.""" + orig_dir = os.getcwd() + + # The Eigen CMake build in the release Draco has pinned is, to put it mildly, + # user unfriendly. Instead of wasting time trying to integrate it here, just + # shutil.copytree() everything in $eigen_submodule_path to + # $CMAKE_INSTALL_PREFIX/include/Eigen. + # Eigen claims to be header-only, so this should be adequate for Draco's + # needs here. + eigen_submodule_path = os.path.join( + DRACO_SOURCES_PATH, 'third_party', 'eigen', 'Eigen') + + # "Install" Eigen for the shared install root. + eigen_install_path = os.path.join( + DRACO_SHARED_INSTALL_PATH, 'include', 'Eigen') + shutil.copytree(src=eigen_submodule_path, dst=eigen_install_path) + + # "Install" Eigen for the static install root. + eigen_install_path = os.path.join( + DRACO_STATIC_INSTALL_PATH, 'include', 'Eigen') + shutil.copytree(src=eigen_submodule_path, dst=eigen_install_path) + + # Build and install gulrak/filesystem for shared and static configurations. + # Note that this is basically running gulrak/filesystem's CMake build as an + # install script. + fs_submodule_path = os.path.join( + DRACO_SOURCES_PATH, 'third_party', 'filesystem') + + # Install gulrak/filesystem in the shared draco install root. + fs_shared_build = os.path.join(DRACO_SHARED_BUILD_PATH, '_fs') + pathlib.Path(fs_shared_build).mkdir(parents=True, exist_ok=True) + os.chdir(fs_shared_build) + cmake_args = [] + cmake_args.append(f'-DCMAKE_INSTALL_PREFIX={DRACO_SHARED_INSTALL_PATH}') + cmake_args.append('-DBUILD_SHARED_LIBS=ON') + cmake_args.append('-DGHC_FILESYSTEM_BUILD_TESTING=OFF') + cmake_args.append('-DGHC_FILESYSTEM_BUILD_EXAMPLES=OFF') + cmake_configure(source_path=fs_submodule_path, cmake_args=cmake_args) + cmake_build(cmake_args=['--target install']) + + # Install gulrak/filesystem in the shared draco install root. + fs_static_build = os.path.join(DRACO_STATIC_BUILD_PATH, '_fs') + pathlib.Path(fs_static_build).mkdir(parents=True, exist_ok=True) + os.chdir(fs_static_build) + cmake_args = [] + cmake_args.append(f'-DCMAKE_INSTALL_PREFIX={DRACO_SHARED_INSTALL_PATH}') + cmake_args.append('-DBUILD_SHARED_LIBS=OFF') + cmake_args.append('-DGHC_FILESYSTEM_BUILD_TESTING=OFF') + cmake_args.append('-DGHC_FILESYSTEM_BUILD_EXAMPLES=OFF') + cmake_configure(source_path=fs_submodule_path, cmake_args=cmake_args) + cmake_build(cmake_args=['--target install']) + + # Build and install TinyGLTF for shared and static configurations. + # Note, as above, that this is basically running TinyGLTF's CMake build as an + # install script. + tinygltf_submodule_path = os.path.join( + DRACO_SOURCES_PATH, 'third_party', 'tinygltf') + + # Install TinyGLTF in the shared draco install root. + tinygltf_shared_build = os.path.join(DRACO_SHARED_BUILD_PATH, '_TinyGLTF') + pathlib.Path(tinygltf_shared_build).mkdir(parents=True, exist_ok=True) + os.chdir(tinygltf_shared_build) + cmake_args = [] + cmake_args.append(f'-DCMAKE_INSTALL_PREFIX={DRACO_SHARED_INSTALL_PATH}') + cmake_args.append('-DTINYGLTF_BUILD_EXAMPLES=OFF') + cmake_configure(source_path=tinygltf_submodule_path, cmake_args=cmake_args) + cmake_build(cmake_args=['--target install']) + + # Install TinyGLTF in the static draco install root. + tinygltf_static_build = os.path.join(DRACO_STATIC_BUILD_PATH, '_TinyGLTF') + pathlib.Path(tinygltf_static_build).mkdir(parents=True, exist_ok=True) + os.chdir(tinygltf_static_build) + cmake_args = [] + cmake_args.append(f'-DCMAKE_INSTALL_PREFIX={DRACO_STATIC_INSTALL_PATH}') + cmake_args.append('-DTINYGLTF_BUILD_EXAMPLES=OFF') + cmake_configure(source_path=tinygltf_submodule_path, cmake_args=cmake_args) + cmake_build(cmake_args=['--target install']) + + os.chdir(orig_dir) + + +def build_and_install_draco(): + """Builds Draco in shared and static configurations.""" + orig_dir = os.getcwd() + + if ENABLE_TRANSCODER: + build_and_install_transcoder_dependencies() + + # Build and install Draco in shared library config for the current host + # machine. + os.chdir(DRACO_SHARED_BUILD_PATH) + cmake_args = [] + cmake_args.append(f'-DCMAKE_INSTALL_PREFIX={DRACO_SHARED_INSTALL_PATH}') + cmake_args.append('-DBUILD_SHARED_LIBS=ON') + if ENABLE_TRANSCODER: + cmake_args.append('-DDRACO_TRANSCODER_SUPPORTED=ON') + cmake_configure(source_path=DRACO_SOURCES_PATH, cmake_args=cmake_args) + cmake_build(cmake_args=['--target install']) + + # Build and install Draco in the static config for the current host machine. + os.chdir(DRACO_STATIC_BUILD_PATH) + cmake_args = [] + cmake_args.append(f'-DCMAKE_INSTALL_PREFIX={DRACO_STATIC_INSTALL_PATH}') + cmake_args.append('-DBUILD_SHARED_LIBS=OFF') + if ENABLE_TRANSCODER: + cmake_args.append('-DDRACO_TRANSCODER_SUPPORTED=ON') + cmake_configure(source_path=DRACO_SOURCES_PATH, cmake_args=cmake_args) + cmake_build(cmake_args=['--target install']) + + os.chdir(orig_dir) + + +def build_test_project(): + """Builds the test application in shared and static configurations.""" + orig_dir = os.getcwd() + + # Configure the test project against draco shared and build it. + os.chdir(TEST_SHARED_BUILD_PATH) + cmake_args = [] + cmake_args.append(f'-DCMAKE_INSTALL_PREFIX={TEST_SHARED_INSTALL_PATH}') + cmake_args.append(f'-DCMAKE_PREFIX_PATH={DRACO_SHARED_INSTALL_PATH}') + cmake_args.append(f'-DCMAKE_INSTALL_RPATH={DRACO_SHARED_INSTALL_LIB_PATH}') + cmake_configure(source_path=f'{TEST_SOURCES_PATH}', cmake_args=cmake_args) + cmake_build(cmake_args=['--target install']) + run_install_check(TEST_SHARED_INSTALL_PATH) + + # Configure the test project against draco static and build it. + os.chdir(TEST_STATIC_BUILD_PATH) + cmake_args = [] + cmake_args.append(f'-DCMAKE_INSTALL_PREFIX={TEST_STATIC_INSTALL_PATH}') + cmake_args.append(f'-DCMAKE_PREFIX_PATH={DRACO_STATIC_INSTALL_PATH}') + cmake_configure(source_path=f'{TEST_SOURCES_PATH}', cmake_args=cmake_args) + cmake_build(cmake_args=['--target install']) + run_install_check(TEST_STATIC_INSTALL_PATH) + + os.chdir(orig_dir) + + +def test_draco_install(): + create_output_directories() + build_and_install_draco() + build_test_project() + cleanup() + + +if __name__ == '__main__': + CMAKE_AVAILABLE_GENERATORS = cmake_get_available_generators() + + parser = argparse.ArgumentParser() + parser.add_argument( + '-G', '--generator', help='CMake builds use the specified generator.') + parser.add_argument( + '-D', '--cmake_define', + action='append', + help='Passes argument through to CMake as a CMake variable via cmake -D.') + parser.add_argument( + '-t', '--with_transcoder', + action='store_true', + help='Run tests with Draco transcoder support enabled.') + parser.add_argument( + '-v', + '--verbose', + action='store_true', + help='Show configuration and build output.') + args = parser.parse_args() + + if args.cmake_define: + CMAKE_DEFINES = args.cmake_define + if args.generator: + CMAKE_GENERATOR = args.generator + if args.verbose: + VERBOSE = True + if args.with_transcoder: + ENABLE_TRANSCODER = True + + if VERBOSE: + print(f'CMAKE={CMAKE}') + print(f'CMAKE_DEFINES={CMAKE_DEFINES}') + print(f'CMAKE_GENERATOR={CMAKE_GENERATOR}') + print(f'CMAKE_AVAILABLE_GENERATORS={CMAKE_AVAILABLE_GENERATORS}') + print(f'ENABLE_TRANSCODER={ENABLE_TRANSCODER}') + print(f'DRACO_SOURCES_PATH={DRACO_SOURCES_PATH}') + print(f'DRACO_SHARED_BUILD_PATH={DRACO_SHARED_BUILD_PATH}') + print(f'DRACO_STATIC_BUILD_PATH={DRACO_STATIC_BUILD_PATH}') + print(f'DRACO_SHARED_INSTALL_PATH={DRACO_SHARED_INSTALL_PATH}') + print(f'DRACO_STATIC_INSTALL_PATH={DRACO_STATIC_INSTALL_PATH}') + print(f'NUM_PROCESSES={NUM_PROCESSES}') + print(f'TEST_SHARED_BUILD_PATH={TEST_SHARED_BUILD_PATH}') + print(f'TEST_STATIC_BUILD_PATH={TEST_STATIC_BUILD_PATH}') + print(f'TEST_SOURCES_PATH={TEST_SOURCES_PATH}') + print(f'VERBOSE={VERBOSE}') + + if CMAKE_GENERATOR and CMAKE_GENERATOR not in CMAKE_AVAILABLE_GENERATORS: + raise ValueError(f'CMake generator unavailable: {CMAKE_GENERATOR}.') + + test_draco_install() diff --git a/contrib/stb/stb_image.h b/contrib/stb/stb_image.h index 8d173c66a..5e807a0a6 100644 --- a/contrib/stb/stb_image.h +++ b/contrib/stb/stb_image.h @@ -1,4 +1,4 @@ -/* stb_image - v2.26 - public domain image loader - http://nothings.org/stb +/* stb_image - v2.28 - public domain image loader - http://nothings.org/stb no warranty implied; use at your own risk Do this: @@ -48,6 +48,8 @@ LICENSE RECENT REVISION HISTORY: + 2.28 (2023-01-29) many error fixes, security errors, just tons of stuff + 2.27 (2021-07-11) document stbi_info better, 16-bit PNM support, bug fixes 2.26 (2020-07-13) many minor fixes 2.25 (2020-02-02) fix warnings 2.24 (2020-02-02) fix warnings; thread-local failure_reason and flip_vertically @@ -89,7 +91,7 @@ RECENT REVISION HISTORY: Jeremy Sawicki (handle all ImageNet JPGs) Optimizations & bugfixes Mikhail Morozov (1-bit BMP) Fabian "ryg" Giesen Anael Seghezzi (is-16-bit query) - Arseny Kapoulkine + Arseny Kapoulkine Simon Breuss (16-bit PNM) John-Mark Allen Carmelo J Fdez-Aguera @@ -102,19 +104,21 @@ RECENT REVISION HISTORY: Thomas Ruf Ronny Chevalier github:rlyeh Janez Zemva John Bartholomew Michal Cichon github:romigrou Jonathan Blow Ken Hamada Tero Hanninen github:svdijk - Laurent Gomila Cort Stratton github:snagar + Eugene Golushkov Laurent Gomila Cort Stratton github:snagar Aruelien Pocheville Sergio Gonzalez Thibault Reuille github:Zelex Cass Everitt Ryamond Barbiero github:grim210 Paul Du Bois Engin Manap Aldo Culquicondor github:sammyhw Philipp Wiesemann Dale Weiler Oriol Ferrer Mesia github:phprus - Josh Tobin Matthew Gregan github:poppolopoppo + Josh Tobin Neil Bickford Matthew Gregan github:poppolopoppo Julian Raschke Gregory Mullen Christian Floisand github:darealshinji Baldur Karlsson Kevin Schmidt JR Smith github:Michaelangel007 - Brad Weinberger Matvey Cherevko [reserved] + Brad Weinberger Matvey Cherevko github:mosra Luca Sas Alexander Veselov Zack Middleton [reserved] Ryan C. Gordon [reserved] [reserved] DO NOT ADD YOUR NAME HERE + Jacko Dirks + To add your name to the credits, pick a random blank space in the middle and fill it. 80% of merge conflicts on stb PRs are due to people adding their name at the end of the credits. @@ -137,7 +141,7 @@ RECENT REVISION HISTORY: // // ... x = width, y = height, n = # 8-bit components per pixel ... // // ... replace '0' with '1'..'4' to force that many components per pixel // // ... but 'n' will always be the number that it would have been if you said 0 -// stbi_image_free(data) +// stbi_image_free(data); // // Standard parameters: // int *x -- outputs image width in pixels @@ -176,6 +180,32 @@ RECENT REVISION HISTORY: // // Paletted PNG, BMP, GIF, and PIC images are automatically depalettized. // +// To query the width, height and component count of an image without having to +// decode the full file, you can use the stbi_info family of functions: +// +// int x,y,n,ok; +// ok = stbi_info(filename, &x, &y, &n); +// // returns ok=1 and sets x, y, n if image is a supported format, +// // 0 otherwise. +// +// Note that stb_image pervasively uses ints in its public API for sizes, +// including sizes of memory buffers. This is now part of the API and thus +// hard to change without causing breakage. As a result, the various image +// loaders all have certain limits on image size; these differ somewhat +// by format but generally boil down to either just under 2GB or just under +// 1GB. When the decoded image would be larger than this, stb_image decoding +// will fail. +// +// Additionally, stb_image will reject image files that have any of their +// dimensions set to a larger value than the configurable STBI_MAX_DIMENSIONS, +// which defaults to 2**24 = 16777216 pixels. Due to the above memory limit, +// the only way to have an image with such dimensions load correctly +// is for it to have a rather extreme aspect ratio. Either way, the +// assumption here is that such larger images are likely to be malformed +// or malicious. If you do need to load an image with individual dimensions +// larger than that, and it still fits in the overall size limit, you can +// #define STBI_MAX_DIMENSIONS on your own to be something larger. +// // =========================================================================== // // UNICODE: @@ -281,11 +311,10 @@ RECENT REVISION HISTORY: // // iPhone PNG support: // -// By default we convert iphone-formatted PNGs back to RGB, even though -// they are internally encoded differently. You can disable this conversion -// by calling stbi_convert_iphone_png_to_rgb(0), in which case -// you will always just get the native iphone "format" through (which -// is BGR stored in RGB). +// We optionally support converting iPhone-formatted PNGs (which store +// premultiplied BGRA) back to RGB, even though they're internally encoded +// differently. To enable this conversion, call +// stbi_convert_iphone_png_to_rgb(1). // // Call stbi_set_unpremultiply_on_load(1) as well to force a divide per // pixel to remove any premultiplied alpha *only* if the image file explicitly @@ -489,6 +518,8 @@ STBIDEF void stbi_set_flip_vertically_on_load(int flag_true_if_should_flip); // as above, but only applies to images loaded on the thread that calls the function // this function is only available if your compiler supports thread-local variables; // calling it will fail to link if your compiler doesn't +STBIDEF void stbi_set_unpremultiply_on_load_thread(int flag_true_if_should_unpremultiply); +STBIDEF void stbi_convert_iphone_png_to_rgb_thread(int flag_true_if_should_convert); STBIDEF void stbi_set_flip_vertically_on_load_thread(int flag_true_if_should_flip); // ZLIB client - used by PNG, available for other purposes @@ -605,7 +636,7 @@ STBIDEF int stbi_zlib_decode_noheader_buffer(char *obuffer, int olen, const ch #endif #endif -#ifdef _MSC_VER +#if defined(_MSC_VER) || defined(__SYMBIAN32__) typedef unsigned short stbi__uint16; typedef signed short stbi__int16; typedef unsigned int stbi__uint32; @@ -634,7 +665,7 @@ typedef unsigned char validate_uint32[sizeof(stbi__uint32)==4 ? 1 : -1]; #ifdef STBI_HAS_LROTL #define stbi_lrot(x,y) _lrotl(x,y) #else - #define stbi_lrot(x,y) (((x) << (y)) | ((x) >> (32 - (y)))) + #define stbi_lrot(x,y) (((x) << (y)) | ((x) >> (-(y) & 31))) #endif #if defined(STBI_MALLOC) && defined(STBI_FREE) && (defined(STBI_REALLOC) || defined(STBI_REALLOC_SIZED)) @@ -748,9 +779,12 @@ static int stbi__sse2_available(void) #ifdef STBI_NEON #include -// assume GCC or Clang on ARM targets +#ifdef _MSC_VER +#define STBI_SIMD_ALIGN(type, name) __declspec(align(16)) type name +#else #define STBI_SIMD_ALIGN(type, name) type name __attribute__((aligned(16))) #endif +#endif #ifndef STBI_SIMD_ALIGN #define STBI_SIMD_ALIGN(type, name) type name @@ -924,6 +958,7 @@ static int stbi__gif_info(stbi__context *s, int *x, int *y, int *comp); static int stbi__pnm_test(stbi__context *s); static void *stbi__pnm_load(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri); static int stbi__pnm_info(stbi__context *s, int *x, int *y, int *comp); +static int stbi__pnm_is16(stbi__context *s); #endif static @@ -998,7 +1033,7 @@ static int stbi__mad3sizes_valid(int a, int b, int c, int add) } // returns 1 if "a*b*c*d + add" has no negative terms/factors and doesn't overflow -#if !defined(STBI_NO_LINEAR) || !defined(STBI_NO_HDR) +#if !defined(STBI_NO_LINEAR) || !defined(STBI_NO_HDR) || !defined(STBI_NO_PNM) static int stbi__mad4sizes_valid(int a, int b, int c, int d, int add) { return stbi__mul2sizes_valid(a, b) && stbi__mul2sizes_valid(a*b, c) && @@ -1021,7 +1056,7 @@ static void *stbi__malloc_mad3(int a, int b, int c, int add) return stbi__malloc(a*b*c + add); } -#if !defined(STBI_NO_LINEAR) || !defined(STBI_NO_HDR) +#if !defined(STBI_NO_LINEAR) || !defined(STBI_NO_HDR) || !defined(STBI_NO_PNM) static void *stbi__malloc_mad4(int a, int b, int c, int d, int add) { if (!stbi__mad4sizes_valid(a, b, c, d, add)) return NULL; @@ -1029,6 +1064,23 @@ static void *stbi__malloc_mad4(int a, int b, int c, int d, int add) } #endif +// returns 1 if the sum of two signed ints is valid (between -2^31 and 2^31-1 inclusive), 0 on overflow. +static int stbi__addints_valid(int a, int b) +{ + if ((a >= 0) != (b >= 0)) return 1; // a and b have different signs, so no overflow + if (a < 0 && b < 0) return a >= INT_MIN - b; // same as a + b >= INT_MIN; INT_MIN - b cannot overflow since b < 0. + return a <= INT_MAX - b; +} + +// returns 1 if the product of two signed shorts is valid, 0 on overflow. +static int stbi__mul2shorts_valid(short a, short b) +{ + if (b == 0 || b == -1) return 1; // multiplication by 0 is always 0; check for -1 so SHRT_MIN/b doesn't overflow + if ((a >= 0) == (b >= 0)) return a <= SHRT_MAX/b; // product is positive, so similar to mul2sizes_valid + if (b < 0) return a <= SHRT_MIN / b; // same as a * b >= SHRT_MIN + return a >= SHRT_MIN / b; +} + // stbi__err - error // stbi__errpf - error returning pointer to float // stbi__errpuc - error returning pointer to unsigned char @@ -1087,9 +1139,8 @@ static void *stbi__load_main(stbi__context *s, int *x, int *y, int *comp, int re ri->channel_order = STBI_ORDER_RGB; // all current input & output are this, but this is here so we can add BGR order ri->num_channels = 0; - #ifndef STBI_NO_JPEG - if (stbi__jpeg_test(s)) return stbi__jpeg_load(s,x,y,comp,req_comp, ri); - #endif + // test the formats with a very explicit header first (at least a FOURCC + // or distinctive magic number first) #ifndef STBI_NO_PNG if (stbi__png_test(s)) return stbi__png_load(s,x,y,comp,req_comp, ri); #endif @@ -1107,6 +1158,13 @@ static void *stbi__load_main(stbi__context *s, int *x, int *y, int *comp, int re #ifndef STBI_NO_PIC if (stbi__pic_test(s)) return stbi__pic_load(s,x,y,comp,req_comp, ri); #endif + + // then the formats that can end up attempting to load with just 1 or 2 + // bytes matching expectations; these are prone to false positives, so + // try them later + #ifndef STBI_NO_JPEG + if (stbi__jpeg_test(s)) return stbi__jpeg_load(s,x,y,comp,req_comp, ri); + #endif #ifndef STBI_NO_PNM if (stbi__pnm_test(s)) return stbi__pnm_load(s,x,y,comp,req_comp, ri); #endif @@ -1262,12 +1320,12 @@ static void stbi__float_postprocess(float *result, int *x, int *y, int *comp, in #ifndef STBI_NO_STDIO -#if defined(_MSC_VER) && defined(STBI_WINDOWS_UTF8) +#if defined(_WIN32) && defined(STBI_WINDOWS_UTF8) STBI_EXTERN __declspec(dllimport) int __stdcall MultiByteToWideChar(unsigned int cp, unsigned long flags, const char *str, int cbmb, wchar_t *widestr, int cchwide); STBI_EXTERN __declspec(dllimport) int __stdcall WideCharToMultiByte(unsigned int cp, unsigned long flags, const wchar_t *widestr, int cchwide, char *str, int cbmb, const char *defchar, int *used_default); #endif -#if defined(_MSC_VER) && defined(STBI_WINDOWS_UTF8) +#if defined(_WIN32) && defined(STBI_WINDOWS_UTF8) STBIDEF int stbi_convert_wchar_to_utf8(char *buffer, size_t bufferlen, const wchar_t* input) { return WideCharToMultiByte(65001 /* UTF8 */, 0, input, -1, buffer, (int) bufferlen, NULL, NULL); @@ -1277,16 +1335,16 @@ STBIDEF int stbi_convert_wchar_to_utf8(char *buffer, size_t bufferlen, const wch static FILE *stbi__fopen(char const *filename, char const *mode) { FILE *f; -#if defined(_MSC_VER) && defined(STBI_WINDOWS_UTF8) +#if defined(_WIN32) && defined(STBI_WINDOWS_UTF8) wchar_t wMode[64]; wchar_t wFilename[1024]; - if (0 == MultiByteToWideChar(65001 /* UTF8 */, 0, filename, -1, wFilename, sizeof(wFilename))) + if (0 == MultiByteToWideChar(65001 /* UTF8 */, 0, filename, -1, wFilename, sizeof(wFilename)/sizeof(*wFilename))) return 0; - if (0 == MultiByteToWideChar(65001 /* UTF8 */, 0, mode, -1, wMode, sizeof(wMode))) + if (0 == MultiByteToWideChar(65001 /* UTF8 */, 0, mode, -1, wMode, sizeof(wMode)/sizeof(*wMode))) return 0; -#if _MSC_VER >= 1400 +#if defined(_MSC_VER) && _MSC_VER >= 1400 if (0 != _wfopen_s(&f, wFilename, wMode)) f = 0; #else @@ -1662,7 +1720,8 @@ static int stbi__get16le(stbi__context *s) static stbi__uint32 stbi__get32le(stbi__context *s) { stbi__uint32 z = stbi__get16le(s); - return z + (stbi__get16le(s) << 16); + z += (stbi__uint32)stbi__get16le(s) << 16; + return z; } #endif @@ -1944,9 +2003,12 @@ static int stbi__build_huffman(stbi__huffman *h, int *count) int i,j,k=0; unsigned int code; // build size list for each symbol (from JPEG spec) - for (i=0; i < 16; ++i) - for (j=0; j < count[i]; ++j) + for (i=0; i < 16; ++i) { + for (j=0; j < count[i]; ++j) { h->size[k++] = (stbi_uc) (i+1); + if(k >= 257) return stbi__err("bad size list","Corrupt JPEG"); + } + } h->size[k] = 0; // compute actual symbols (from jpeg spec) @@ -2071,6 +2133,8 @@ stbi_inline static int stbi__jpeg_huff_decode(stbi__jpeg *j, stbi__huffman *h) // convert the huffman code to the symbol id c = ((j->code_buffer >> (32 - k)) & stbi__bmask[k]) + h->delta[k]; + if(c < 0 || c >= 256) // symbol id out of bounds! + return -1; STBI_ASSERT((((j->code_buffer) >> (32 - h->size[c])) & stbi__bmask[h->size[c]]) == h->code[c]); // convert the id to a symbol @@ -2089,14 +2153,14 @@ stbi_inline static int stbi__extend_receive(stbi__jpeg *j, int n) unsigned int k; int sgn; if (j->code_bits < n) stbi__grow_buffer_unsafe(j); + if (j->code_bits < n) return 0; // ran out of bits from stream, return 0s intead of continuing - sgn = (stbi__int32)j->code_buffer >> 31; // sign bit is always in MSB + sgn = j->code_buffer >> 31; // sign bit always in MSB; 0 if MSB clear (positive), 1 if MSB set (negative) k = stbi_lrot(j->code_buffer, n); - if (n < 0 || n >= (int) (sizeof(stbi__bmask)/sizeof(*stbi__bmask))) return 0; j->code_buffer = k & ~stbi__bmask[n]; k &= stbi__bmask[n]; j->code_bits -= n; - return k + (stbi__jbias[n] & ~sgn); + return k + (stbi__jbias[n] & (sgn - 1)); } // get some unsigned bits @@ -2104,6 +2168,7 @@ stbi_inline static int stbi__jpeg_get_bits(stbi__jpeg *j, int n) { unsigned int k; if (j->code_bits < n) stbi__grow_buffer_unsafe(j); + if (j->code_bits < n) return 0; // ran out of bits from stream, return 0s intead of continuing k = stbi_lrot(j->code_buffer, n); j->code_buffer = k & ~stbi__bmask[n]; k &= stbi__bmask[n]; @@ -2115,6 +2180,7 @@ stbi_inline static int stbi__jpeg_get_bit(stbi__jpeg *j) { unsigned int k; if (j->code_bits < 1) stbi__grow_buffer_unsafe(j); + if (j->code_bits < 1) return 0; // ran out of bits from stream, return 0s intead of continuing k = j->code_buffer; j->code_buffer <<= 1; --j->code_bits; @@ -2146,14 +2212,16 @@ static int stbi__jpeg_decode_block(stbi__jpeg *j, short data[64], stbi__huffman if (j->code_bits < 16) stbi__grow_buffer_unsafe(j); t = stbi__jpeg_huff_decode(j, hdc); - if (t < 0) return stbi__err("bad huffman code","Corrupt JPEG"); + if (t < 0 || t > 15) return stbi__err("bad huffman code","Corrupt JPEG"); // 0 all the ac values now so we can do it 32-bits at a time memset(data,0,64*sizeof(data[0])); diff = t ? stbi__extend_receive(j, t) : 0; + if (!stbi__addints_valid(j->img_comp[b].dc_pred, diff)) return stbi__err("bad delta","Corrupt JPEG"); dc = j->img_comp[b].dc_pred + diff; j->img_comp[b].dc_pred = dc; + if (!stbi__mul2shorts_valid(dc, dequant[0])) return stbi__err("can't merge dc and ac", "Corrupt JPEG"); data[0] = (short) (dc * dequant[0]); // decode AC components, see JPEG spec @@ -2167,6 +2235,7 @@ static int stbi__jpeg_decode_block(stbi__jpeg *j, short data[64], stbi__huffman if (r) { // fast-AC path k += (r >> 4) & 15; // run s = r & 15; // combined length + if (s > j->code_bits) return stbi__err("bad huffman code", "Combined length longer than code bits available"); j->code_buffer <<= s; j->code_bits -= s; // decode into unzigzag'd location @@ -2203,12 +2272,14 @@ static int stbi__jpeg_decode_block_prog_dc(stbi__jpeg *j, short data[64], stbi__ // first scan for DC coefficient, must be first memset(data,0,64*sizeof(data[0])); // 0 all the ac values now t = stbi__jpeg_huff_decode(j, hdc); - if (t == -1) return stbi__err("can't merge dc and ac", "Corrupt JPEG"); + if (t < 0 || t > 15) return stbi__err("can't merge dc and ac", "Corrupt JPEG"); diff = t ? stbi__extend_receive(j, t) : 0; + if (!stbi__addints_valid(j->img_comp[b].dc_pred, diff)) return stbi__err("bad delta", "Corrupt JPEG"); dc = j->img_comp[b].dc_pred + diff; j->img_comp[b].dc_pred = dc; - data[0] = (short) (dc << j->succ_low); + if (!stbi__mul2shorts_valid(dc, 1 << j->succ_low)) return stbi__err("can't merge dc and ac", "Corrupt JPEG"); + data[0] = (short) (dc * (1 << j->succ_low)); } else { // refinement scan for DC coefficient if (stbi__jpeg_get_bit(j)) @@ -2242,10 +2313,11 @@ static int stbi__jpeg_decode_block_prog_ac(stbi__jpeg *j, short data[64], stbi__ if (r) { // fast-AC path k += (r >> 4) & 15; // run s = r & 15; // combined length + if (s > j->code_bits) return stbi__err("bad huffman code", "Combined length longer than code bits available"); j->code_buffer <<= s; j->code_bits -= s; zig = stbi__jpeg_dezigzag[k++]; - data[zig] = (short) ((r >> 8) << shift); + data[zig] = (short) ((r >> 8) * (1 << shift)); } else { int rs = stbi__jpeg_huff_decode(j, hac); if (rs < 0) return stbi__err("bad huffman code","Corrupt JPEG"); @@ -2263,7 +2335,7 @@ static int stbi__jpeg_decode_block_prog_ac(stbi__jpeg *j, short data[64], stbi__ } else { k += r; zig = stbi__jpeg_dezigzag[k++]; - data[zig] = (short) (stbi__extend_receive(j,s) << shift); + data[zig] = (short) (stbi__extend_receive(j,s) * (1 << shift)); } } } while (k <= j->spec_end); @@ -3062,6 +3134,7 @@ static int stbi__process_marker(stbi__jpeg *z, int m) sizes[i] = stbi__get8(z->s); n += sizes[i]; } + if(n > 256) return stbi__err("bad DHT header","Corrupt JPEG"); // Loop over i < n would write past end of values! L -= 17; if (tc == 0) { if (!stbi__build_huffman(z->huff_dc+th, sizes)) return 0; @@ -3227,6 +3300,13 @@ static int stbi__process_frame_header(stbi__jpeg *z, int scan) if (z->img_comp[i].v > v_max) v_max = z->img_comp[i].v; } + // check that plane subsampling factors are integer ratios; our resamplers can't deal with fractional ratios + // and I've never seen a non-corrupted JPEG file actually use them + for (i=0; i < s->img_n; ++i) { + if (h_max % z->img_comp[i].h != 0) return stbi__err("bad H","Corrupt JPEG"); + if (v_max % z->img_comp[i].v != 0) return stbi__err("bad V","Corrupt JPEG"); + } + // compute interleaved mcu info z->img_h_max = h_max; z->img_v_max = v_max; @@ -3304,6 +3384,28 @@ static int stbi__decode_jpeg_header(stbi__jpeg *z, int scan) return 1; } +static int stbi__skip_jpeg_junk_at_end(stbi__jpeg *j) +{ + // some JPEGs have junk at end, skip over it but if we find what looks + // like a valid marker, resume there + while (!stbi__at_eof(j->s)) { + int x = stbi__get8(j->s); + while (x == 255) { // might be a marker + if (stbi__at_eof(j->s)) return STBI__MARKER_none; + x = stbi__get8(j->s); + if (x != 0x00 && x != 0xff) { + // not a stuffed zero or lead-in to another marker, looks + // like an actual marker, return it + return x; + } + // stuffed zero has x=0 now which ends the loop, meaning we go + // back to regular scan loop. + // repeated 0xff keeps trying to read the next byte of the marker. + } + } + return STBI__MARKER_none; +} + // decode image to YCbCr format static int stbi__decode_jpeg_image(stbi__jpeg *j) { @@ -3320,25 +3422,22 @@ static int stbi__decode_jpeg_image(stbi__jpeg *j) if (!stbi__process_scan_header(j)) return 0; if (!stbi__parse_entropy_coded_data(j)) return 0; if (j->marker == STBI__MARKER_none ) { - // handle 0s at the end of image data from IP Kamera 9060 - while (!stbi__at_eof(j->s)) { - int x = stbi__get8(j->s); - if (x == 255) { - j->marker = stbi__get8(j->s); - break; - } - } + j->marker = stbi__skip_jpeg_junk_at_end(j); // if we reach eof without hitting a marker, stbi__get_marker() below will fail and we'll eventually return 0 } + m = stbi__get_marker(j); + if (STBI__RESTART(m)) + m = stbi__get_marker(j); } else if (stbi__DNL(m)) { int Ld = stbi__get16be(j->s); stbi__uint32 NL = stbi__get16be(j->s); if (Ld != 4) return stbi__err("bad DNL len", "Corrupt JPEG"); if (NL != j->s->img_y) return stbi__err("bad DNL height", "Corrupt JPEG"); + m = stbi__get_marker(j); } else { - if (!stbi__process_marker(j, m)) return 0; + if (!stbi__process_marker(j, m)) return 1; + m = stbi__get_marker(j); } - m = stbi__get_marker(j); } if (j->progressive) stbi__jpeg_finish(j); @@ -3782,6 +3881,10 @@ static stbi_uc *load_jpeg_image(stbi__jpeg *z, int *out_x, int *out_y, int *comp else decode_n = z->s->img_n; + // nothing to do if no components requested; check this now to avoid + // accessing uninitialized coutput[0] later + if (decode_n <= 0) { stbi__cleanup_jpeg(z); return NULL; } + // resample and color-convert { int k; @@ -3924,6 +4027,8 @@ static void *stbi__jpeg_load(stbi__context *s, int *x, int *y, int *comp, int re { unsigned char* result; stbi__jpeg* j = (stbi__jpeg*) stbi__malloc(sizeof(stbi__jpeg)); + if (!j) return stbi__errpuc("outofmem", "Out of memory"); + memset(j, 0, sizeof(stbi__jpeg)); STBI_NOTUSED(ri); j->s = s; stbi__setup_jpeg(j); @@ -3936,6 +4041,8 @@ static int stbi__jpeg_test(stbi__context *s) { int r; stbi__jpeg* j = (stbi__jpeg*)stbi__malloc(sizeof(stbi__jpeg)); + if (!j) return stbi__err("outofmem", "Out of memory"); + memset(j, 0, sizeof(stbi__jpeg)); j->s = s; stbi__setup_jpeg(j); r = stbi__decode_jpeg_header(j, STBI__SCAN_type); @@ -3960,6 +4067,8 @@ static int stbi__jpeg_info(stbi__context *s, int *x, int *y, int *comp) { int result; stbi__jpeg* j = (stbi__jpeg*) (stbi__malloc(sizeof(stbi__jpeg))); + if (!j) return stbi__err("outofmem", "Out of memory"); + memset(j, 0, sizeof(stbi__jpeg)); j->s = s; result = stbi__jpeg_info_raw(j, x, y, comp); STBI_FREE(j); @@ -3979,6 +4088,7 @@ static int stbi__jpeg_info(stbi__context *s, int *x, int *y, int *comp) // fast-way is faster to check than jpeg huffman, but slow way is slower #define STBI__ZFAST_BITS 9 // accelerate all cases in default tables #define STBI__ZFAST_MASK ((1 << STBI__ZFAST_BITS) - 1) +#define STBI__ZNSYMS 288 // number of symbols in literal/length alphabet // zlib-style huffman encoding // (jpegs packs from left, zlib from right, so can't share code) @@ -3988,8 +4098,8 @@ typedef struct stbi__uint16 firstcode[16]; int maxcode[17]; stbi__uint16 firstsymbol[16]; - stbi_uc size[288]; - stbi__uint16 value[288]; + stbi_uc size[STBI__ZNSYMS]; + stbi__uint16 value[STBI__ZNSYMS]; } stbi__zhuffman; stbi_inline static int stbi__bitreverse16(int n) @@ -4120,7 +4230,7 @@ static int stbi__zhuffman_decode_slowpath(stbi__zbuf *a, stbi__zhuffman *z) if (s >= 16) return -1; // invalid code! // code size is s, so: b = (k >> (16-s)) - z->firstcode[s] + z->firstsymbol[s]; - if ((unsigned int)b >= sizeof (z->size)) return -1; // some data was corrupt somewhere! + if (b >= STBI__ZNSYMS) return -1; // some data was corrupt somewhere! if (z->size[b] != s) return -1; // was originally an assert, but report failure instead. a->code_buffer >>= s; a->num_bits -= s; @@ -4201,11 +4311,12 @@ static int stbi__parse_huffman_block(stbi__zbuf *a) a->zout = zout; return 1; } + if (z >= 286) return stbi__err("bad huffman code","Corrupt PNG"); // per DEFLATE, length codes 286 and 287 must not appear in compressed data z -= 257; len = stbi__zlength_base[z]; if (stbi__zlength_extra[z]) len += stbi__zreceive(a, stbi__zlength_extra[z]); z = stbi__zhuffman_decode(a, &a->z_distance); - if (z < 0) return stbi__err("bad huffman code","Corrupt PNG"); + if (z < 0 || z >= 30) return stbi__err("bad huffman code","Corrupt PNG"); // per DEFLATE, distance codes 30 and 31 must not appear in compressed data dist = stbi__zdist_base[z]; if (stbi__zdist_extra[z]) dist += stbi__zreceive(a, stbi__zdist_extra[z]); if (zout - a->zout_start < dist) return stbi__err("bad dist","Corrupt PNG"); @@ -4317,7 +4428,7 @@ static int stbi__parse_zlib_header(stbi__zbuf *a) return 1; } -static const stbi_uc stbi__zdefault_length[288] = +static const stbi_uc stbi__zdefault_length[STBI__ZNSYMS] = { 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8, 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8, 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8, 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8, @@ -4363,7 +4474,7 @@ static int stbi__parse_zlib(stbi__zbuf *a, int parse_header) } else { if (type == 1) { // use fixed code lengths - if (!stbi__zbuild_huffman(&a->z_length , stbi__zdefault_length , 288)) return 0; + if (!stbi__zbuild_huffman(&a->z_length , stbi__zdefault_length , STBI__ZNSYMS)) return 0; if (!stbi__zbuild_huffman(&a->z_distance, stbi__zdefault_distance, 32)) return 0; } else { if (!stbi__compute_huffman_codes(a)) return 0; @@ -4759,6 +4870,7 @@ static int stbi__create_png_image(stbi__png *a, stbi_uc *image_data, stbi__uint3 // de-interlacing final = (stbi_uc *) stbi__malloc_mad3(a->s->img_x, a->s->img_y, out_bytes, 0); + if (!final) return stbi__err("outofmem", "Out of memory"); for (p=0; p < 7; ++p) { int xorig[] = { 0,4,0,2,0,1,0 }; int yorig[] = { 0,0,4,0,2,0,1 }; @@ -4879,19 +4991,46 @@ static int stbi__expand_png_palette(stbi__png *a, stbi_uc *palette, int len, int return 1; } -static int stbi__unpremultiply_on_load = 0; -static int stbi__de_iphone_flag = 0; +static int stbi__unpremultiply_on_load_global = 0; +static int stbi__de_iphone_flag_global = 0; STBIDEF void stbi_set_unpremultiply_on_load(int flag_true_if_should_unpremultiply) { - stbi__unpremultiply_on_load = flag_true_if_should_unpremultiply; + stbi__unpremultiply_on_load_global = flag_true_if_should_unpremultiply; } STBIDEF void stbi_convert_iphone_png_to_rgb(int flag_true_if_should_convert) { - stbi__de_iphone_flag = flag_true_if_should_convert; + stbi__de_iphone_flag_global = flag_true_if_should_convert; } +#ifndef STBI_THREAD_LOCAL +#define stbi__unpremultiply_on_load stbi__unpremultiply_on_load_global +#define stbi__de_iphone_flag stbi__de_iphone_flag_global +#else +static STBI_THREAD_LOCAL int stbi__unpremultiply_on_load_local, stbi__unpremultiply_on_load_set; +static STBI_THREAD_LOCAL int stbi__de_iphone_flag_local, stbi__de_iphone_flag_set; + +STBIDEF void stbi_set_unpremultiply_on_load_thread(int flag_true_if_should_unpremultiply) +{ + stbi__unpremultiply_on_load_local = flag_true_if_should_unpremultiply; + stbi__unpremultiply_on_load_set = 1; +} + +STBIDEF void stbi_convert_iphone_png_to_rgb_thread(int flag_true_if_should_convert) +{ + stbi__de_iphone_flag_local = flag_true_if_should_convert; + stbi__de_iphone_flag_set = 1; +} + +#define stbi__unpremultiply_on_load (stbi__unpremultiply_on_load_set \ + ? stbi__unpremultiply_on_load_local \ + : stbi__unpremultiply_on_load_global) +#define stbi__de_iphone_flag (stbi__de_iphone_flag_set \ + ? stbi__de_iphone_flag_local \ + : stbi__de_iphone_flag_global) +#endif // STBI_THREAD_LOCAL + static void stbi__de_iphone(stbi__png *z) { stbi__context *s = z->s; @@ -4941,7 +5080,7 @@ static int stbi__parse_png_file(stbi__png *z, int scan, int req_comp) { stbi_uc palette[1024], pal_img_n=0; stbi_uc has_trans=0, tc[3]={0}; - stbi__uint16 tc16[3]={0}; + stbi__uint16 tc16[3]; stbi__uint32 ioff=0, idata_limit=0, i, pal_len=0; int first=1,k,interlace=0, color=0, is_iphone=0; stbi__context *s = z->s; @@ -4981,14 +5120,13 @@ static int stbi__parse_png_file(stbi__png *z, int scan, int req_comp) if (!pal_img_n) { s->img_n = (color & 2 ? 3 : 1) + (color & 4 ? 1 : 0); if ((1 << 30) / s->img_x / s->img_n < s->img_y) return stbi__err("too large", "Image too large to decode"); - if (scan == STBI__SCAN_header) return 1; } else { // if paletted, then pal_n is our final components, and // img_n is # components to decompress/filter. s->img_n = 1; if ((1 << 30) / s->img_x / 4 < s->img_y) return stbi__err("too large","Corrupt PNG"); - // if SCAN_header, have to scan to see if we have a tRNS } + // even with SCAN_header, have to scan to see if we have a tRNS break; } @@ -5020,6 +5158,8 @@ static int stbi__parse_png_file(stbi__png *z, int scan, int req_comp) if (!(s->img_n & 1)) return stbi__err("tRNS with alpha","Corrupt PNG"); if (c.length != (stbi__uint32) s->img_n*2) return stbi__err("bad tRNS len","Corrupt PNG"); has_trans = 1; + // non-paletted with tRNS = constant alpha. if header-scanning, we can stop now. + if (scan == STBI__SCAN_header) { ++s->img_n; return 1; } if (z->depth == 16) { for (k = 0; k < s->img_n; ++k) tc16[k] = (stbi__uint16)stbi__get16be(s); // copy the values as-is } else { @@ -5032,7 +5172,13 @@ static int stbi__parse_png_file(stbi__png *z, int scan, int req_comp) case STBI__PNG_TYPE('I','D','A','T'): { if (first) return stbi__err("first not IHDR", "Corrupt PNG"); if (pal_img_n && !pal_len) return stbi__err("no PLTE","Corrupt PNG"); - if (scan == STBI__SCAN_header) { s->img_n = pal_img_n; return 1; } + if (scan == STBI__SCAN_header) { + // header scan definitely stops at first IDAT + if (pal_img_n) + s->img_n = pal_img_n; + return 1; + } + if (c.length > (1u << 30)) return stbi__err("IDAT size limit", "IDAT section larger than 2^30 bytes"); if ((int)(ioff + c.length) < (int)ioff) return 0; if (ioff + c.length > idata_limit) { stbi__uint32 idata_limit_old = idata_limit; @@ -5272,6 +5418,32 @@ typedef struct int extra_read; } stbi__bmp_data; +static int stbi__bmp_set_mask_defaults(stbi__bmp_data *info, int compress) +{ + // BI_BITFIELDS specifies masks explicitly, don't override + if (compress == 3) + return 1; + + if (compress == 0) { + if (info->bpp == 16) { + info->mr = 31u << 10; + info->mg = 31u << 5; + info->mb = 31u << 0; + } else if (info->bpp == 32) { + info->mr = 0xffu << 16; + info->mg = 0xffu << 8; + info->mb = 0xffu << 0; + info->ma = 0xffu << 24; + info->all_a = 0; // if all_a is 0 at end, then we loaded alpha channel but it was all 0 + } else { + // otherwise, use defaults, which is all-0 + info->mr = info->mg = info->mb = info->ma = 0; + } + return 1; + } + return 0; // error +} + static void *stbi__bmp_parse_header(stbi__context *s, stbi__bmp_data *info) { int hsz; @@ -5299,6 +5471,8 @@ static void *stbi__bmp_parse_header(stbi__context *s, stbi__bmp_data *info) if (hsz != 12) { int compress = stbi__get32le(s); if (compress == 1 || compress == 2) return stbi__errpuc("BMP RLE", "BMP type not supported: RLE"); + if (compress >= 4) return stbi__errpuc("BMP JPEG/PNG", "BMP type not supported: unsupported compression"); // this includes PNG/JPEG modes + if (compress == 3 && info->bpp != 16 && info->bpp != 32) return stbi__errpuc("bad BMP", "bad BMP"); // bitfields requires 16 or 32 bits/pixel stbi__get32le(s); // discard sizeof stbi__get32le(s); // discard hres stbi__get32le(s); // discard vres @@ -5313,17 +5487,7 @@ static void *stbi__bmp_parse_header(stbi__context *s, stbi__bmp_data *info) } if (info->bpp == 16 || info->bpp == 32) { if (compress == 0) { - if (info->bpp == 32) { - info->mr = 0xffu << 16; - info->mg = 0xffu << 8; - info->mb = 0xffu << 0; - info->ma = 0xffu << 24; - info->all_a = 0; // if all_a is 0 at end, then we loaded alpha channel but it was all 0 - } else { - info->mr = 31u << 10; - info->mg = 31u << 5; - info->mb = 31u << 0; - } + stbi__bmp_set_mask_defaults(info, compress); } else if (compress == 3) { info->mr = stbi__get32le(s); info->mg = stbi__get32le(s); @@ -5338,6 +5502,7 @@ static void *stbi__bmp_parse_header(stbi__context *s, stbi__bmp_data *info) return stbi__errpuc("bad BMP", "bad BMP"); } } else { + // V4/V5 header int i; if (hsz != 108 && hsz != 124) return stbi__errpuc("bad BMP", "bad BMP"); @@ -5345,6 +5510,8 @@ static void *stbi__bmp_parse_header(stbi__context *s, stbi__bmp_data *info) info->mg = stbi__get32le(s); info->mb = stbi__get32le(s); info->ma = stbi__get32le(s); + if (compress != 3) // override mr/mg/mb unless in BI_BITFIELDS mode, as per docs + stbi__bmp_set_mask_defaults(info, compress); stbi__get32le(s); // discard color space for (i=0; i < 12; ++i) stbi__get32le(s); // discard color space parameters @@ -5394,9 +5561,22 @@ static void *stbi__bmp_load(stbi__context *s, int *x, int *y, int *comp, int req psize = (info.offset - info.extra_read - info.hsz) >> 2; } if (psize == 0) { - STBI_ASSERT(info.offset == s->callback_already_read + (int) (s->img_buffer - s->img_buffer_original)); - if (info.offset != s->callback_already_read + (s->img_buffer - s->buffer_start)) { - return stbi__errpuc("bad offset", "Corrupt BMP"); + // accept some number of extra bytes after the header, but if the offset points either to before + // the header ends or implies a large amount of extra data, reject the file as malformed + int bytes_read_so_far = s->callback_already_read + (int)(s->img_buffer - s->img_buffer_original); + int header_limit = 1024; // max we actually read is below 256 bytes currently. + int extra_data_limit = 256*4; // what ordinarily goes here is a palette; 256 entries*4 bytes is its max size. + if (bytes_read_so_far <= 0 || bytes_read_so_far > header_limit) { + return stbi__errpuc("bad header", "Corrupt BMP"); + } + // we established that bytes_read_so_far is positive and sensible. + // the first half of this test rejects offsets that are either too small positives, or + // negative, and guarantees that info.offset >= bytes_read_so_far > 0. this in turn + // ensures the number computed in the second half of the test can't overflow. + if (info.offset < bytes_read_so_far || info.offset - bytes_read_so_far > extra_data_limit) { + return stbi__errpuc("bad offset", "Corrupt BMP"); + } else { + stbi__skip(s, info.offset - bytes_read_so_far); } } @@ -6342,6 +6522,7 @@ static void *stbi__pic_load(stbi__context *s,int *px,int *py,int *comp,int req_c // intermediate buffer is RGBA result = (stbi_uc *) stbi__malloc_mad3(x, y, 4, 0); + if (!result) return stbi__errpuc("outofmem", "Out of memory"); memset(result, 0xff, x*y*4); if (!stbi__pic_load_core(s,x,y,comp, result)) { @@ -6457,6 +6638,7 @@ static int stbi__gif_header(stbi__context *s, stbi__gif *g, int *comp, int is_in static int stbi__gif_info_raw(stbi__context *s, int *x, int *y, int *comp) { stbi__gif* g = (stbi__gif*) stbi__malloc(sizeof(stbi__gif)); + if (!g) return stbi__err("outofmem", "Out of memory"); if (!stbi__gif_header(s, g, comp, 1)) { STBI_FREE(g); stbi__rewind( s ); @@ -6766,6 +6948,17 @@ static stbi_uc *stbi__gif_load_next(stbi__context *s, stbi__gif *g, int *comp, i } } +static void *stbi__load_gif_main_outofmem(stbi__gif *g, stbi_uc *out, int **delays) +{ + STBI_FREE(g->out); + STBI_FREE(g->history); + STBI_FREE(g->background); + + if (out) STBI_FREE(out); + if (delays && *delays) STBI_FREE(*delays); + return stbi__errpuc("outofmem", "Out of memory"); +} + static void *stbi__load_gif_main(stbi__context *s, int **delays, int *x, int *y, int *z, int *comp, int req_comp) { if (stbi__gif_test(s)) { @@ -6775,6 +6968,12 @@ static void *stbi__load_gif_main(stbi__context *s, int **delays, int *x, int *y, stbi_uc *two_back = 0; stbi__gif g; int stride; + int out_size = 0; + int delays_size = 0; + + STBI_NOTUSED(out_size); + STBI_NOTUSED(delays_size); + memset(&g, 0, sizeof(g)); if (delays) { *delays = 0; @@ -6791,24 +6990,31 @@ static void *stbi__load_gif_main(stbi__context *s, int **delays, int *x, int *y, stride = g.w * g.h * 4; if (out) { - void *tmp = (stbi_uc*) STBI_REALLOC( out, layers * stride ); - if (NULL == tmp) { - STBI_FREE(g.out); - STBI_FREE(g.history); - STBI_FREE(g.background); - return stbi__errpuc("outofmem", "Out of memory"); - } + void *tmp = (stbi_uc*) STBI_REALLOC_SIZED( out, out_size, layers * stride ); + if (!tmp) + return stbi__load_gif_main_outofmem(&g, out, delays); else { out = (stbi_uc*) tmp; + out_size = layers * stride; } if (delays) { - *delays = (int*) STBI_REALLOC( *delays, sizeof(int) * layers ); + int *new_delays = (int*) STBI_REALLOC_SIZED( *delays, delays_size, sizeof(int) * layers ); + if (!new_delays) + return stbi__load_gif_main_outofmem(&g, out, delays); + *delays = new_delays; + delays_size = layers * sizeof(int); } } else { out = (stbi_uc*)stbi__malloc( layers * stride ); + if (!out) + return stbi__load_gif_main_outofmem(&g, out, delays); + out_size = layers * stride; if (delays) { *delays = (int*) stbi__malloc( layers * sizeof(int) ); + if (!*delays) + return stbi__load_gif_main_outofmem(&g, out, delays); + delays_size = layers * sizeof(int); } } memcpy( out + ((layers - 1) * stride), u, stride ); @@ -7058,12 +7264,12 @@ static float *stbi__hdr_load(stbi__context *s, int *x, int *y, int *comp, int re // Run value = stbi__get8(s); count -= 128; - if (count > nleft) { STBI_FREE(hdr_data); STBI_FREE(scanline); return stbi__errpf("corrupt", "bad RLE data in HDR"); } + if ((count == 0) || (count > nleft)) { STBI_FREE(hdr_data); STBI_FREE(scanline); return stbi__errpf("corrupt", "bad RLE data in HDR"); } for (z = 0; z < count; ++z) scanline[i++ * 4 + k] = value; } else { // Dump - if (count > nleft) { STBI_FREE(hdr_data); STBI_FREE(scanline); return stbi__errpf("corrupt", "bad RLE data in HDR"); } + if ((count == 0) || (count > nleft)) { STBI_FREE(hdr_data); STBI_FREE(scanline); return stbi__errpf("corrupt", "bad RLE data in HDR"); } for (z = 0; z < count; ++z) scanline[i++ * 4 + k] = stbi__get8(s); } @@ -7132,9 +7338,10 @@ static int stbi__bmp_info(stbi__context *s, int *x, int *y, int *comp) info.all_a = 255; p = stbi__bmp_parse_header(s, &info); - stbi__rewind( s ); - if (p == NULL) + if (p == NULL) { + stbi__rewind( s ); return 0; + } if (x) *x = s->img_x; if (y) *y = s->img_y; if (comp) { @@ -7200,8 +7407,8 @@ static int stbi__psd_is16(stbi__context *s) stbi__rewind( s ); return 0; } - (void) stbi__get32be(s); - (void) stbi__get32be(s); + STBI_NOTUSED(stbi__get32be(s)); + STBI_NOTUSED(stbi__get32be(s)); depth = stbi__get16be(s); if (depth != 16) { stbi__rewind( s ); @@ -7280,7 +7487,6 @@ static int stbi__pic_info(stbi__context *s, int *x, int *y, int *comp) // Known limitations: // Does not support comments in the header section // Does not support ASCII image data (formats P2 and P3) -// Does not support 16-bit-per-channel #ifndef STBI_NO_PNM @@ -7301,7 +7507,8 @@ static void *stbi__pnm_load(stbi__context *s, int *x, int *y, int *comp, int req stbi_uc *out; STBI_NOTUSED(ri); - if (!stbi__pnm_info(s, (int *)&s->img_x, (int *)&s->img_y, (int *)&s->img_n)) + ri->bits_per_channel = stbi__pnm_info(s, (int *)&s->img_x, (int *)&s->img_y, (int *)&s->img_n); + if (ri->bits_per_channel == 0) return 0; if (s->img_y > STBI_MAX_DIMENSIONS) return stbi__errpuc("too large","Very large image (corrupt?)"); @@ -7311,15 +7518,22 @@ static void *stbi__pnm_load(stbi__context *s, int *x, int *y, int *comp, int req *y = s->img_y; if (comp) *comp = s->img_n; - if (!stbi__mad3sizes_valid(s->img_n, s->img_x, s->img_y, 0)) + if (!stbi__mad4sizes_valid(s->img_n, s->img_x, s->img_y, ri->bits_per_channel / 8, 0)) return stbi__errpuc("too large", "PNM too large"); - out = (stbi_uc *) stbi__malloc_mad3(s->img_n, s->img_x, s->img_y, 0); + out = (stbi_uc *) stbi__malloc_mad4(s->img_n, s->img_x, s->img_y, ri->bits_per_channel / 8, 0); if (!out) return stbi__errpuc("outofmem", "Out of memory"); - stbi__getn(s, out, s->img_n * s->img_x * s->img_y); + if (!stbi__getn(s, out, s->img_n * s->img_x * s->img_y * (ri->bits_per_channel / 8))) { + STBI_FREE(out); + return stbi__errpuc("bad PNM", "PNM file truncated"); + } if (req_comp && req_comp != s->img_n) { - out = stbi__convert_format(out, s->img_n, req_comp, s->img_x, s->img_y); + if (ri->bits_per_channel == 16) { + out = (stbi_uc *) stbi__convert_format16((stbi__uint16 *) out, s->img_n, req_comp, s->img_x, s->img_y); + } else { + out = stbi__convert_format(out, s->img_n, req_comp, s->img_x, s->img_y); + } if (out == NULL) return out; // stbi__convert_format frees input on failure } return out; @@ -7356,6 +7570,8 @@ static int stbi__pnm_getinteger(stbi__context *s, char *c) while (!stbi__at_eof(s) && stbi__pnm_isdigit(*c)) { value = value*10 + (*c - '0'); *c = (char) stbi__get8(s); + if((value > 214748364) || (value == 214748364 && *c > '7')) + return stbi__err("integer parse overflow", "Parsing an integer in the PPM header overflowed a 32-bit int"); } return value; @@ -7386,17 +7602,29 @@ static int stbi__pnm_info(stbi__context *s, int *x, int *y, int *comp) stbi__pnm_skip_whitespace(s, &c); *x = stbi__pnm_getinteger(s, &c); // read width + if(*x == 0) + return stbi__err("invalid width", "PPM image header had zero or overflowing width"); stbi__pnm_skip_whitespace(s, &c); *y = stbi__pnm_getinteger(s, &c); // read height + if (*y == 0) + return stbi__err("invalid width", "PPM image header had zero or overflowing width"); stbi__pnm_skip_whitespace(s, &c); maxv = stbi__pnm_getinteger(s, &c); // read max value - - if (maxv > 255) - return stbi__err("max value > 255", "PPM image not 8-bit"); + if (maxv > 65535) + return stbi__err("max value > 65535", "PPM image supports only 8-bit and 16-bit images"); + else if (maxv > 255) + return 16; else - return 1; + return 8; +} + +static int stbi__pnm_is16(stbi__context *s) +{ + if (stbi__pnm_info(s, NULL, NULL, NULL) == 16) + return 1; + return 0; } #endif @@ -7452,6 +7680,9 @@ static int stbi__is_16_main(stbi__context *s) if (stbi__psd_is16(s)) return 1; #endif + #ifndef STBI_NO_PNM + if (stbi__pnm_is16(s)) return 1; + #endif return 0; } diff --git a/include/assimp/Base64.hpp b/include/assimp/Base64.hpp index 403723857..10288713c 100644 --- a/include/assimp/Base64.hpp +++ b/include/assimp/Base64.hpp @@ -43,6 +43,8 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #ifndef AI_BASE64_HPP_INC #define AI_BASE64_HPP_INC +#include + #include #include #include @@ -54,35 +56,35 @@ namespace Base64 { /// @param in The UTF-64 buffer. /// @param inLength The size of the buffer /// @param out The encoded ASCII string. -void Encode(const uint8_t *in, size_t inLength, std::string &out); +ASSIMP_API void Encode(const uint8_t *in, size_t inLength, std::string &out); /// @brief Will encode the given character buffer from UTF64 to ASCII. /// @param in A vector, which contains the buffer for encoding. /// @param out The encoded ASCII string. -void Encode(const std::vector& in, std::string &out); +ASSIMP_API void Encode(const std::vector &in, std::string &out); /// @brief Will encode the given character buffer from UTF64 to ASCII. /// @param in A vector, which contains the buffer for encoding. /// @return The encoded ASCII string. -std::string Encode(const std::vector& in); +ASSIMP_API std::string Encode(const std::vector &in); /// @brief Will decode the given character buffer from ASCII to UTF64. /// @param in The ASCII buffer to decode. /// @param inLength The size of the buffer. /// @param out The decoded buffer. /// @return The new buffer size. -size_t Decode(const char *in, size_t inLength, uint8_t *&out); +ASSIMP_API size_t Decode(const char *in, size_t inLength, uint8_t *&out); /// @brief Will decode the given character buffer from ASCII to UTF64. /// @param in The ASCII buffer to decode as a std::string. /// @param out The decoded buffer. /// @return The new buffer size. -size_t Decode(const std::string& in, std::vector& out); +ASSIMP_API size_t Decode(const std::string &in, std::vector &out); /// @brief Will decode the given character buffer from ASCII to UTF64. /// @param in The ASCII string. /// @return The decoded buffer in a vector. -std::vector Decode(const std::string& in); +ASSIMP_API std::vector Decode(const std::string &in); } // namespace Base64 } // namespace Assimp diff --git a/include/assimp/MemoryIOWrapper.h b/include/assimp/MemoryIOWrapper.h index 77071e96f..1b986ff1b 100644 --- a/include/assimp/MemoryIOWrapper.h +++ b/include/assimp/MemoryIOWrapper.h @@ -150,7 +150,6 @@ public: // ------------------------------------------------------------------- /// @brief Tests for the existence of a file at the given path. bool Exists(const char* pFile) const override { - printf("Exists\n"); if (0 == strncmp( pFile, AI_MEMORYIO_MAGIC_FILENAME, AI_MEMORYIO_MAGIC_FILENAME_LENGTH ) ) { return true; } diff --git a/include/assimp/types.h b/include/assimp/types.h index a41363b8a..605dc590f 100644 --- a/include/assimp/types.h +++ b/include/assimp/types.h @@ -73,6 +73,12 @@ typedef uint32_t ai_uint32; #ifdef __cplusplus +#ifdef ASSIMP_USE_HUNTER +# include +#else +# include "../contrib/utf8cpp/source/utf8.h" +#endif + #include #include // for std::nothrow_t #include // for aiString::Set(const std::string&) diff --git a/samples/SimpleTexturedDirectx11/CMakeLists.txt b/samples/SimpleTexturedDirectx11/CMakeLists.txt index 007ada3af..de83734ba 100644 --- a/samples/SimpleTexturedDirectx11/CMakeLists.txt +++ b/samples/SimpleTexturedDirectx11/CMakeLists.txt @@ -8,6 +8,9 @@ if ( MSVC ) ADD_DEFINITIONS( -D_SCL_SECURE_NO_WARNINGS ) ADD_DEFINITIONS( -D_CRT_SECURE_NO_WARNINGS ) REMOVE_DEFINITIONS( -DUNICODE -D_UNICODE ) + if ( MSVC_VERSION GREATER_EQUAL 1930 ) + ADD_DEFINITIONS( -D_SILENCE_CXX17_CODECVT_HEADER_DEPRECATION_WARNING=1 ) + endif () endif () ADD_COMPILE_DEFINITIONS(SHADER_PATH="${CMAKE_CURRENT_SOURCE_DIR}/SimpleTexturedDirectx11/") @@ -33,8 +36,6 @@ ADD_EXECUTABLE( assimp_simpletextureddirectx11 WIN32 #SimpleTexturedDirectx11/VertexShader.hlsl SimpleTexturedDirectx11/main.cpp SimpleTexturedDirectx11/SafeRelease.hpp - ${SAMPLES_SHARED_CODE_DIR}/UTFConverter.cpp - ${SAMPLES_SHARED_CODE_DIR}/UTFConverter.h ) TARGET_USE_COMMON_OUTPUT_DIRECTORY(assimp_simpletextureddirectx11) diff --git a/samples/SimpleTexturedDirectx11/SimpleTexturedDirectx11/main.cpp b/samples/SimpleTexturedDirectx11/SimpleTexturedDirectx11/main.cpp index f7b35024b..4da5820a1 100644 --- a/samples/SimpleTexturedDirectx11/SimpleTexturedDirectx11/main.cpp +++ b/samples/SimpleTexturedDirectx11/SimpleTexturedDirectx11/main.cpp @@ -13,6 +13,8 @@ // Written by IAS. :) // --------------------------------------------------------------------------- +#include + #include #include #include @@ -21,8 +23,8 @@ #include #include #include + #include "ModelLoader.h" -#include "UTFConverter.h" #include "SafeRelease.hpp" #ifdef _MSC_VER @@ -33,7 +35,6 @@ #endif // _MSC_VER using namespace DirectX; -using namespace AssimpSamples::SharedCode; #define VERTEX_SHADER_FILE L"VertexShader.hlsl" #define PIXEL_SHADER_FILE L"PixelShader.hlsl" @@ -50,10 +51,10 @@ struct ConstantBuffer { // ------------------------------------------------------------ // Window Variables // ------------------------------------------------------------ -#define SCREEN_WIDTH 800 -#define SCREEN_HEIGHT 600 +static constexpr uint32_t SCREEN_WIDTH = 800; +static constexpr uint32_t SCREEN_HEIGHT = 600; -const char g_szClassName[] = "directxWindowClass"; +constexpr char g_szClassName[] = "directxWindowClass"; static std::string g_ModelPath; @@ -154,8 +155,14 @@ int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE /*hPrevInstance*/, } // Retrieve the model file path. - g_ModelPath = UTFConverter(argv[1]).str(); + std::wstring filename(argv[1]); + + char *targetStart = new char[filename.size()+1]; + memset(targetStart, '\0', filename.size() + 1); + utf8::utf16to8(filename.c_str(), filename.c_str() + filename.size(), targetStart); + g_ModelPath = targetStart; + delete[] targetStart; free_command_line_allocated_memory(); WNDCLASSEX wc; @@ -511,9 +518,9 @@ void InitPipeline() { ID3DBlob *VS, *PS; if(FAILED(CompileShaderFromFile(SHADER_PATH VERTEX_SHADER_FILE, 0, "main", "vs_4_0", &VS))) - Throwanerror(UTFConverter(L"Failed to compile shader from file " VERTEX_SHADER_FILE).c_str()); + Throwanerror("Failed to compile shader from file"); if(FAILED(CompileShaderFromFile(SHADER_PATH PIXEL_SHADER_FILE, 0, "main", "ps_4_0", &PS))) - Throwanerror(UTFConverter(L"Failed to compile shader from file " PIXEL_SHADER_FILE).c_str()); + Throwanerror("Failed to compile shader from file "); dev->CreateVertexShader(VS->GetBufferPointer(), VS->GetBufferSize(), nullptr, &pVS); dev->CreatePixelShader(PS->GetBufferPointer(), PS->GetBufferSize(), nullptr, &pPS); diff --git a/samples/SimpleTexturedOpenGL/CMakeLists.txt b/samples/SimpleTexturedOpenGL/CMakeLists.txt index 1837af033..70837e87c 100644 --- a/samples/SimpleTexturedOpenGL/CMakeLists.txt +++ b/samples/SimpleTexturedOpenGL/CMakeLists.txt @@ -30,8 +30,6 @@ LINK_DIRECTORIES( ADD_EXECUTABLE( assimp_simpletexturedogl WIN32 SimpleTexturedOpenGL/src/model_loading.cpp - ${SAMPLES_SHARED_CODE_DIR}/UTFConverter.cpp - ${SAMPLES_SHARED_CODE_DIR}/UTFConverter.h ) TARGET_USE_COMMON_OUTPUT_DIRECTORY(assimp_simpletexturedogl) diff --git a/samples/SimpleTexturedOpenGL/SimpleTexturedOpenGL/src/model_loading.cpp b/samples/SimpleTexturedOpenGL/SimpleTexturedOpenGL/src/model_loading.cpp index 48066f189..7d730a630 100644 --- a/samples/SimpleTexturedOpenGL/SimpleTexturedOpenGL/src/model_loading.cpp +++ b/samples/SimpleTexturedOpenGL/SimpleTexturedOpenGL/src/model_loading.cpp @@ -41,15 +41,14 @@ #include #include #include -#include "UTFConverter.h" // The default hard-coded path. Can be overridden by supplying a path through the command line. static std::string modelpath = "../../test/models/OBJ/spider.obj"; -HGLRC hRC=nullptr; // Permanent Rendering Context -HDC hDC=nullptr; // Private GDI Device Context -HWND g_hWnd=nullptr; // Holds Window Handle -HINSTANCE g_hInstance=nullptr; // Holds The Instance Of The Application +HGLRC hRC = nullptr; // Permanent Rendering Context +HDC hDC = nullptr; // Private GDI Device Context +HWND g_hWnd = nullptr; // Holds Window Handle +HINSTANCE g_hInstance = nullptr; // Holds The Instance Of The Application bool keys[256]; // Array used for Keyboard Routine; bool active=TRUE; // Window Active Flag Set To TRUE by Default @@ -69,8 +68,6 @@ GLfloat LightAmbient[]= { 0.5f, 0.5f, 0.5f, 1.0f }; GLfloat LightDiffuse[]= { 1.0f, 1.0f, 1.0f, 1.0f }; GLfloat LightPosition[]= { 0.0f, 0.0f, 15.0f, 1.0f }; - - // the global Assimp scene object const aiScene* g_scene = nullptr; GLuint scene_list = 0; @@ -83,12 +80,8 @@ GLuint* textureIds; // pointer to texture Array // Create an instance of the Importer class Assimp::Importer importer; -using namespace AssimpSamples::SharedCode; - -void createAILogger() -{ - // Change this line to normal if you not want to analyse the import process - //Assimp::Logger::LogSeverity severity = Assimp::Logger::NORMAL; +void createAILogger() { + // Change this line to normal if you not want to analyze the import process Assimp::Logger::LogSeverity severity = Assimp::Logger::VERBOSE; // Create a logger instance for Console Output @@ -101,62 +94,52 @@ void createAILogger() Assimp::DefaultLogger::get()->info("this is my info-call"); } -void destroyAILogger() -{ - // Kill it after the work is done +void destroyAILogger() { Assimp::DefaultLogger::kill(); } -void logInfo(std::string logString) -{ - // Will add message to File with "info" Tag +void logInfo(const std::string &logString) { Assimp::DefaultLogger::get()->info(logString.c_str()); } -void logDebug(const char* logString) -{ - // Will add message to File with "debug" Tag +void logDebug(const char* logString) { Assimp::DefaultLogger::get()->debug(logString); } -bool Import3DFromFile( const std::string& pFile) -{ +bool Import3DFromFile( const std::string &filename) { // Check if file exists - std::ifstream fin(pFile.c_str()); - if(!fin.fail()) - { - fin.close(); + std::ifstream fin(filename.c_str()); + if(fin.fail()) { + std::string message = "Couldn't open file: " + filename; + std::wstring targetMessage; + //utf8::utf8to16(message.c_str(), message.c_str() + message.size(), targetMessage); + ::MessageBox(nullptr, targetMessage.c_str(), L"Error", MB_OK | MB_ICONEXCLAMATION); + logInfo(importer.GetErrorString()); + return false; } - else - { - MessageBox(nullptr, UTFConverter("Couldn't open file: " + pFile).c_wstr() , TEXT("ERROR"), MB_OK | MB_ICONEXCLAMATION); - logInfo( importer.GetErrorString()); - return false; - } - - g_scene = importer.ReadFile(pFile, aiProcessPreset_TargetRealtime_Quality); + + fin.close(); + + g_scene = importer.ReadFile(filename, aiProcessPreset_TargetRealtime_Quality); // If the import failed, report it - if(!g_scene) - { + if (g_scene == nullptr) { logInfo( importer.GetErrorString()); return false; } // Now we can access the file's contents. - logInfo("Import of scene " + pFile + " succeeded."); + logInfo("Import of scene " + filename + " succeeded."); // We're done. Everything will be cleaned up by the importer destructor return true; } // Resize And Initialize The GL Window -void ReSizeGLScene(GLsizei width, GLsizei height) -{ +void ReSizeGLScene(GLsizei width, GLsizei height) { // Prevent A Divide By Zero By - if (height==0) - { + if (height == 0) { // Making Height Equal One height=1; } @@ -174,43 +157,26 @@ void ReSizeGLScene(GLsizei width, GLsizei height) } -std::string getBasePath(const std::string& path) -{ +std::string getBasePath(const std::string& path) { size_t pos = path.find_last_of("\\/"); return (std::string::npos == pos) ? "" : path.substr(0, pos + 1); } -void freeTextureIds() -{ - textureIdMap.clear(); //no need to delete pointers in it manually here. (Pointers point to textureIds deleted in next step) +void freeTextureIds() { + // no need to delete pointers in it manually here. (Pointers point to textureIds deleted in next step) + textureIdMap.clear(); - if (textureIds) - { + if (textureIds) { delete[] textureIds; textureIds = nullptr; } } -int LoadGLTextures(const aiScene* scene) -{ +int LoadGLTextures(const aiScene* scene) { freeTextureIds(); - //ILboolean success; - - /* Before calling ilInit() version should be checked. */ - /*if (ilGetInteger(IL_VERSION_NUM) < IL_VERSION) - { - /// wrong DevIL version /// - std::string err_msg = "Wrong DevIL version. Old devil.dll in system32/SysWow64?"; - char* cErr_msg = (char *) err_msg.c_str(); - abortGLInit(cErr_msg); - return -1; - }*/ - - //ilInit(); /* Initialization of DevIL */ - - if (scene->HasTextures()) return 1; - //abortGLInit("Support for meshes with embedded textures is not implemented"); + if (scene->HasTextures()) + return 1; /* getTexture Filenames and Numb of Textures */ for (unsigned int m=0; mmNumMaterials; m++) @@ -230,14 +196,6 @@ int LoadGLTextures(const aiScene* scene) const size_t numTextures = textureIdMap.size(); - - /* array with DevIL image IDs */ - //ILuint* imageIds = NULL; -// imageIds = new ILuint[numTextures]; - - /* generate DevIL Image IDs */ -// ilGenImages(numTextures, imageIds); /* Generation of numTextures image names */ - /* create and fill array with GL texture ids */ textureIds = new GLuint[numTextures]; glGenTextures(static_cast(numTextures), textureIds); /* Texture name generation */ @@ -248,29 +206,17 @@ int LoadGLTextures(const aiScene* scene) std::string basepath = getBasePath(modelpath); for (size_t i=0; i 1) { std::wstring modelpathW(argv[1]); - modelpath = UTFConverter(modelpathW).str(); + char *tmp = new char[modelpathW.size() + 1]; + memset(tmp, '\0', modelpathW.size() + 1); + utf8::utf16to8(modelpathW.c_str(), modelpathW.c_str() + modelpathW.size(), tmp); + modelpath = tmp; + delete[]tmp; } if (!Import3DFromFile(modelpath)) @@ -871,7 +835,7 @@ int WINAPI WinMain( HINSTANCE /*hInstance*/, // The instance { if (msg.message==WM_QUIT) { - done=TRUE; + done = TRUE; } else { diff --git a/scripts/scan_printf.sh b/scripts/scan_printf.sh new file mode 100755 index 000000000..f8c902941 --- /dev/null +++ b/scripts/scan_printf.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +grep \ + --include=\*.{c,cpp,h} \ + --exclude={pstdint,m3d}.h \ + -rnw include code \ + -e '^\s*printf' + +if [ $? -eq 0 ]; then + echo "Debug statement(s) detected. Please uncomment (using single-line comment), remove, or manually add to exclude filter, if appropriate" + exit 1 +fi + diff --git a/test/models/MDL/MDL (HL1)/multiple_roots.mdl b/test/models/MDL/MDL (HL1)/multiple_roots.mdl new file mode 100644 index 000000000..f8fc1eed7 Binary files /dev/null and b/test/models/MDL/MDL (HL1)/multiple_roots.mdl differ diff --git a/test/models/PLY/pond.0.ply b/test/models/PLY/pond.0.ply index 02b7683ce..a45e0322d 100644 Binary files a/test/models/PLY/pond.0.ply and b/test/models/PLY/pond.0.ply differ diff --git a/test/unit/ImportExport/MDL/utMDLImporter_HL1_ImportSettings.cpp b/test/unit/ImportExport/MDL/utMDLImporter_HL1_ImportSettings.cpp index 4614066e6..d50c2b35a 100644 --- a/test/unit/ImportExport/MDL/utMDLImporter_HL1_ImportSettings.cpp +++ b/test/unit/ImportExport/MDL/utMDLImporter_HL1_ImportSettings.cpp @@ -190,7 +190,7 @@ private: Assimp::Importer importer; importer.SetPropertyBool(setting_key, setting_value); const aiScene *scene = importer.ReadFile(file_path, aiProcess_ValidateDataStructure); - EXPECT_NE(nullptr, scene); + ASSERT_NE(nullptr, scene); func(scene); } diff --git a/test/unit/ImportExport/MDL/utMDLImporter_HL1_Materials.cpp b/test/unit/ImportExport/MDL/utMDLImporter_HL1_Materials.cpp index 2389c0ffc..f733893ca 100644 --- a/test/unit/ImportExport/MDL/utMDLImporter_HL1_Materials.cpp +++ b/test/unit/ImportExport/MDL/utMDLImporter_HL1_Materials.cpp @@ -61,8 +61,8 @@ public: void flatShadeTexture() { Assimp::Importer importer; const aiScene *scene = importer.ReadFile(ASSIMP_TEST_MDL_HL1_MODELS_DIR "chrome_sphere.mdl", aiProcess_ValidateDataStructure); - EXPECT_NE(nullptr, scene); - EXPECT_NE(nullptr, scene->mMaterials); + ASSERT_NE(nullptr, scene); + ASSERT_NE(nullptr, scene->mMaterials); aiShadingMode shading_mode = aiShadingMode_Flat; scene->mMaterials[0]->Get(AI_MATKEY_SHADING_MODEL, shading_mode); @@ -74,8 +74,8 @@ public: void chromeTexture() { Assimp::Importer importer; const aiScene *scene = importer.ReadFile(ASSIMP_TEST_MDL_HL1_MODELS_DIR "chrome_sphere.mdl", aiProcess_ValidateDataStructure); - EXPECT_NE(nullptr, scene); - EXPECT_NE(nullptr, scene->mMaterials); + ASSERT_NE(nullptr, scene); + ASSERT_NE(nullptr, scene->mMaterials); int chrome; scene->mMaterials[0]->Get(AI_MDL_HL1_MATKEY_CHROME(aiTextureType_DIFFUSE, 0), chrome); @@ -87,8 +87,8 @@ public: void additiveBlendTexture() { Assimp::Importer importer; const aiScene *scene = importer.ReadFile(ASSIMP_TEST_MDL_HL1_MODELS_DIR "blend_additive.mdl", aiProcess_ValidateDataStructure); - EXPECT_NE(nullptr, scene); - EXPECT_NE(nullptr, scene->mMaterials); + ASSERT_NE(nullptr, scene); + ASSERT_NE(nullptr, scene->mMaterials); aiBlendMode blend_mode = aiBlendMode_Default; scene->mMaterials[0]->Get(AI_MATKEY_BLEND_FUNC, blend_mode); @@ -101,8 +101,8 @@ public: void textureWithColorMask() { Assimp::Importer importer; const aiScene *scene = importer.ReadFile(ASSIMP_TEST_MDL_HL1_MODELS_DIR "alpha_test.mdl", aiProcess_ValidateDataStructure); - EXPECT_NE(nullptr, scene); - EXPECT_NE(nullptr, scene->mMaterials); + ASSERT_NE(nullptr, scene); + ASSERT_NE(nullptr, scene->mMaterials); int texture_flags = 0; scene->mMaterials[0]->Get(AI_MATKEY_TEXFLAGS_DIFFUSE(0), texture_flags); diff --git a/test/unit/ImportExport/MDL/utMDLImporter_HL1_Nodes.cpp b/test/unit/ImportExport/MDL/utMDLImporter_HL1_Nodes.cpp index 4420727e9..49ae8a16c 100644 --- a/test/unit/ImportExport/MDL/utMDLImporter_HL1_Nodes.cpp +++ b/test/unit/ImportExport/MDL/utMDLImporter_HL1_Nodes.cpp @@ -55,6 +55,17 @@ using namespace Assimp; class utMDLImporter_HL1_Nodes : public ::testing::Test { + /** + * @note Represents a flattened node hierarchy where each item is a pair + * containing the node level and it's name. + */ + using Hierarchy = std::vector>; + + /** + * @note A vector of strings. Used for symplifying syntax. + */ + using StringVector = std::vector; + public: /** * @note The following tests require a basic understanding @@ -63,6 +74,51 @@ public: * (Valve Developer Community). */ + // Given a model, verify that the bones nodes hierarchy is correctly formed. + void checkBoneHierarchy() { + Assimp::Importer importer; + const aiScene *scene = importer.ReadFile(ASSIMP_TEST_MDL_HL1_MODELS_DIR "multiple_roots.mdl", aiProcess_ValidateDataStructure); + ASSERT_NE(nullptr, scene); + ASSERT_NE(nullptr, scene->mRootNode); + + // First, check that "" and "" are linked. + const aiNode* node_MDL_root = scene->mRootNode->FindNode(AI_MDL_HL1_NODE_ROOT); + ASSERT_NE(nullptr, node_MDL_root); + + const aiNode *node_MDL_bones = scene->mRootNode->FindNode(AI_MDL_HL1_NODE_BONES); + ASSERT_NE(nullptr, node_MDL_bones); + ASSERT_NE(nullptr, node_MDL_bones->mParent); + ASSERT_EQ(node_MDL_root, node_MDL_bones->mParent); + + // Second, verify "" hierarchy. + const Hierarchy expected_hierarchy = { + { 0, AI_MDL_HL1_NODE_BONES }, + { 1, "root1_bone1" }, + { 2, "root1_bone2" }, + { 3, "root1_bone4" }, + { 3, "root1_bone5" }, + { 2, "root1_bone3" }, + { 3, "root1_bone6" }, + { 1, "root2_bone1" }, + { 2, "root2_bone2" }, + { 2, "root2_bone3" }, + { 3, "root2_bone5" }, + { 2, "root2_bone4" }, + { 3, "root2_bone6" }, + { 1, "root3_bone1" }, + { 2, "root3_bone2" }, + { 2, "root3_bone3" }, + { 2, "root3_bone4" }, + { 3, "root3_bone5" }, + { 4, "root3_bone6" }, + { 4, "root3_bone7" }, + }; + + Hierarchy actual_hierarchy; + flatten_hierarchy(node_MDL_bones, actual_hierarchy); + ASSERT_EQ(expected_hierarchy, actual_hierarchy); + } + /* Given a model with bones that have empty names, verify that all the bones of the imported model have unique and no empty names. @@ -80,9 +136,9 @@ public: void emptyBonesNames() { Assimp::Importer importer; const aiScene *scene = importer.ReadFile(ASSIMP_TEST_MDL_HL1_MODELS_DIR "unnamed_bones.mdl", aiProcess_ValidateDataStructure); - EXPECT_NE(nullptr, scene); + ASSERT_NE(nullptr, scene); - const std::vector expected_bones_names = { + const StringVector expected_bones_names = { "Bone", "Bone_0", "Bone_1", @@ -94,7 +150,9 @@ public: "Bone_7" }; - expect_named_children(scene, AI_MDL_HL1_NODE_BONES, expected_bones_names); + StringVector actual_bones_names; + get_node_children_names(scene->mRootNode->FindNode(AI_MDL_HL1_NODE_BONES), actual_bones_names); + ASSERT_EQ(expected_bones_names, actual_bones_names); } /* Given a model with bodyparts that have empty names, @@ -114,9 +172,9 @@ public: void emptyBodypartsNames() { Assimp::Importer importer; const aiScene *scene = importer.ReadFile(ASSIMP_TEST_MDL_HL1_MODELS_DIR "unnamed_bodyparts.mdl", aiProcess_ValidateDataStructure); - EXPECT_NE(nullptr, scene); + ASSERT_NE(nullptr, scene); - const std::vector expected_bodyparts_names = { + const StringVector expected_bodyparts_names = { "Bodypart", "Bodypart_1", "Bodypart_5", @@ -128,7 +186,10 @@ public: "Bodypart_7" }; - expect_named_children(scene, AI_MDL_HL1_NODE_BODYPARTS, expected_bodyparts_names); + StringVector actual_bodyparts_names; + // Get the bodyparts names "without" the submodels. + get_node_children_names(scene->mRootNode->FindNode(AI_MDL_HL1_NODE_BODYPARTS), actual_bodyparts_names, 0); + ASSERT_EQ(expected_bodyparts_names, actual_bodyparts_names); } /* Given a model with bodyparts that have duplicate names, @@ -148,9 +209,9 @@ public: void duplicateBodypartsNames() { Assimp::Importer importer; const aiScene *scene = importer.ReadFile(ASSIMP_TEST_MDL_HL1_MODELS_DIR "duplicate_bodyparts.mdl", aiProcess_ValidateDataStructure); - EXPECT_NE(nullptr, scene); + ASSERT_NE(nullptr, scene); - const std::vector expected_bodyparts_names = { + const StringVector expected_bodyparts_names = { "Bodypart", "Bodypart_1", "Bodypart_2", @@ -162,7 +223,10 @@ public: "Bodypart_4" }; - expect_named_children(scene, AI_MDL_HL1_NODE_BODYPARTS, expected_bodyparts_names); + StringVector actual_bodyparts_names; + // Get the bodyparts names "without" the submodels. + get_node_children_names(scene->mRootNode->FindNode(AI_MDL_HL1_NODE_BODYPARTS), actual_bodyparts_names, 0); + ASSERT_EQ(expected_bodyparts_names, actual_bodyparts_names); } /* Given a model with several bodyparts that contains multiple @@ -190,9 +254,9 @@ public: void duplicateSubModelsNames() { Assimp::Importer importer; const aiScene *scene = importer.ReadFile(ASSIMP_TEST_MDL_HL1_MODELS_DIR "duplicate_submodels.mdl", aiProcess_ValidateDataStructure); - EXPECT_NE(nullptr, scene); + ASSERT_NE(nullptr, scene); - const std::vector> expected_bodypart_sub_models_names = { + const std::vector expected_bodypart_sub_models_names = { { "triangle", "triangle_0", @@ -208,11 +272,15 @@ public: }; const aiNode *bodyparts_node = scene->mRootNode->FindNode(AI_MDL_HL1_NODE_BODYPARTS); - EXPECT_NE(nullptr, bodyparts_node); + ASSERT_NE(nullptr, bodyparts_node); EXPECT_EQ(3u, bodyparts_node->mNumChildren); - for (unsigned int i = 0; i < bodyparts_node->mNumChildren; ++i) { - expect_named_children(bodyparts_node->mChildren[i], - expected_bodypart_sub_models_names[i]); + + StringVector actual_submodels_names; + for (unsigned int i = 0; i < bodyparts_node->mNumChildren; ++i) + { + actual_submodels_names.clear(); + get_node_children_names(bodyparts_node->mChildren[i], actual_submodels_names); + ASSERT_EQ(expected_bodypart_sub_models_names[i], actual_submodels_names); } } @@ -233,9 +301,9 @@ public: void duplicateSequenceNames() { Assimp::Importer importer; const aiScene *scene = importer.ReadFile(ASSIMP_TEST_MDL_HL1_MODELS_DIR "duplicate_sequences.mdl", aiProcess_ValidateDataStructure); - EXPECT_NE(nullptr, scene); + ASSERT_NE(nullptr, scene); - const std::vector expected_sequence_names = { + const StringVector expected_sequence_names = { "idle_1", "idle", "idle_2", @@ -247,7 +315,9 @@ public: "idle_7" }; - expect_named_children(scene, AI_MDL_HL1_NODE_SEQUENCE_INFOS, expected_sequence_names); + StringVector actual_sequence_names; + get_node_children_names(scene->mRootNode->FindNode(AI_MDL_HL1_NODE_SEQUENCE_INFOS), actual_sequence_names); + ASSERT_EQ(expected_sequence_names, actual_sequence_names); } /* Given a model with sequences that have empty names, verify @@ -267,9 +337,9 @@ public: void emptySequenceNames() { Assimp::Importer importer; const aiScene *scene = importer.ReadFile(ASSIMP_TEST_MDL_HL1_MODELS_DIR "unnamed_sequences.mdl", aiProcess_ValidateDataStructure); - EXPECT_NE(nullptr, scene); + ASSERT_NE(nullptr, scene); - const std::vector expected_sequence_names = { + const StringVector expected_sequence_names = { "Sequence", "Sequence_1", "Sequence_0", @@ -281,7 +351,9 @@ public: "Sequence_6" }; - expect_named_children(scene, AI_MDL_HL1_NODE_SEQUENCE_INFOS, expected_sequence_names); + StringVector actual_sequence_names; + get_node_children_names(scene->mRootNode->FindNode(AI_MDL_HL1_NODE_SEQUENCE_INFOS), actual_sequence_names); + ASSERT_EQ(expected_sequence_names, actual_sequence_names); } /* Given a model with sequence groups that have duplicate names, @@ -302,9 +374,9 @@ public: void duplicateSequenceGroupNames() { Assimp::Importer importer; const aiScene *scene = importer.ReadFile(ASSIMP_TEST_MDL_HL1_MODELS_DIR "duplicate_sequence_groups/duplicate_sequence_groups.mdl", aiProcess_ValidateDataStructure); - EXPECT_NE(nullptr, scene); + ASSERT_NE(nullptr, scene); - const std::vector expected_sequence_names = { + const StringVector expected_sequence_names = { "default", "SequenceGroup", "SequenceGroup_1", @@ -317,7 +389,9 @@ public: "SequenceGroup_2" }; - expect_named_children(scene, AI_MDL_HL1_NODE_SEQUENCE_GROUPS, expected_sequence_names); + StringVector actual_sequence_names; + get_node_children_names(scene->mRootNode->FindNode(AI_MDL_HL1_NODE_SEQUENCE_GROUPS), actual_sequence_names); + ASSERT_EQ(expected_sequence_names, actual_sequence_names); } /* Given a model with sequence groups that have empty names, @@ -338,9 +412,9 @@ public: void emptySequenceGroupNames() { Assimp::Importer importer; const aiScene *scene = importer.ReadFile(ASSIMP_TEST_MDL_HL1_MODELS_DIR "unnamed_sequence_groups/unnamed_sequence_groups.mdl", aiProcess_ValidateDataStructure); - EXPECT_NE(nullptr, scene); + ASSERT_NE(nullptr, scene); - const std::vector expected_sequence_names = { + const StringVector expected_sequence_names = { "default", "SequenceGroup", "SequenceGroup_2", @@ -353,7 +427,9 @@ public: "SequenceGroup_4" }; - expect_named_children(scene, AI_MDL_HL1_NODE_SEQUENCE_GROUPS, expected_sequence_names); + StringVector actual_sequence_names; + get_node_children_names(scene->mRootNode->FindNode(AI_MDL_HL1_NODE_SEQUENCE_GROUPS), actual_sequence_names); + ASSERT_EQ(expected_sequence_names, actual_sequence_names); } /* Verify that mOffsetMatrix applies the correct @@ -364,7 +440,7 @@ public: Assimp::Importer importer; const aiScene *scene = importer.ReadFile(MDL_HL1_FILE_MAN, aiProcess_ValidateDataStructure); - EXPECT_NE(nullptr, scene); + ASSERT_NE(nullptr, scene); aiNode *scene_bones_node = scene->mRootNode->FindNode(AI_MDL_HL1_NODE_BONES); @@ -398,26 +474,58 @@ public: } private: - void expect_named_children(const aiNode *parent_node, const std::vector &expected_names) { - EXPECT_NE(nullptr, parent_node); - EXPECT_EQ(expected_names.size(), parent_node->mNumChildren); - - for (unsigned int i = 0; i < parent_node->mNumChildren; ++i) - EXPECT_EQ(expected_names[i], parent_node->mChildren[i]->mName.C_Str()); - } - - void expect_named_children(const aiScene *scene, const char *node_name, const std::vector &expected_names) { - expect_named_children(scene->mRootNode->FindNode(node_name), expected_names); - } - void expect_equal_matrices(const aiMatrix4x4 &expected, const aiMatrix4x4 &actual, float abs_error) { for (int i = 0; i < 4; ++i) { for (int j = 0; j < 4; ++j) EXPECT_NEAR(expected[i][j], actual[i][j], abs_error); } } + + /** Get a flattened representation of a node's hierarchy. + * \param[in] node The node. + * \param[out] hierarchy The flattened node's hierarchy. + */ + void flatten_hierarchy(const aiNode *node, Hierarchy &hierarchy) + { + flatten_hierarchy_impl(node, hierarchy, 0); + } + + void flatten_hierarchy_impl(const aiNode *node, Hierarchy &hierarchy, unsigned int level) + { + hierarchy.push_back({ level, node->mName.C_Str() }); + for (size_t i = 0; i < node->mNumChildren; ++i) + { + flatten_hierarchy_impl(node->mChildren[i], hierarchy, level + 1); + } + } + + /** Get all node's children names beneath max_level. + * \param[in] node The parent node from which to get all children names. + * \param[out] names The list of children names. + * \param[in] max_level If set to -1, all children names will be collected. + */ + void get_node_children_names(const aiNode *node, StringVector &names, const int max_level = -1) + { + get_node_children_names_impl(node, names, 0, max_level); + } + + void get_node_children_names_impl(const aiNode *node, StringVector &names, int level, const int max_level = -1) + { + for (size_t i = 0; i < node->mNumChildren; ++i) + { + names.push_back(node->mChildren[i]->mName.C_Str()); + if (max_level == -1 || level < max_level) + { + get_node_children_names_impl(node->mChildren[i], names, level + 1, max_level); + } + } + } }; +TEST_F(utMDLImporter_HL1_Nodes, checkBoneHierarchy) { + checkBoneHierarchy(); +} + TEST_F(utMDLImporter_HL1_Nodes, emptyBonesNames) { emptyBonesNames(); } diff --git a/test/unit/utACImportExport.cpp b/test/unit/utACImportExport.cpp index 9615a3a3e..c844603cf 100644 --- a/test/unit/utACImportExport.cpp +++ b/test/unit/utACImportExport.cpp @@ -43,6 +43,8 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include #include +#include + using namespace Assimp; @@ -68,6 +70,27 @@ TEST(utACImportExport, importSampleSubdiv) { Assimp::Importer importer; const aiScene *scene = importer.ReadFile(ASSIMP_TEST_MODELS_DIR "/AC/sample_subdiv.ac", aiProcess_ValidateDataStructure); ASSERT_NE(nullptr, scene); + + // check approximate shape by averaging together all vertices + ASSERT_EQ(scene->mNumMeshes, 1u); + aiVector3D vertexAvg(0.0, 0.0, 0.0); + for (unsigned int i = 0; i < scene->mNumMeshes; i++) { + const aiMesh *mesh = scene->mMeshes[i]; + ASSERT_NE(mesh, nullptr); + + ai_real invVertexCount = 1.0 / mesh->mNumVertices; + for (unsigned int j = 0; j < mesh->mNumVertices; j++) { + vertexAvg += mesh->mVertices[j] * invVertexCount; + } + } + + // must not be inf or nan + ASSERT_TRUE(std::isfinite(vertexAvg.x)); + ASSERT_TRUE(std::isfinite(vertexAvg.y)); + ASSERT_TRUE(std::isfinite(vertexAvg.z)); + EXPECT_NEAR(vertexAvg.x, 0.079997420310974121, 0.0001); + EXPECT_NEAR(vertexAvg.y, 0.099498569965362549, 0.0001); + EXPECT_NEAR(vertexAvg.z, -0.10344827175140381, 0.0001); } TEST(utACImportExport, importSphereWithLight) { diff --git a/test/unit/utASEImportExport.cpp b/test/unit/utASEImportExport.cpp index 8014cbbc7..af05a2fe5 100644 --- a/test/unit/utASEImportExport.cpp +++ b/test/unit/utASEImportExport.cpp @@ -44,6 +44,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include #include +#include using namespace Assimp; @@ -63,3 +64,112 @@ public: TEST_F(utASEImportExport, importACFromFileTest) { EXPECT_TRUE(importerTest()); } + + +TEST_F(utASEImportExport, importAnim1) { + ::Assimp::Importer importer; + const aiScene *scene = importer.ReadFile(ASSIMP_TEST_MODELS_DIR "/ASE/anim.ASE", aiProcess_ValidateDataStructure); + + ASSERT_NE(nullptr, scene); +} + + +TEST_F(utASEImportExport, importAnim2) { + ::Assimp::Importer importer; + const aiScene *scene = importer.ReadFile(ASSIMP_TEST_MODELS_DIR "/ASE/anim2.ASE", aiProcess_ValidateDataStructure); + + ASSERT_NE(nullptr, scene); +} + + +TEST_F(utASEImportExport, importCameraRollAnim) { + ::Assimp::Importer importer; + const aiScene *scene = importer.ReadFile(ASSIMP_TEST_MODELS_DIR "/ASE/CameraRollAnim.ase", aiProcess_ValidateDataStructure); + + ASSERT_NE(nullptr, scene); +} + + +TEST_F(utASEImportExport, importMotionCaptureROM) { + ::Assimp::Importer importer; + const aiScene *scene = importer.ReadFile(ASSIMP_TEST_MODELS_DIR "/ASE/MotionCaptureROM.ase", aiProcess_ValidateDataStructure); + + ASSERT_NE(nullptr, scene); +} + + +TEST_F(utASEImportExport, importRotatingCube) { + ::Assimp::Importer importer; + const aiScene *scene = importer.ReadFile(ASSIMP_TEST_MODELS_DIR "/ASE/RotatingCube.ASE", aiProcess_ValidateDataStructure); + + ASSERT_NE(nullptr, scene); +} + + +TEST_F(utASEImportExport, importTargetCameraAnim) { + ::Assimp::Importer importer; + const aiScene *scene = importer.ReadFile(ASSIMP_TEST_MODELS_DIR "/ASE/TargetCameraAnim.ase", aiProcess_ValidateDataStructure); + + ASSERT_NE(nullptr, scene); +} + + +TEST_F(utASEImportExport, importTestFormatDetection) { + ::Assimp::Importer importer; + const aiScene *scene = importer.ReadFile(ASSIMP_TEST_MODELS_DIR "/ASE/TestFormatDetection", aiProcess_ValidateDataStructure); + + ASSERT_NE(nullptr, scene); +} + + +TEST_F(utASEImportExport, importThreeCubesGreen) { + ::Assimp::Importer importer; + const aiScene *scene = importer.ReadFile(ASSIMP_TEST_MODELS_DIR "/ASE/ThreeCubesGreen.ASE", aiProcess_ValidateDataStructure); + + ASSERT_NE(nullptr, scene); + + ::Assimp::Importer importerLE; + const aiScene *sceneLE = importerLE.ReadFile(ASSIMP_TEST_MODELS_DIR "/ASE/ThreeCubesGreen_UTF16LE.ASE", aiProcess_ValidateDataStructure); + + ASSERT_NE(nullptr, sceneLE); + + ::Assimp::Importer importerBE; + const aiScene *sceneBE = importerBE.ReadFile(ASSIMP_TEST_MODELS_DIR "/ASE/ThreeCubesGreen_UTF16BE.ASE", aiProcess_ValidateDataStructure); + + ASSERT_NE(nullptr, sceneBE); + + // TODO: these scenes should probably be identical + // verify that is the case and then add tests to check it +} + + +TEST_F(utASEImportExport, importUVTransform_Normal) { + ::Assimp::Importer importer; + const aiScene *scene = importer.ReadFile(ASSIMP_TEST_MODELS_DIR "/ASE/TestUVTransform/UVTransform_Normal.ASE", aiProcess_ValidateDataStructure); + + ASSERT_NE(nullptr, scene); +} + + +TEST_F(utASEImportExport, importUVTransform_ScaleUV1_2_OffsetUV0_0_9_Rotate_72_mirrorU) { + ::Assimp::Importer importer; + const aiScene *scene = importer.ReadFile(ASSIMP_TEST_MODELS_DIR "/ASE/TestUVTransform/UVTransform_ScaleUV1-2_OffsetUV0-0.9_Rotate-72_mirrorU.ase", aiProcess_ValidateDataStructure); + + ASSERT_NE(nullptr, scene); +} + + +TEST_F(utASEImportExport, importUVTransform_ScaleUV2x) { + ::Assimp::Importer importer; + const aiScene *scene = importer.ReadFile(ASSIMP_TEST_MODELS_DIR "/ASE/TestUVTransform/UVTransform_ScaleUV2x.ASE", aiProcess_ValidateDataStructure); + + ASSERT_NE(nullptr, scene); +} + + +TEST_F(utASEImportExport, importUVTransform_ScaleUV2x_Rotate45) { + ::Assimp::Importer importer; + const aiScene *scene = importer.ReadFile(ASSIMP_TEST_MODELS_DIR "/ASE/TestUVTransform/UVTransform_ScaleUV2x_Rotate45.ASE", aiProcess_ValidateDataStructure); + + ASSERT_NE(nullptr, scene); +} diff --git a/test/unit/utBlenderImportExport.cpp b/test/unit/utBlenderImportExport.cpp index c9cce72b4..c220b7daa 100644 --- a/test/unit/utBlenderImportExport.cpp +++ b/test/unit/utBlenderImportExport.cpp @@ -45,6 +45,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include #include +#include using namespace Assimp; @@ -156,6 +157,27 @@ TEST(utBlenderImporter, importSuzanneSubdiv_252) { Assimp::Importer importer; const aiScene *scene = importer.ReadFile(ASSIMP_TEST_MODELS_DIR "/BLEND/SuzanneSubdiv_252.blend", aiProcess_ValidateDataStructure); ASSERT_NE(nullptr, scene); + + // check approximate shape by averaging together all vertices + ASSERT_EQ(scene->mNumMeshes, 1u); + aiVector3D vertexAvg(0.0, 0.0, 0.0); + for (unsigned int i = 0; i < scene->mNumMeshes; i++) { + const aiMesh *mesh = scene->mMeshes[i]; + ASSERT_NE(mesh, nullptr); + + ai_real invVertexCount = 1.0 / mesh->mNumVertices; + for (unsigned int j = 0; j < mesh->mNumVertices; j++) { + vertexAvg += mesh->mVertices[j] * invVertexCount; + } + } + + // must not be inf or nan + ASSERT_TRUE(std::isfinite(vertexAvg.x)); + ASSERT_TRUE(std::isfinite(vertexAvg.y)); + ASSERT_TRUE(std::isfinite(vertexAvg.z)); + EXPECT_NEAR(vertexAvg.x, 6.4022515289252624e-08, 0.0001); + EXPECT_NEAR(vertexAvg.y, 0.060569953173398972, 0.0001); + EXPECT_NEAR(vertexAvg.z, 0.31429031491279602, 0.0001); } TEST(utBlenderImporter, importTexturedCube_ImageGlob_248) { diff --git a/tools/assimp_view/AnimEvaluator.cpp b/tools/assimp_view/AnimEvaluator.cpp index ad8e7f5b2..4f8d1bda8 100644 --- a/tools/assimp_view/AnimEvaluator.cpp +++ b/tools/assimp_view/AnimEvaluator.cpp @@ -55,12 +55,6 @@ AnimEvaluator::AnimEvaluator(const aiAnimation *pAnim) : mLastPositions.resize(pAnim->mNumChannels, std::make_tuple(0, 0, 0)); } -// ------------------------------------------------------------------------------------------------ -// Destructor. -AnimEvaluator::~AnimEvaluator() { - // empty -} - // ------------------------------------------------------------------------------------------------ // Evaluates the animation tracks for a given time stamp. void AnimEvaluator::Evaluate(double pTime) { diff --git a/tools/assimp_view/AnimEvaluator.h b/tools/assimp_view/AnimEvaluator.h index 76b22ea8a..aa44ab211 100644 --- a/tools/assimp_view/AnimEvaluator.h +++ b/tools/assimp_view/AnimEvaluator.h @@ -66,7 +66,7 @@ public: AnimEvaluator(const aiAnimation *pAnim); /// @brief The class destructor. - ~AnimEvaluator(); + ~AnimEvaluator() = default; /// @brief Evaluates the animation tracks for a given time stamp. /// The calculated pose can be retrieved as an array of transformation diff --git a/tools/assimp_view/AssetHelper.h b/tools/assimp_view/AssetHelper.h index 1ae469f85..8661d875f 100644 --- a/tools/assimp_view/AssetHelper.h +++ b/tools/assimp_view/AssetHelper.h @@ -77,6 +77,13 @@ public: pcScene = NULL; } + // set the normal set to be used + void SetNormalSet(unsigned int iSet); + + // flip all normal vectors + void FlipNormals(); + void FlipNormalsInt(); + //--------------------------------------------------------------- // default vertex data structure // (even if tangents, bitangents or normals aren't @@ -221,16 +228,8 @@ public: // Specifies the normal set to be used unsigned int iNormalSet; - - // ------------------------------------------------------------------ - // set the normal set to be used - void SetNormalSet(unsigned int iSet); - - // ------------------------------------------------------------------ - // flip all normal vectors - void FlipNormals(); - void FlipNormalsInt(); }; + } // namespace AssimpView #endif // !! IG diff --git a/tools/assimp_view/Material.cpp b/tools/assimp_view/Material.cpp index e3c023bd9..c28231332 100644 --- a/tools/assimp_view/Material.cpp +++ b/tools/assimp_view/Material.cpp @@ -175,33 +175,29 @@ VOID WINAPI FillFunc(D3DXVECTOR4* pOut, pOut->x = pOut->y = 1.0f; pOut->z = 0.0f; } - return; } //------------------------------------------------------------------------------- -int CMaterialManager::UpdateSpecularMaterials() - { - if (g_pcAsset && g_pcAsset->pcScene) - { - for (unsigned int i = 0; i < g_pcAsset->pcScene->mNumMeshes;++i) - { - if (aiShadingMode_Phong == g_pcAsset->apcMeshes[i]->eShadingMode) - { +int CMaterialManager::UpdateSpecularMaterials() { + if (g_pcAsset && g_pcAsset->pcScene) { + for (unsigned int i = 0; i < g_pcAsset->pcScene->mNumMeshes;++i) { + if (aiShadingMode_Phong == g_pcAsset->apcMeshes[i]->eShadingMode) { this->DeleteMaterial(g_pcAsset->apcMeshes[i]); this->CreateMaterial(g_pcAsset->apcMeshes[i],g_pcAsset->pcScene->mMeshes[i]); - } } } - return 1; } + return 1; +} + //------------------------------------------------------------------------------- -int CMaterialManager::SetDefaultTexture(IDirect3DTexture9** p_ppiOut) -{ +int CMaterialManager::SetDefaultTexture(IDirect3DTexture9** p_ppiOut) { if (sDefaultTexture) { sDefaultTexture->AddRef(); *p_ppiOut = sDefaultTexture; return 1; } + if(FAILED(g_piDevice->CreateTexture( 256, 256, diff --git a/tools/assimp_view/assimp_view.cpp b/tools/assimp_view/assimp_view.cpp index c5c48fd93..e00c6e39d 100644 --- a/tools/assimp_view/assimp_view.cpp +++ b/tools/assimp_view/assimp_view.cpp @@ -145,9 +145,7 @@ float g_fLoadTime = 0.0f; // The loader thread loads the asset while the progress dialog displays the // smart progress bar //------------------------------------------------------------------------------- -DWORD WINAPI LoadThreadProc(LPVOID lpParameter) { - UNREFERENCED_PARAMETER(lpParameter); - +DWORD WINAPI LoadThreadProc(LPVOID) { // get current time double fCur = (double)timeGetTime(); @@ -367,7 +365,7 @@ int CalculateBounds(aiNode *piNode, aiVector3D *p_avOut, const aiMatrix4x4 &piMa // The function calculates the boundaries of the mesh and modifies the // global world transformation matrix according to the aset AABB //------------------------------------------------------------------------------- -int ScaleAsset(void) { +int ScaleAsset() { aiVector3D aiVecs[2] = { aiVector3D(1e10f, 1e10f, 1e10f), aiVector3D(-1e10f, -1e10f, -1e10f) }; @@ -521,8 +519,7 @@ int CreateAssetData() { } } else { // create 16 bit index buffer - if (FAILED(g_piDevice->CreateIndexBuffer(2 * -numIndices, + if (FAILED(g_piDevice->CreateIndexBuffer(2 * numIndices, D3DUSAGE_WRITEONLY | dwUsage, D3DFMT_INDEX16, D3DPOOL_DEFAULT, diff --git a/tools/assimp_view/assimp_view.h b/tools/assimp_view/assimp_view.h index cbcee3cac..e67cc9fd0 100644 --- a/tools/assimp_view/assimp_view.h +++ b/tools/assimp_view/assimp_view.h @@ -98,6 +98,12 @@ namespace AssimpView { //------------------------------------------------------------------------------- // Function prototypes //------------------------------------------------------------------------------- +class AssimpVew { +public: + AssimpVew(); + ~AssimpVew(); +}; + int InitD3D(void); int ShutdownD3D(void); int CreateDevice(bool p_bMultiSample, bool p_bSuperSample, bool bHW = true);