From 50d7e6fc61c8d1d6d0a0c75f35e6d59a68fcd509 Mon Sep 17 00:00:00 2001 From: Kim Kulling Date: Wed, 16 Feb 2022 00:18:32 +0100 Subject: [PATCH 01/17] Add definition for skeleton --- include/assimp/mesh.h | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/include/assimp/mesh.h b/include/assimp/mesh.h index 5704e7ca0..87b9de262 100644 --- a/include/assimp/mesh.h +++ b/include/assimp/mesh.h @@ -937,6 +937,20 @@ struct aiMesh { #endif // __cplusplus }; +struct aiSkeleton { + C_STRUCT aiString mName; + unsigned int mNumWeights; + C_STRUCT aiVertexWeight *mWeights; + +#ifdef __cplusplus + aiSkeleton() AI_NO_EXCEPT : mName(), mNumWeights(0), mWeights(nullptr) { + // empty + } + ~aiSkeleton() { + + } +#endif // __cplusplus +}; #ifdef __cplusplus } #endif //! extern "C" From e5747dad9bb22ac80470482f0e60f7f8a56ce59c Mon Sep 17 00:00:00 2001 From: Kim Kulling Date: Tue, 5 Apr 2022 20:07:22 +0200 Subject: [PATCH 02/17] First concepts --- code/AssetLib/FBX/FBXConverter.cpp | 16 ++-- code/AssetLib/FBX/FBXImportSettings.h | 6 ++ code/AssetLib/FBX/FBXImporter.cpp | 47 +++++---- code/AssetLib/FBX/FBXImporter.h | 13 +-- code/Common/SkeletonMeshBuilder.cpp | 14 +-- include/assimp/config.h.in | 9 ++ include/assimp/mesh.h | 132 +++++++++++++++++++++----- include/assimp/scene.h | 14 +++ 8 files changed, 182 insertions(+), 69 deletions(-) diff --git a/code/AssetLib/FBX/FBXConverter.cpp b/code/AssetLib/FBX/FBXConverter.cpp index 32872108b..c79d43cbf 100644 --- a/code/AssetLib/FBX/FBXConverter.cpp +++ b/code/AssetLib/FBX/FBXConverter.cpp @@ -1435,6 +1435,16 @@ unsigned int FBXConverter::ConvertMeshMultiMaterial(const MeshGeometry &mesh, co return static_cast(mMeshes.size() - 1); } +void ConvertWeightsToSkeleton(const MeshGeometry &geo, const aiMatrix4x4 &absolute_transform, aiNode *parent, unsigned int materialIndex, + std::vector *outputVertStartIndices) { + ai_assert(geo.DeformerSkin() != nullptr); + + const Skin &sk = *geo.DeformerSkin(); + for (auto &cluster : sk.Clusters()) { + cluster->Transform(); + } +} + void FBXConverter::ConvertWeights(aiMesh *out, const MeshGeometry &geo, const aiMatrix4x4 &absolute_transform, aiNode *parent, unsigned int materialIndex, @@ -1529,12 +1539,6 @@ void FBXConverter::ConvertWeights(aiMesh *out, const MeshGeometry &geo, } } -const aiNode *GetNodeByName(aiNode *current_node) { - aiNode *iter = current_node; - //printf("Child count: %d", iter->mNumChildren); - return iter; -} - void FBXConverter::ConvertCluster(std::vector &local_mesh_bones, const Cluster *cl, std::vector &out_indices, std::vector &index_out_indices, std::vector &count_out_indices, const aiMatrix4x4 &absolute_transform, diff --git a/code/AssetLib/FBX/FBXImportSettings.h b/code/AssetLib/FBX/FBXImportSettings.h index 90e64bf04..698901180 100644 --- a/code/AssetLib/FBX/FBXImportSettings.h +++ b/code/AssetLib/FBX/FBXImportSettings.h @@ -60,6 +60,7 @@ struct ImportSettings { readLights(true), readAnimations(true), readWeights(true), + useSkeleton(false), preservePivots(true), optimizeEmptyAnimationCurves(true), useLegacyEmbeddedTextureNaming(false), @@ -112,6 +113,11 @@ struct ImportSettings { * Default value is true. */ bool readWeights; + /** will convert all animation data into a skeleton (experimental) + * Default value is false. + */ + bool useSkeleton; + /** preserve transformation pivots and offsets. Since these can * not directly be represented in assimp, additional dummy * nodes will be generated. Note that settings this to false diff --git a/code/AssetLib/FBX/FBXImporter.cpp b/code/AssetLib/FBX/FBXImporter.cpp index 0f63acc8f..7ff194905 100644 --- a/code/AssetLib/FBX/FBXImporter.cpp +++ b/code/AssetLib/FBX/FBXImporter.cpp @@ -90,12 +90,9 @@ static const aiImporterDesc desc = { // ------------------------------------------------------------------------------------------------ // Constructor to be privately used by #Importer -FBXImporter::FBXImporter() { -} - -// ------------------------------------------------------------------------------------------------ -// Destructor, private as well -FBXImporter::~FBXImporter() { +FBXImporter::FBXImporter() : + mSettings() { + // empty } // ------------------------------------------------------------------------------------------------ @@ -115,20 +112,21 @@ const aiImporterDesc *FBXImporter::GetInfo() const { // ------------------------------------------------------------------------------------------------ // Setup configuration properties for the loader void FBXImporter::SetupProperties(const Importer *pImp) { - settings.readAllLayers = pImp->GetPropertyBool(AI_CONFIG_IMPORT_FBX_READ_ALL_GEOMETRY_LAYERS, true); - settings.readAllMaterials = pImp->GetPropertyBool(AI_CONFIG_IMPORT_FBX_READ_ALL_MATERIALS, false); - settings.readMaterials = pImp->GetPropertyBool(AI_CONFIG_IMPORT_FBX_READ_MATERIALS, true); - settings.readTextures = pImp->GetPropertyBool(AI_CONFIG_IMPORT_FBX_READ_TEXTURES, true); - settings.readCameras = pImp->GetPropertyBool(AI_CONFIG_IMPORT_FBX_READ_CAMERAS, true); - settings.readLights = pImp->GetPropertyBool(AI_CONFIG_IMPORT_FBX_READ_LIGHTS, true); - settings.readAnimations = pImp->GetPropertyBool(AI_CONFIG_IMPORT_FBX_READ_ANIMATIONS, true); - settings.readWeights = pImp->GetPropertyBool(AI_CONFIG_IMPORT_FBX_READ_WEIGHTS, true); - settings.strictMode = pImp->GetPropertyBool(AI_CONFIG_IMPORT_FBX_STRICT_MODE, false); - settings.preservePivots = pImp->GetPropertyBool(AI_CONFIG_IMPORT_FBX_PRESERVE_PIVOTS, true); - settings.optimizeEmptyAnimationCurves = pImp->GetPropertyBool(AI_CONFIG_IMPORT_FBX_OPTIMIZE_EMPTY_ANIMATION_CURVES, true); - settings.useLegacyEmbeddedTextureNaming = pImp->GetPropertyBool(AI_CONFIG_IMPORT_FBX_EMBEDDED_TEXTURES_LEGACY_NAMING, false); - settings.removeEmptyBones = pImp->GetPropertyBool(AI_CONFIG_IMPORT_REMOVE_EMPTY_BONES, true); - settings.convertToMeters = pImp->GetPropertyBool(AI_CONFIG_FBX_CONVERT_TO_M, false); + mSettings.readAllLayers = pImp->GetPropertyBool(AI_CONFIG_IMPORT_FBX_READ_ALL_GEOMETRY_LAYERS, true); + mSettings.readAllMaterials = pImp->GetPropertyBool(AI_CONFIG_IMPORT_FBX_READ_ALL_MATERIALS, false); + mSettings.readMaterials = pImp->GetPropertyBool(AI_CONFIG_IMPORT_FBX_READ_MATERIALS, true); + mSettings.readTextures = pImp->GetPropertyBool(AI_CONFIG_IMPORT_FBX_READ_TEXTURES, true); + mSettings.readCameras = pImp->GetPropertyBool(AI_CONFIG_IMPORT_FBX_READ_CAMERAS, true); + mSettings.readLights = pImp->GetPropertyBool(AI_CONFIG_IMPORT_FBX_READ_LIGHTS, true); + mSettings.readAnimations = pImp->GetPropertyBool(AI_CONFIG_IMPORT_FBX_READ_ANIMATIONS, true); + mSettings.readWeights = pImp->GetPropertyBool(AI_CONFIG_IMPORT_FBX_READ_WEIGHTS, true); + mSettings.strictMode = pImp->GetPropertyBool(AI_CONFIG_IMPORT_FBX_STRICT_MODE, false); + mSettings.preservePivots = pImp->GetPropertyBool(AI_CONFIG_IMPORT_FBX_PRESERVE_PIVOTS, true); + mSettings.optimizeEmptyAnimationCurves = pImp->GetPropertyBool(AI_CONFIG_IMPORT_FBX_OPTIMIZE_EMPTY_ANIMATION_CURVES, true); + mSettings.useLegacyEmbeddedTextureNaming = pImp->GetPropertyBool(AI_CONFIG_IMPORT_FBX_EMBEDDED_TEXTURES_LEGACY_NAMING, false); + mSettings.removeEmptyBones = pImp->GetPropertyBool(AI_CONFIG_IMPORT_REMOVE_EMPTY_BONES, true); + mSettings.convertToMeters = pImp->GetPropertyBool(AI_CONFIG_FBX_CONVERT_TO_M, false); + mSettings.useSkeleton = pImp->GetPropertyBool(AI_CONFIG_FBX_USE_SKELETON_BONE_CONTAINER, false); } // ------------------------------------------------------------------------------------------------ @@ -155,7 +153,7 @@ void FBXImporter::InternReadFile(const std::string &pFile, aiScene *pScene, IOSy contents[contents.size() - 1] = 0; const char *const begin = &*contents.begin(); - // broadphase tokenizing pass in which we identify the core + // broad-phase tokenized pass in which we identify the core // syntax elements of FBX (brackets, commas, key:value mappings) TokenList tokens; try { @@ -173,15 +171,14 @@ void FBXImporter::InternReadFile(const std::string &pFile, aiScene *pScene, IOSy Parser parser(tokens, is_binary); // take the raw parse-tree and convert it to a FBX DOM - Document doc(parser, settings); + Document doc(parser, mSettings); // convert the FBX DOM to aiScene - ConvertToAssimpScene(pScene, doc, settings.removeEmptyBones); + ConvertToAssimpScene(pScene, doc, mSettings.removeEmptyBones); // size relative to cm float size_relative_to_cm = doc.GlobalSettings().UnitScaleFactor(); - if (size_relative_to_cm == 0.0) - { + if (size_relative_to_cm == 0.0) { // BaseImporter later asserts that fileScale is non-zero. ThrowException("The UnitScaleFactor must be non-zero"); } diff --git a/code/AssetLib/FBX/FBXImporter.h b/code/AssetLib/FBX/FBXImporter.h index a212efe11..9acabaddd 100644 --- a/code/AssetLib/FBX/FBXImporter.h +++ b/code/AssetLib/FBX/FBXImporter.h @@ -69,13 +69,14 @@ typedef class basic_formatter, std::allocator // ------------------------------------------------------------------------------------------- class FBXImporter : public BaseImporter, public LogFunctions { public: + /// @brief The class constructor. FBXImporter(); - ~FBXImporter() override; - // -------------------- - bool CanRead(const std::string &pFile, - IOSystem *pIOHandler, - bool checkSig) const override; + /// @brief The class destructor, default implementation. + ~FBXImporter() override = default; + + /// @brief Will check the file for readability. + bool CanRead(const std::string &pFile, IOSystem *pIOHandler, bool checkSig) const override; protected: // -------------------- @@ -90,7 +91,7 @@ protected: IOSystem *pIOHandler) override; private: - FBX::ImportSettings settings; + FBX::ImportSettings mSettings; }; // !class FBXImporter } // end of namespace Assimp diff --git a/code/Common/SkeletonMeshBuilder.cpp b/code/Common/SkeletonMeshBuilder.cpp index 5ea30d9c7..d00b96d2d 100644 --- a/code/Common/SkeletonMeshBuilder.cpp +++ b/code/Common/SkeletonMeshBuilder.cpp @@ -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, @@ -97,13 +96,14 @@ void SkeletonMeshBuilder::CreateGeometry(const aiNode *pNode) { const aiMatrix4x4 &childTransform = pNode->mChildren[a]->mTransformation; aiVector3D childpos(childTransform.a4, childTransform.b4, childTransform.c4); ai_real distanceToChild = childpos.Length(); - if (distanceToChild < 0.0001) + if (distanceToChild < ai_epsilon) { continue; + } aiVector3D up = aiVector3D(childpos).Normalize(); - aiVector3D orth(1.0, 0.0, 0.0); - if (std::fabs(orth * up) > 0.99) + if (std::fabs(orth * up) > 0.99) { orth.Set(0.0, 1.0, 0.0); + } aiVector3D front = (up ^ orth).Normalize(); aiVector3D side = (front ^ up).Normalize(); @@ -183,8 +183,9 @@ void SkeletonMeshBuilder::CreateGeometry(const aiNode *pNode) { // add all the vertices to the bone's influences bone->mNumWeights = numVertices; bone->mWeights = new aiVertexWeight[numVertices]; - for (unsigned int a = 0; a < numVertices; a++) + for (unsigned int a = 0; a < numVertices; ++a) { bone->mWeights[a] = aiVertexWeight(vertexStartIndex + a, 1.0); + } // HACK: (thom) transform all vertices to the bone's local space. Should be done before adding // them to the array, but I'm tired now and I'm annoyed. @@ -194,8 +195,9 @@ void SkeletonMeshBuilder::CreateGeometry(const aiNode *pNode) { } // and finally recurse into the children list - for (unsigned int a = 0; a < pNode->mNumChildren; a++) + for (unsigned int a = 0; a < pNode->mNumChildren; ++a) { CreateGeometry(pNode->mChildren[a]); + } } // ------------------------------------------------------------------------------------------------ diff --git a/include/assimp/config.h.in b/include/assimp/config.h.in index bf0076572..1ed5a2114 100644 --- a/include/assimp/config.h.in +++ b/include/assimp/config.h.in @@ -691,6 +691,15 @@ enum aiComponent #define AI_CONFIG_FBX_CONVERT_TO_M \ "AI_CONFIG_FBX_CONVERT_TO_M" +// --------------------------------------------------------------------------- +/** @brief Will enable the skeleton structo to store bone data. + * + * This will decouple the bone coupling to the mesh. This feature is + * experimental. + */ +#define AI_CONFIG_FBX_USE_SKELETON_BONE_CONTAINER \ + "AI_CONFIG_FBX_USE_SKELETON_BONE_CONTAINER" + // --------------------------------------------------------------------------- /** @brief Set the vertex animation keyframe to be imported * diff --git a/include/assimp/mesh.h b/include/assimp/mesh.h index 87b9de262..cd334f786 100644 --- a/include/assimp/mesh.h +++ b/include/assimp/mesh.h @@ -120,7 +120,7 @@ extern "C" { * primitive are actually present in a mesh. The #aiProcess_SortByPType flag * executes a special post-processing algorithm which splits meshes with * *different* primitive types mixed up (e.g. lines and triangles) in several - * 'clean' submeshes. Furthermore there is a configuration option ( + * 'clean' sub-meshes. Furthermore there is a configuration option ( * #AI_CONFIG_PP_SBP_REMOVE) to force #aiProcess_SortByPType to remove * specific kinds of primitives from the imported scene, completely and forever. * In many cases you'll probably want to set this setting to @@ -269,12 +269,12 @@ struct aiBone { unsigned int mNumWeights; #ifndef ASSIMP_BUILD_NO_ARMATUREPOPULATE_PROCESS - // The bone armature node - used for skeleton conversion - // you must enable aiProcess_PopulateArmatureData to populate this + /// The bone armature node - used for skeleton conversion + /// you must enable aiProcess_PopulateArmatureData to populate this C_STRUCT aiNode *mArmature; - // The bone node in the scene - used for skeleton conversion - // you must enable aiProcess_PopulateArmatureData to populate this + /// The bone node in the scene - used for skeleton conversion + /// you must enable aiProcess_PopulateArmatureData to populate this C_STRUCT aiNode *mNode; #endif @@ -296,7 +296,7 @@ struct aiBone { #ifdef __cplusplus - //! Default constructor + /// @brief Default constructor aiBone() AI_NO_EXCEPT : mName(), mNumWeights(0), @@ -309,7 +309,7 @@ struct aiBone { // empty } - //! Copy constructor + /// @brief Copy constructor aiBone(const aiBone &other) : mName(other.mName), mNumWeights(other.mNumWeights), @@ -319,14 +319,27 @@ struct aiBone { #endif mWeights(nullptr), mOffsetMatrix(other.mOffsetMatrix) { - if (other.mWeights && other.mNumWeights) { - mWeights = new aiVertexWeight[mNumWeights]; - ::memcpy(mWeights, other.mWeights, mNumWeights * sizeof(aiVertexWeight)); + copyVertexWeights(other); + } + + void copyVertexWeights( const aiBone &other ) { + if (other.mWeights == nullptr || other.mNumWeights == 0) { + mWeights = nullptr; + mNumWeights = 0; + return; } + + mNumWeights = other.mNumWeights; + if (mWeights) { + delete[] mWeights; + } + + mWeights = new aiVertexWeight[mNumWeights]; + ::memcpy(mWeights, other.mWeights, mNumWeights * sizeof(aiVertexWeight)); } //! Assignment operator - aiBone &operator=(const aiBone &other) { + aiBone &operator = (const aiBone &other) { if (this == &other) { return *this; } @@ -334,21 +347,13 @@ struct aiBone { mName = other.mName; mNumWeights = other.mNumWeights; mOffsetMatrix = other.mOffsetMatrix; - - if (other.mWeights && other.mNumWeights) { - if (mWeights) { - delete[] mWeights; - } - - mWeights = new aiVertexWeight[mNumWeights]; - ::memcpy(mWeights, other.mWeights, mNumWeights * sizeof(aiVertexWeight)); - } + copyVertexWeights(other); return *this; } bool operator==(const aiBone &rhs) const { - if (mName != rhs.mName || mNumWeights != rhs.mNumWeights) { + if (mName != rhs.mName || mNumWeights != rhs.mNumWeights ) { return false; } @@ -937,17 +942,92 @@ struct aiMesh { #endif // __cplusplus }; -struct aiSkeleton { - C_STRUCT aiString mName; - unsigned int mNumWeights; +struct aiSkeletonBone { + /// The parent bone index, is -1 one if this bone represents the root bone. + int mParent; + +#ifndef ASSIMP_BUILD_NO_ARMATUREPOPULATE_PROCESS + /// The bone armature node - used for skeleton conversion + /// you must enable aiProcess_PopulateArmatureData to populate this + C_STRUCT aiNode *mArmature; + + /// The bone node in the scene - used for skeleton conversion + /// you must enable aiProcess_PopulateArmatureData to populate this + C_STRUCT aiNode *mNode; + +#endif + /// @brief The number of weights + unsigned int mNumnWeights; + + /// The influence weights of this bone, by vertex index. C_STRUCT aiVertexWeight *mWeights; + /** Matrix that transforms from bone space to mesh space in bind pose. + * + * This matrix describes the position of the mesh + * in the local space of this bone when the skeleton was bound. + * Thus it can be used directly to determine a desired vertex position, + * given the world-space transform of the bone when animated, + * and the position of the vertex in mesh space. + * + * It is sometimes called an inverse-bind matrix, + * or inverse bind pose matrix. + */ + C_STRUCT aiMatrix4x4 mOffsetMatrix; + + /// Matrix that transforms the locale bone in bind pose. + C_STRUCT aiMatrix4x4 mLocalMatrix; + #ifdef __cplusplus - aiSkeleton() AI_NO_EXCEPT : mName(), mNumWeights(0), mWeights(nullptr) { + aiSkeletonBone() : + mParent(-1), + mArmature(nullptr), + mNode(nullptr), + mNumnWeights(0), + mWeights(nullptr), + mOffsetMatrix(), + mLocalMatrix() { // empty } - ~aiSkeleton() { + ~aiSkeletonBone() { + delete[] mWeights; + mWeights = nullptr; + } +#endif // __cplusplus +}; +/** + * @brief + */ +struct aiSkeleton { + /** + * + */ + C_STRUCT aiString mName; + + /** + * + */ + unsigned int mNumBones; + + /** + * + */ + C_STRUCT aiSkeletonBone *mBones; + +#ifdef __cplusplus + /** + * + */ + aiSkeleton() AI_NO_EXCEPT : mName(), mNumBones(0), mBones(nullptr) { + // empty + } + + /** + * + */ + ~aiSkeleton() { + delete[] mBones; } #endif // __cplusplus }; diff --git a/include/assimp/scene.h b/include/assimp/scene.h index f4c6d7960..2dac27da2 100644 --- a/include/assimp/scene.h +++ b/include/assimp/scene.h @@ -343,6 +343,16 @@ struct aiScene */ C_STRUCT aiString mName; + /** + * + */ + unsigned int mNumSkeletons; + + /** + * + */ + C_STRUCT aiSkeleton **mSkeletons; + #ifdef __cplusplus //! Default constructor - set everything to 0/nullptr @@ -383,6 +393,10 @@ struct aiScene return mAnimations != nullptr && mNumAnimations > 0; } + bool hasSkeletons() const { + return mSkeletons != nullptr && mNumSkeletons > 0; + } + //! Returns a short filename from a full path static const char* GetShortFilename(const char* filename) { const char* lastSlash = strrchr(filename, '/'); From 37be87b0bda5eba3f9b1967f3aa29a857da8b0cb Mon Sep 17 00:00:00 2001 From: Kim Kulling Date: Tue, 19 Apr 2022 23:37:16 +0200 Subject: [PATCH 03/17] Some minor findings --- code/AssetLib/FBX/FBXCommon.h | 3 ++- code/AssetLib/FBX/FBXCompileConfig.h | 1 - code/AssetLib/FBX/FBXConverter.cpp | 33 +++++++++++++++++---------- code/AssetLib/FBX/FBXDeformer.cpp | 7 ++---- code/AssetLib/FBX/FBXDocumentUtil.h | 12 ++++------ code/AssetLib/FBX/FBXExporter.cpp | 2 +- code/AssetLib/FBX/FBXMeshGeometry.cpp | 24 ++++--------------- code/AssetLib/FBX/FBXMeshGeometry.h | 20 ++++++++-------- code/AssetLib/FBX/FBXParser.cpp | 12 ---------- code/AssetLib/FBX/FBXParser.h | 4 ++-- 10 files changed, 48 insertions(+), 70 deletions(-) diff --git a/code/AssetLib/FBX/FBXCommon.h b/code/AssetLib/FBX/FBXCommon.h index ec7459c9e..c592c5649 100644 --- a/code/AssetLib/FBX/FBXCommon.h +++ b/code/AssetLib/FBX/FBXCommon.h @@ -50,7 +50,8 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. namespace Assimp { namespace FBX { -const std::string NULL_RECORD = { // 25 null bytes in 64-bit and 13 null bytes in 32-bit +static constexpr size_t NumNullRecords = 25; +const char NULL_RECORD[NumNullRecords] = { // 25 null bytes in 64-bit and 13 null bytes in 32-bit '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0' }; // who knows why, it looks like two integers 32/64 bit (compressed and uncompressed sizes?) + 1 byte (might be compression type?) diff --git a/code/AssetLib/FBX/FBXCompileConfig.h b/code/AssetLib/FBX/FBXCompileConfig.h index 75787d303..927a3c5f6 100644 --- a/code/AssetLib/FBX/FBXCompileConfig.h +++ b/code/AssetLib/FBX/FBXCompileConfig.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, diff --git a/code/AssetLib/FBX/FBXConverter.cpp b/code/AssetLib/FBX/FBXConverter.cpp index c79d43cbf..e8cfe1dc0 100644 --- a/code/AssetLib/FBX/FBXConverter.cpp +++ b/code/AssetLib/FBX/FBXConverter.cpp @@ -1439,9 +1439,20 @@ void ConvertWeightsToSkeleton(const MeshGeometry &geo, const aiMatrix4x4 &absolu std::vector *outputVertStartIndices) { ai_assert(geo.DeformerSkin() != nullptr); + aiSkeleton *skeleton = new aiSkeleton; const Skin &sk = *geo.DeformerSkin(); - for (auto &cluster : sk.Clusters()) { - cluster->Transform(); + try { + for (auto &cluster : sk.Clusters()) { + const WeightIndexArray &indices = cluster->GetIndices(); + const MatIndexArray &mats = geo.GetMaterialIndices(); + + aiMatrix4x4 transform = cluster->Transform(); + for (WeightIndexArray::value_type index : indices) { + unsigned int count = 0; + const unsigned int *out_idx = geo.ToOutputVertexIndex(index, count); + } + } + } catch (...) { } } @@ -1451,14 +1462,11 @@ void FBXConverter::ConvertWeights(aiMesh *out, const MeshGeometry &geo, std::vector *outputVertStartIndices) { ai_assert(geo.DeformerSkin()); - std::vector out_indices; - std::vector index_out_indices; - std::vector count_out_indices; + std::vector out_indices, index_out_indices, count_out_indices; const Skin &sk = *geo.DeformerSkin(); - std::vector bones; - + std::vector bones; const bool no_mat_check = materialIndex == NO_MATERIAL_SEPARATION; ai_assert(no_mat_check || outputVertStartIndices); @@ -1539,12 +1547,13 @@ void FBXConverter::ConvertWeights(aiMesh *out, const MeshGeometry &geo, } } -void FBXConverter::ConvertCluster(std::vector &local_mesh_bones, const Cluster *cl, +void FBXConverter::ConvertCluster(std::vector &local_mesh_bones, const Cluster *cluster, std::vector &out_indices, std::vector &index_out_indices, std::vector &count_out_indices, const aiMatrix4x4 &absolute_transform, aiNode *) { - ai_assert(cl); // make sure cluster valid - std::string deformer_name = cl->TargetNode()->Name(); + ai_assert(cluster != nullptr); // make sure cluster valid + + std::string deformer_name = cluster->TargetNode()->Name(); aiString bone_name = aiString(FixNodeName(deformer_name)); aiBone *bone = nullptr; @@ -1558,7 +1567,7 @@ void FBXConverter::ConvertCluster(std::vector &local_mesh_bones, const bone->mName = bone_name; // store local transform link for post processing - bone->mOffsetMatrix = cl->TransformLink(); + bone->mOffsetMatrix = cluster->TransformLink(); bone->mOffsetMatrix.Inverse(); aiMatrix4x4 matrix = (aiMatrix4x4)absolute_transform; @@ -1575,7 +1584,7 @@ void FBXConverter::ConvertCluster(std::vector &local_mesh_bones, const cursor = bone->mWeights = new aiVertexWeight[out_indices.size()]; const size_t no_index_sentinel = std::numeric_limits::max(); - const WeightArray &weights = cl->GetWeights(); + const WeightArray &weights = cluster->GetWeights(); const size_t c = index_out_indices.size(); for (size_t i = 0; i < c; ++i) { diff --git a/code/AssetLib/FBX/FBXDeformer.cpp b/code/AssetLib/FBX/FBXDeformer.cpp index ba245ed6d..ed6330666 100644 --- a/code/AssetLib/FBX/FBXDeformer.cpp +++ b/code/AssetLib/FBX/FBXDeformer.cpp @@ -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, @@ -58,16 +57,14 @@ namespace FBX { using namespace Util; // ------------------------------------------------------------------------------------------------ -Deformer::Deformer(uint64_t id, const Element& element, const Document& doc, const std::string& name) - : Object(id,element,name) -{ +Deformer::Deformer(uint64_t id, const Element& element, const Document& doc, const std::string& name) : + Object(id,element,name) { const Scope& sc = GetRequiredScope(element); const std::string& classname = ParseTokenAsString(GetRequiredToken(element,2)); props = GetPropertyTable(doc,"Deformer.Fbx" + classname,element,sc,true); } - // ------------------------------------------------------------------------------------------------ Deformer::~Deformer() { diff --git a/code/AssetLib/FBX/FBXDocumentUtil.h b/code/AssetLib/FBX/FBXDocumentUtil.h index 2d76ee031..d32c12e1a 100644 --- a/code/AssetLib/FBX/FBXDocumentUtil.h +++ b/code/AssetLib/FBX/FBXDocumentUtil.h @@ -74,13 +74,11 @@ std::shared_ptr GetPropertyTable(const Document& doc, // ------------------------------------------------------------------------------------------------ template -inline -const T* ProcessSimpleConnection(const Connection& con, - bool is_object_property_conn, - const char* name, - const Element& element, - const char** propNameOut = nullptr) -{ +inline const T* ProcessSimpleConnection(const Connection& con, + bool is_object_property_conn, + const char* name, + const Element& element, + const char** propNameOut = nullptr) { if (is_object_property_conn && !con.PropertyName().length()) { DOMWarning("expected incoming " + std::string(name) + " link to be an object-object connection, ignoring", diff --git a/code/AssetLib/FBX/FBXExporter.cpp b/code/AssetLib/FBX/FBXExporter.cpp index 695672883..563ac68f0 100644 --- a/code/AssetLib/FBX/FBXExporter.cpp +++ b/code/AssetLib/FBX/FBXExporter.cpp @@ -255,7 +255,7 @@ void FBXExporter::WriteBinaryHeader() void FBXExporter::WriteBinaryFooter() { - outfile->Write(NULL_RECORD.c_str(), NULL_RECORD.size(), 1); + outfile->Write(NULL_RECORD, NumNullRecords, 1); outfile->Write(GENERIC_FOOTID.c_str(), GENERIC_FOOTID.size(), 1); diff --git a/code/AssetLib/FBX/FBXMeshGeometry.cpp b/code/AssetLib/FBX/FBXMeshGeometry.cpp index 1f92fa1a7..a0fb0e57e 100644 --- a/code/AssetLib/FBX/FBXMeshGeometry.cpp +++ b/code/AssetLib/FBX/FBXMeshGeometry.cpp @@ -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, @@ -54,18 +53,15 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "FBXImportSettings.h" #include "FBXDocumentUtil.h" - namespace Assimp { namespace FBX { using namespace Util; // ------------------------------------------------------------------------------------------------ -Geometry::Geometry(uint64_t id, const Element& element, const std::string& name, const Document& doc) - : Object(id, element, name) - , skin() -{ - const std::vector& conns = doc.GetConnectionsByDestinationSequenced(ID(),"Deformer"); +Geometry::Geometry(uint64_t id, const Element& element, const std::string& name, const Document& doc) : + Object(id, element, name), skin() { + const std::vector &conns = doc.GetConnectionsByDestinationSequenced(ID(),"Deformer"); for(const Connection* con : conns) { const Skin* const sk = ProcessSimpleConnection(*con, false, "Skin -> Geometry", element); if(sk) { @@ -78,12 +74,6 @@ Geometry::Geometry(uint64_t id, const Element& element, const std::string& name, } } -// ------------------------------------------------------------------------------------------------ -Geometry::~Geometry() -{ - // empty -} - // ------------------------------------------------------------------------------------------------ const std::vector& Geometry::GetBlendShapes() const { return blendShapes; @@ -183,18 +173,12 @@ MeshGeometry::MeshGeometry(uint64_t id, const Element& element, const std::strin if(doc.Settings().readAllLayers || index == 0) { const Scope& layer = GetRequiredScope(*(*it).second); ReadLayer(layer); - } - else { + } else { FBXImporter::LogWarn("ignoring additional geometry layers"); } } } -// ------------------------------------------------------------------------------------------------ -MeshGeometry::~MeshGeometry() { - // empty -} - // ------------------------------------------------------------------------------------------------ const std::vector& MeshGeometry::GetVertices() const { return m_vertices; diff --git a/code/AssetLib/FBX/FBXMeshGeometry.h b/code/AssetLib/FBX/FBXMeshGeometry.h index 3cca38aa5..3eb279bed 100644 --- a/code/AssetLib/FBX/FBXMeshGeometry.h +++ b/code/AssetLib/FBX/FBXMeshGeometry.h @@ -55,22 +55,25 @@ namespace FBX { /** * DOM base class for all kinds of FBX geometry */ -class Geometry : public Object -{ +class Geometry : public Object { public: + /// @brief The class constructor with all parameters. + /// @param id The id. + /// @param element + /// @param name + /// @param doc Geometry( uint64_t id, const Element& element, const std::string& name, const Document& doc ); - virtual ~Geometry(); + virtual ~Geometry() = default; - /** Get the Skin attached to this geometry or nullptr */ + /// Get the Skin attached to this geometry or nullptr const Skin* DeformerSkin() const; - /** Get the BlendShape attached to this geometry or nullptr */ + /// Get the BlendShape attached to this geometry or nullptr const std::vector& GetBlendShapes() const; private: const Skin* skin; std::vector blendShapes; - }; typedef std::vector MatIndexArray; @@ -79,14 +82,13 @@ typedef std::vector MatIndexArray; /** * DOM class for FBX geometry of type "Mesh" */ -class MeshGeometry : public Geometry -{ +class MeshGeometry : public Geometry { public: /** The class constructor */ MeshGeometry( uint64_t id, const Element& element, const std::string& name, const Document& doc ); /** The class destructor */ - virtual ~MeshGeometry(); + virtual ~MeshGeometry() = default; /** Get a list of all vertex points, non-unique*/ const std::vector& GetVertices() const; diff --git a/code/AssetLib/FBX/FBXParser.cpp b/code/AssetLib/FBX/FBXParser.cpp index e20377a3c..7cb7ae739 100644 --- a/code/AssetLib/FBX/FBXParser.cpp +++ b/code/AssetLib/FBX/FBXParser.cpp @@ -162,12 +162,6 @@ Element::Element(const Token& key_token, Parser& parser) : key_token(key_token) while(n->Type() != TokenType_KEY && n->Type() != TokenType_CLOSE_BRACKET); } -// ------------------------------------------------------------------------------------------------ -Element::~Element() -{ - // no need to delete tokens, they are owned by the parser -} - // ------------------------------------------------------------------------------------------------ Scope::Scope(Parser& parser,bool topLevel) { @@ -226,12 +220,6 @@ Parser::Parser (const TokenList& tokens, bool is_binary) root.reset(new Scope(*this,true)); } -// ------------------------------------------------------------------------------------------------ -Parser::~Parser() -{ - // empty -} - // ------------------------------------------------------------------------------------------------ TokenPtr Parser::AdvanceToNextToken() { diff --git a/code/AssetLib/FBX/FBXParser.h b/code/AssetLib/FBX/FBXParser.h index 314481e42..6aeedb211 100644 --- a/code/AssetLib/FBX/FBXParser.h +++ b/code/AssetLib/FBX/FBXParser.h @@ -87,7 +87,7 @@ class Element { public: Element(const Token& key_token, Parser& parser); - ~Element(); + ~Element() = default; const Scope* Compound() const { return compound.get(); @@ -160,7 +160,7 @@ public: /** Parse given a token list. Does not take ownership of the tokens - * the objects must persist during the entire parser lifetime */ Parser (const TokenList& tokens,bool is_binary); - ~Parser(); + ~Parser() = default; const Scope& GetRootScope() const { return *root.get(); From 6cdd1d3cc6d57787c0fb916b7635f200df5af947 Mon Sep 17 00:00:00 2001 From: Kim Kulling Date: Thu, 28 Apr 2022 21:12:26 +0200 Subject: [PATCH 04/17] Generate container for skeleton during FBX-Import --- code/AssetLib/FBX/FBXAnimation.cpp | 21 ----- code/AssetLib/FBX/FBXConverter.cpp | 56 +++++++------ code/AssetLib/FBX/FBXConverter.h | 19 ++++- code/AssetLib/FBX/FBXDocument.cpp | 50 +++--------- code/AssetLib/FBX/FBXDocument.h | 26 +++--- code/AssetLib/FBX/FBXMeshGeometry.h | 1 + code/AssetLib/FBX/FBXNodeAttribute.cpp | 105 +++++++------------------ code/AssetLib/FBX/FBXParser.cpp | 4 +- include/assimp/mesh.h | 5 ++ 9 files changed, 109 insertions(+), 178 deletions(-) diff --git a/code/AssetLib/FBX/FBXAnimation.cpp b/code/AssetLib/FBX/FBXAnimation.cpp index 2fa3b7b05..af92ebe51 100644 --- a/code/AssetLib/FBX/FBXAnimation.cpp +++ b/code/AssetLib/FBX/FBXAnimation.cpp @@ -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, @@ -87,11 +86,6 @@ AnimationCurve::AnimationCurve(uint64_t id, const Element &element, const std::s } } -// ------------------------------------------------------------------------------------------------ -AnimationCurve::~AnimationCurve() { - // empty -} - // ------------------------------------------------------------------------------------------------ AnimationCurveNode::AnimationCurveNode(uint64_t id, const Element &element, const std::string &name, const Document &doc, const char *const *target_prop_whitelist /*= nullptr*/, @@ -147,11 +141,6 @@ AnimationCurveNode::AnimationCurveNode(uint64_t id, const Element &element, cons props = GetPropertyTable(doc, "AnimationCurveNode.FbxAnimCurveNode", element, sc, false); } -// ------------------------------------------------------------------------------------------------ -AnimationCurveNode::~AnimationCurveNode() { - // empty -} - // ------------------------------------------------------------------------------------------------ const AnimationCurveMap &AnimationCurveNode::Curves() const { if (curves.empty()) { @@ -193,11 +182,6 @@ AnimationLayer::AnimationLayer(uint64_t id, const Element &element, const std::s props = GetPropertyTable(doc, "AnimationLayer.FbxAnimLayer", element, sc, true); } -// ------------------------------------------------------------------------------------------------ -AnimationLayer::~AnimationLayer() { - // empty -} - // ------------------------------------------------------------------------------------------------ AnimationCurveNodeList AnimationLayer::Nodes(const char *const *target_prop_whitelist /*= nullptr*/, size_t whitelist_size /*= 0*/) const { @@ -279,11 +263,6 @@ AnimationStack::AnimationStack(uint64_t id, const Element &element, const std::s } } -// ------------------------------------------------------------------------------------------------ -AnimationStack::~AnimationStack() { - // empty -} - } // namespace FBX } // namespace Assimp diff --git a/code/AssetLib/FBX/FBXConverter.cpp b/code/AssetLib/FBX/FBXConverter.cpp index e8cfe1dc0..34a97c889 100644 --- a/code/AssetLib/FBX/FBXConverter.cpp +++ b/code/AssetLib/FBX/FBXConverter.cpp @@ -907,7 +907,6 @@ void FBXConverter::ConvertModel(const Model &model, aiNode *parent, aiNode *root meshes.reserve(geos.size()); for (const Geometry *geo : geos) { - const MeshGeometry *const mesh = dynamic_cast(geo); const LineGeometry *const line = dynamic_cast(geo); if (mesh) { @@ -1151,8 +1150,10 @@ unsigned int FBXConverter::ConvertMeshSingleMaterial(const MeshGeometry &mesh, c ConvertMaterialForMesh(out_mesh, model, mesh, mindices[0]); } - if (doc.Settings().readWeights && mesh.DeformerSkin() != nullptr) { + if (doc.Settings().readWeights && mesh.DeformerSkin() != nullptr && !doc.Settings().useSkeleton) { ConvertWeights(out_mesh, mesh, absolute_transform, parent, NO_MATERIAL_SEPARATION, nullptr); + } else if (doc.Settings().readWeights && mesh.DeformerSkin() != nullptr && doc.Settings().useSkeleton) { + ConvertWeightsToSkeleton(out_mesh, mesh, absolute_transform, parent, NO_MATERIAL_SEPARATION, nullptr); } std::vector animMeshes; @@ -1435,25 +1436,35 @@ unsigned int FBXConverter::ConvertMeshMultiMaterial(const MeshGeometry &mesh, co return static_cast(mMeshes.size() - 1); } -void ConvertWeightsToSkeleton(const MeshGeometry &geo, const aiMatrix4x4 &absolute_transform, aiNode *parent, unsigned int materialIndex, - std::vector *outputVertStartIndices) { - ai_assert(geo.DeformerSkin() != nullptr); +static void copyBoneToSkeletonBone(aiMesh *mesh, aiBone *bone, aiSkeletonBone *skeletonBone ) { + skeletonBone->mNumnWeights = bone->mNumWeights; + skeletonBone->mWeights = bone->mWeights; + skeletonBone->mOffsetMatrix = bone->mOffsetMatrix; + skeletonBone->mMeshId = mesh; + skeletonBone->mNode = bone->mNode; + skeletonBone->mParent = -1; +} - aiSkeleton *skeleton = new aiSkeleton; - const Skin &sk = *geo.DeformerSkin(); - try { - for (auto &cluster : sk.Clusters()) { - const WeightIndexArray &indices = cluster->GetIndices(); - const MatIndexArray &mats = geo.GetMaterialIndices(); +void FBXConverter::ConvertWeightsToSkeleton(aiMesh *out, const MeshGeometry &geo, const aiMatrix4x4 &absolute_transform, aiNode *parent, unsigned int materialIndex, + std::vector *outputVertStartIndices, SkeletonBoneContainer &skeletonContainer) { - aiMatrix4x4 transform = cluster->Transform(); - for (WeightIndexArray::value_type index : indices) { - unsigned int count = 0; - const unsigned int *out_idx = geo.ToOutputVertexIndex(index, count); - } - } - } catch (...) { + if (skeletonContainer.SkeletonBoneToMeshLookup.find(out) != skeletonContainer.SkeletonBoneToMeshLookup.end()) { + return; } + + ConvertWeights(out, geo, absolute_transform, parent, materialIndex, outputVertStartIndices); + skeletonContainer.MeshArray.emplace_back(out); + SkeletonBoneArray *ba = new SkeletonBoneArray; + for (size_t i = 0; i < out->mNumBones; ++i) { + aiBone *bone = out->mBones[i]; + if (bone == nullptr) { + continue; + } + aiSkeletonBone *skeletonBone = new aiSkeletonBone; + copyBoneToSkeletonBone(out, bone, skeletonBone); + ba->emplace_back(skeletonBone); + } + skeletonContainer.SkeletonBoneToMeshLookup[out] = ba; } void FBXConverter::ConvertWeights(aiMesh *out, const MeshGeometry &geo, @@ -1539,12 +1550,11 @@ void FBXConverter::ConvertWeights(aiMesh *out, const MeshGeometry &geo, out->mBones = nullptr; out->mNumBones = 0; return; - } else { - out->mBones = new aiBone *[bones.size()](); - out->mNumBones = static_cast(bones.size()); + } - std::swap_ranges(bones.begin(), bones.end(), out->mBones); - } + out->mBones = new aiBone *[bones.size()](); + out->mNumBones = static_cast(bones.size()); + std::swap_ranges(bones.begin(), bones.end(), out->mBones); } void FBXConverter::ConvertCluster(std::vector &local_mesh_bones, const Cluster *cluster, diff --git a/code/AssetLib/FBX/FBXConverter.h b/code/AssetLib/FBX/FBXConverter.h index becfdb3e8..a393f9ba0 100644 --- a/code/AssetLib/FBX/FBXConverter.h +++ b/code/AssetLib/FBX/FBXConverter.h @@ -75,7 +75,18 @@ typedef std::map morphAnimData; namespace Assimp { namespace FBX { +class MeshGeometry; + +using SkeletonBoneArray = std::vector; +using SkeletonBoneToMesh = std::map; + +struct SkeletonBoneContainer { + std::vector MeshArray; + SkeletonBoneToMesh SkeletonBoneToMeshLookup; +}; + class Document; + /** * Convert a FBX #Document to #aiScene * @param out Empty scene to be populated @@ -224,6 +235,11 @@ private: aiNode *parent = nullptr, unsigned int materialIndex = NO_MATERIAL_SEPARATION, std::vector *outputVertStartIndices = nullptr); + // ------------------------------------------------------------------------------------------------ + void ConvertWeightsToSkeleton(aiMesh *out, const MeshGeometry &geo, const aiMatrix4x4 &absolute_transform, + aiNode *parent, unsigned int materialIndex, std::vector *outputVertStartIndices, + SkeletonBoneContainer &skeletonContainer); + // ------------------------------------------------------------------------------------------------ void ConvertCluster(std::vector &local_mesh_bones, const Cluster *cl, std::vector &out_indices, std::vector &index_out_indices, @@ -301,7 +317,8 @@ private: void ConvertAnimationStack(const AnimationStack& st); // ------------------------------------------------------------------------------------------------ - void ProcessMorphAnimDatas(std::map* morphAnimDatas, const BlendShapeChannel* bsc, const AnimationCurveNode* node); + void ProcessMorphAnimDatas(std::map* morphAnimDatas, + const BlendShapeChannel* bsc, const AnimationCurveNode* node); // ------------------------------------------------------------------------------------------------ void GenerateNodeAnimations(std::vector& node_anims, diff --git a/code/AssetLib/FBX/FBXDocument.cpp b/code/AssetLib/FBX/FBXDocument.cpp index f228b1749..5bbc5a09c 100644 --- a/code/AssetLib/FBX/FBXDocument.cpp +++ b/code/AssetLib/FBX/FBXDocument.cpp @@ -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, @@ -68,23 +67,13 @@ namespace FBX { using namespace Util; // ------------------------------------------------------------------------------------------------ -LazyObject::LazyObject(uint64_t id, const Element& element, const Document& doc) -: doc(doc) -, element(element) -, id(id) -, flags() { +LazyObject::LazyObject(uint64_t id, const Element& element, const Document& doc) : + doc(doc), element(element), id(id), flags() { // empty } // ------------------------------------------------------------------------------------------------ -LazyObject::~LazyObject() -{ - // empty -} - -// ------------------------------------------------------------------------------------------------ -const Object* LazyObject::Get(bool dieOnError) -{ +const Object* LazyObject::Get(bool dieOnError) { if(IsBeingConstructed() || FailedToConstruct()) { return nullptr; } @@ -234,17 +223,8 @@ const Object* LazyObject::Get(bool dieOnError) } // ------------------------------------------------------------------------------------------------ -Object::Object(uint64_t id, const Element& element, const std::string& name) -: element(element) -, name(name) -, id(id) -{ - // empty -} - -// ------------------------------------------------------------------------------------------------ -Object::~Object() -{ +Object::Object(uint64_t id, const Element& element, const std::string& name) : + element(element), name(name), id(id) { // empty } @@ -254,12 +234,6 @@ FileGlobalSettings::FileGlobalSettings(const Document &doc, std::shared_ptr Document::GetConnectionsSequenced(uint64_t id, co // ------------------------------------------------------------------------------------------------ std::vector Document::GetConnectionsSequenced(uint64_t id, bool is_src, - const ConnectionMap& conns, - const char* const* classnames, - size_t count) const - -{ + const ConnectionMap& conns, + const char* const* classnames, + size_t count) const { ai_assert(classnames); ai_assert( count != 0 ); ai_assert( count <= MAX_CLASSNAMES); - size_t lengths[MAX_CLASSNAMES]; + size_t lengths[MAX_CLASSNAMES] = {}; const size_t c = count; for (size_t i = 0; i < c; ++i) { @@ -657,9 +629,7 @@ std::vector Document::GetConnectionsByDestinationSequenced(ui // ------------------------------------------------------------------------------------------------ std::vector Document::GetConnectionsByDestinationSequenced(uint64_t dest, - const char* const* classnames, size_t count) const - -{ + const char* const* classnames, size_t count) const { return GetConnectionsSequenced(dest, false, ConnectionsByDestination(),classnames, count); } diff --git a/code/AssetLib/FBX/FBXDocument.h b/code/AssetLib/FBX/FBXDocument.h index bac7e7769..245874b5c 100644 --- a/code/AssetLib/FBX/FBXDocument.h +++ b/code/AssetLib/FBX/FBXDocument.h @@ -89,7 +89,7 @@ class LazyObject { public: LazyObject(uint64_t id, const Element& element, const Document& doc); - ~LazyObject(); + ~LazyObject() = default; const Object* Get(bool dieOnError = false); @@ -139,7 +139,7 @@ class Object { public: Object(uint64_t id, const Element& element, const std::string& name); - virtual ~Object(); + virtual ~Object() = default; const Element& SourceElement() const { return element; @@ -165,7 +165,7 @@ class NodeAttribute : public Object { public: NodeAttribute(uint64_t id, const Element& element, const Document& doc, const std::string& name); - virtual ~NodeAttribute(); + virtual ~NodeAttribute() = default; const PropertyTable& Props() const { ai_assert(props.get()); @@ -181,7 +181,7 @@ class CameraSwitcher : public NodeAttribute { public: CameraSwitcher(uint64_t id, const Element& element, const Document& doc, const std::string& name); - virtual ~CameraSwitcher(); + virtual ~CameraSwitcher() = default; int CameraID() const { return cameraId; @@ -226,7 +226,7 @@ class Camera : public NodeAttribute { public: Camera(uint64_t id, const Element& element, const Document& doc, const std::string& name); - virtual ~Camera(); + virtual ~Camera() = default; fbx_simple_property(Position, aiVector3D, aiVector3D(0,0,0)) fbx_simple_property(UpVector, aiVector3D, aiVector3D(0,1,0)) @@ -251,21 +251,21 @@ public: class Null : public NodeAttribute { public: Null(uint64_t id, const Element& element, const Document& doc, const std::string& name); - virtual ~Null(); + virtual ~Null() = default; }; /** DOM base class for FBX limb node markers attached to a node */ class LimbNode : public NodeAttribute { public: LimbNode(uint64_t id, const Element& element, const Document& doc, const std::string& name); - virtual ~LimbNode(); + virtual ~LimbNode() = default; }; /** DOM base class for FBX lights attached to a node */ class Light : public NodeAttribute { public: Light(uint64_t id, const Element& element, const Document& doc, const std::string& name); - virtual ~Light(); + virtual ~Light() = default; enum Type { @@ -697,7 +697,7 @@ typedef std::vector KeyValueList; class AnimationCurve : public Object { public: AnimationCurve(uint64_t id, const Element& element, const std::string& name, const Document& doc); - virtual ~AnimationCurve(); + virtual ~AnimationCurve() = default; /** get list of keyframe positions (time). * Invariant: |GetKeys()| > 0 */ @@ -738,7 +738,7 @@ public: AnimationCurveNode(uint64_t id, const Element& element, const std::string& name, const Document& doc, const char *const *target_prop_whitelist = nullptr, size_t whitelist_size = 0); - virtual ~AnimationCurveNode(); + virtual ~AnimationCurveNode() = default; const PropertyTable& Props() const { ai_assert(props.get()); @@ -783,7 +783,7 @@ typedef std::vector AnimationCurveNodeList; class AnimationLayer : public Object { public: AnimationLayer(uint64_t id, const Element& element, const std::string& name, const Document& doc); - virtual ~AnimationLayer(); + virtual ~AnimationLayer() = default; const PropertyTable& Props() const { ai_assert(props.get()); @@ -806,7 +806,7 @@ typedef std::vector AnimationLayerList; class AnimationStack : public Object { public: AnimationStack(uint64_t id, const Element& element, const std::string& name, const Document& doc); - virtual ~AnimationStack(); + virtual ~AnimationStack() = default; fbx_simple_property(LocalStart, int64_t, 0L) fbx_simple_property(LocalStop, int64_t, 0L) @@ -1022,7 +1022,7 @@ class FileGlobalSettings { public: FileGlobalSettings(const Document& doc, std::shared_ptr props); - ~FileGlobalSettings(); + ~FileGlobalSettings() = default; const PropertyTable& Props() const { ai_assert(props.get()); diff --git a/code/AssetLib/FBX/FBXMeshGeometry.h b/code/AssetLib/FBX/FBXMeshGeometry.h index 3eb279bed..ad24877e4 100644 --- a/code/AssetLib/FBX/FBXMeshGeometry.h +++ b/code/AssetLib/FBX/FBXMeshGeometry.h @@ -132,6 +132,7 @@ public: /** Determine the face to which a particular output vertex index belongs. * This mapping is always unique. */ unsigned int FaceForVertexIndex( unsigned int in_index ) const; + private: void ReadLayer( const Scope& layer ); void ReadLayerElement( const Scope& layerElement ); diff --git a/code/AssetLib/FBX/FBXNodeAttribute.cpp b/code/AssetLib/FBX/FBXNodeAttribute.cpp index a144f417a..34fcdcf77 100644 --- a/code/AssetLib/FBX/FBXNodeAttribute.cpp +++ b/code/AssetLib/FBX/FBXNodeAttribute.cpp @@ -57,114 +57,65 @@ namespace FBX { using namespace Util; // ------------------------------------------------------------------------------------------------ -NodeAttribute::NodeAttribute(uint64_t id, const Element& element, const Document& doc, const std::string& name) -: Object(id,element,name) -, props() -{ - const Scope& sc = GetRequiredScope(element); +NodeAttribute::NodeAttribute(uint64_t id, const Element &element, const Document &doc, const std::string &name) : + Object(id, element, name), props() { + const Scope &sc = GetRequiredScope(element); - const std::string& classname = ParseTokenAsString(GetRequiredToken(element,2)); + const std::string &classname = ParseTokenAsString(GetRequiredToken(element, 2)); // hack on the deriving type but Null/LimbNode attributes are the only case in which // the property table is by design absent and no warning should be generated // for it. const bool is_null_or_limb = !strcmp(classname.c_str(), "Null") || !strcmp(classname.c_str(), "LimbNode"); - props = GetPropertyTable(doc,"NodeAttribute.Fbx" + classname,element,sc, is_null_or_limb); + props = GetPropertyTable(doc, "NodeAttribute.Fbx" + classname, element, sc, is_null_or_limb); } - // ------------------------------------------------------------------------------------------------ -NodeAttribute::~NodeAttribute() -{ - // empty -} +CameraSwitcher::CameraSwitcher(uint64_t id, const Element &element, const Document &doc, const std::string &name) : + NodeAttribute(id, element, doc, name) { + const Scope &sc = GetRequiredScope(element); + const Element *const CameraId = sc["CameraId"]; + const Element *const CameraName = sc["CameraName"]; + const Element *const CameraIndexName = sc["CameraIndexName"]; - -// ------------------------------------------------------------------------------------------------ -CameraSwitcher::CameraSwitcher(uint64_t id, const Element& element, const Document& doc, const std::string& name) - : NodeAttribute(id,element,doc,name) -{ - const Scope& sc = GetRequiredScope(element); - const Element* const CameraId = sc["CameraId"]; - const Element* const CameraName = sc["CameraName"]; - const Element* const CameraIndexName = sc["CameraIndexName"]; - - if(CameraId) { - cameraId = ParseTokenAsInt(GetRequiredToken(*CameraId,0)); + if (CameraId) { + cameraId = ParseTokenAsInt(GetRequiredToken(*CameraId, 0)); } - if(CameraName) { - cameraName = GetRequiredToken(*CameraName,0).StringContents(); + if (CameraName) { + cameraName = GetRequiredToken(*CameraName, 0).StringContents(); } - if(CameraIndexName && CameraIndexName->Tokens().size()) { - cameraIndexName = GetRequiredToken(*CameraIndexName,0).StringContents(); + if (CameraIndexName && CameraIndexName->Tokens().size()) { + cameraIndexName = GetRequiredToken(*CameraIndexName, 0).StringContents(); } } // ------------------------------------------------------------------------------------------------ -CameraSwitcher::~CameraSwitcher() -{ +Camera::Camera(uint64_t id, const Element &element, const Document &doc, const std::string &name) : + NodeAttribute(id, element, doc, name) { // empty } // ------------------------------------------------------------------------------------------------ -Camera::Camera(uint64_t id, const Element& element, const Document& doc, const std::string& name) -: NodeAttribute(id,element,doc,name) -{ +Light::Light(uint64_t id, const Element &element, const Document &doc, const std::string &name) : + NodeAttribute(id, element, doc, name) { // empty } // ------------------------------------------------------------------------------------------------ -Camera::~Camera() -{ +Null::Null(uint64_t id, const Element &element, const Document &doc, const std::string &name) : + NodeAttribute(id, element, doc, name) { // empty } // ------------------------------------------------------------------------------------------------ -Light::Light(uint64_t id, const Element& element, const Document& doc, const std::string& name) -: NodeAttribute(id,element,doc,name) -{ +LimbNode::LimbNode(uint64_t id, const Element &element, const Document &doc, const std::string &name) : + NodeAttribute(id, element, doc, name) { // empty } +} // namespace FBX +} // namespace Assimp -// ------------------------------------------------------------------------------------------------ -Light::~Light() -{ -} - - -// ------------------------------------------------------------------------------------------------ -Null::Null(uint64_t id, const Element& element, const Document& doc, const std::string& name) -: NodeAttribute(id,element,doc,name) -{ - -} - - -// ------------------------------------------------------------------------------------------------ -Null::~Null() -{ - -} - - -// ------------------------------------------------------------------------------------------------ -LimbNode::LimbNode(uint64_t id, const Element& element, const Document& doc, const std::string& name) -: NodeAttribute(id,element,doc,name) -{ - -} - - -// ------------------------------------------------------------------------------------------------ -LimbNode::~LimbNode() -{ - -} - -} -} - -#endif +#endif // ASSIMP_BUILD_NO_FBX_IMPORTER diff --git a/code/AssetLib/FBX/FBXParser.cpp b/code/AssetLib/FBX/FBXParser.cpp index 7cb7ae739..488e075dc 100644 --- a/code/AssetLib/FBX/FBXParser.cpp +++ b/code/AssetLib/FBX/FBXParser.cpp @@ -949,8 +949,7 @@ void ParseVectorDataArray(std::vector& out, const Element& el) // ------------------------------------------------------------------------------------------------ // read an array of uints -void ParseVectorDataArray(std::vector& out, const Element& el) -{ +void ParseVectorDataArray(std::vector& out, const Element& el) { out.resize( 0 ); const TokenList& tok = el.Tokens(); if(tok.empty()) { @@ -1174,7 +1173,6 @@ aiMatrix4x4 ReadMatrix(const Element& element) return result; } - // ------------------------------------------------------------------------------------------------ // wrapper around ParseTokenAsString() with ParseError handling std::string ParseTokenAsString(const Token& t) diff --git a/include/assimp/mesh.h b/include/assimp/mesh.h index cd334f786..23ee14a9d 100644 --- a/include/assimp/mesh.h +++ b/include/assimp/mesh.h @@ -946,6 +946,7 @@ struct aiSkeletonBone { /// The parent bone index, is -1 one if this bone represents the root bone. int mParent; + #ifndef ASSIMP_BUILD_NO_ARMATUREPOPULATE_PROCESS /// The bone armature node - used for skeleton conversion /// you must enable aiProcess_PopulateArmatureData to populate this @@ -959,6 +960,9 @@ struct aiSkeletonBone { /// @brief The number of weights unsigned int mNumnWeights; + /// The mesh index, which will get influenced by the weight. + C_STRUCT aiMesh *mMeshId; + /// The influence weights of this bone, by vertex index. C_STRUCT aiVertexWeight *mWeights; @@ -984,6 +988,7 @@ struct aiSkeletonBone { mArmature(nullptr), mNode(nullptr), mNumnWeights(0), + mMeshId(nullptr), mWeights(nullptr), mOffsetMatrix(), mLocalMatrix() { From 0afb594f40335a281c6ec3f36774e7f70429999f Mon Sep 17 00:00:00 2001 From: Kim Kulling Date: Thu, 28 Apr 2022 21:19:10 +0200 Subject: [PATCH 05/17] Generate container for skeleton during FBX-Import --- code/AssetLib/FBX/FBXConverter.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/code/AssetLib/FBX/FBXConverter.cpp b/code/AssetLib/FBX/FBXConverter.cpp index 34a97c889..8bab3dfb3 100644 --- a/code/AssetLib/FBX/FBXConverter.cpp +++ b/code/AssetLib/FBX/FBXConverter.cpp @@ -1153,7 +1153,8 @@ unsigned int FBXConverter::ConvertMeshSingleMaterial(const MeshGeometry &mesh, c if (doc.Settings().readWeights && mesh.DeformerSkin() != nullptr && !doc.Settings().useSkeleton) { ConvertWeights(out_mesh, mesh, absolute_transform, parent, NO_MATERIAL_SEPARATION, nullptr); } else if (doc.Settings().readWeights && mesh.DeformerSkin() != nullptr && doc.Settings().useSkeleton) { - ConvertWeightsToSkeleton(out_mesh, mesh, absolute_transform, parent, NO_MATERIAL_SEPARATION, nullptr); + SkeletonBoneContainer sbc; + ConvertWeightsToSkeleton(out_mesh, mesh, absolute_transform, parent, NO_MATERIAL_SEPARATION, nullptr, sbc); } std::vector animMeshes; From 64a69682545039754095f902e3dbbd2b17ba27e9 Mon Sep 17 00:00:00 2001 From: Kim Kulling Date: Thu, 28 Apr 2022 21:43:02 +0200 Subject: [PATCH 06/17] Add skeleton generation to aiScene --- code/AssetLib/FBX/FBXConverter.cpp | 38 ++++++++++++++++++++++++++++++ code/AssetLib/FBX/FBXConverter.h | 1 + include/assimp/mesh.h | 2 +- include/assimp/scene.h | 3 +-- 4 files changed, 41 insertions(+), 3 deletions(-) diff --git a/code/AssetLib/FBX/FBXConverter.cpp b/code/AssetLib/FBX/FBXConverter.cpp index 8bab3dfb3..72d12c63b 100644 --- a/code/AssetLib/FBX/FBXConverter.cpp +++ b/code/AssetLib/FBX/FBXConverter.cpp @@ -1030,6 +1030,34 @@ aiMesh *FBXConverter::SetupEmptyMesh(const Geometry &mesh, aiNode *parent) { return out_mesh; } +static aiSkeleton *createAiSkeleton(SkeletonBoneContainer &sbc) { + if (sbc.MeshArray.empty() || sbc.SkeletonBoneToMeshLookup.empty()) { + return nullptr; + } + + aiSkeleton *skeleton = new aiSkeleton; + for (auto *mesh : sbc.MeshArray) { + auto it = sbc.SkeletonBoneToMeshLookup.find(mesh); + if (it == sbc.SkeletonBoneToMeshLookup.end()) { + continue; + } + SkeletonBoneArray *ba = it->second; + if (ba == nullptr) { + continue; + } + + skeleton->mNumBones = static_cast(ba->size()); + skeleton->mBones = new aiSkeletonBone*[skeleton->mNumBones]; + size_t index = 0; + for (auto bone : (* ba)) { + skeleton->mBones[index] = bone; + ++index; + } + } + + return skeleton; +} + unsigned int FBXConverter::ConvertMeshSingleMaterial(const MeshGeometry &mesh, const Model &model, const aiMatrix4x4 &absolute_transform, aiNode *parent, aiNode *) { @@ -1155,6 +1183,10 @@ unsigned int FBXConverter::ConvertMeshSingleMaterial(const MeshGeometry &mesh, c } else if (doc.Settings().readWeights && mesh.DeformerSkin() != nullptr && doc.Settings().useSkeleton) { SkeletonBoneContainer sbc; ConvertWeightsToSkeleton(out_mesh, mesh, absolute_transform, parent, NO_MATERIAL_SEPARATION, nullptr, sbc); + aiSkeleton *skeleton = createAiSkeleton(sbc); + if (skeleton != nullptr) { + mSkeletons.emplace_back(skeleton); + } } std::vector animMeshes; @@ -3657,6 +3689,12 @@ void FBXConverter::TransferDataToScene() { std::swap_ranges(textures.begin(), textures.end(), mSceneOut->mTextures); } + + if (!mSkeletons.empty()) { + mSceneOut->mSkeletons = new aiSkeleton *[mSkeletons.size()]; + mSceneOut->mNumSkeletons = static_cast(mSkeletons.size()); + std::swap_ranges(mSkeletons.begin(), mSkeletons.end(), mSceneOut->mSkeletons); + } } void FBXConverter::ConvertOrphanedEmbeddedTextures() { diff --git a/code/AssetLib/FBX/FBXConverter.h b/code/AssetLib/FBX/FBXConverter.h index a393f9ba0..85d086c03 100644 --- a/code/AssetLib/FBX/FBXConverter.h +++ b/code/AssetLib/FBX/FBXConverter.h @@ -467,6 +467,7 @@ private: double anim_fps; + std::vector mSkeletons; aiScene* const mSceneOut; const FBX::Document& doc; bool mRemoveEmptyBones; diff --git a/include/assimp/mesh.h b/include/assimp/mesh.h index 23ee14a9d..ffac27bb0 100644 --- a/include/assimp/mesh.h +++ b/include/assimp/mesh.h @@ -1018,7 +1018,7 @@ struct aiSkeleton { /** * */ - C_STRUCT aiSkeletonBone *mBones; + C_STRUCT aiSkeletonBone **mBones; #ifdef __cplusplus /** diff --git a/include/assimp/scene.h b/include/assimp/scene.h index 2dac27da2..f6dfd82b0 100644 --- a/include/assimp/scene.h +++ b/include/assimp/scene.h @@ -79,8 +79,7 @@ extern "C" { * the imported scene does consist of only a single root node without children. */ // ------------------------------------------------------------------------------- -struct ASSIMP_API aiNode -{ +struct ASSIMP_API aiNode { /** The name of the node. * * The name might be empty (length of zero) but all nodes which From d5c798f3252479498c600512d4084d43a02a2dc6 Mon Sep 17 00:00:00 2001 From: Kim Kulling Date: Sat, 30 Apr 2022 21:02:48 +0200 Subject: [PATCH 07/17] Fix leak --- code/Common/Version.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/code/Common/Version.cpp b/code/Common/Version.cpp index 86d3f2220..cfb0de4e5 100644 --- a/code/Common/Version.cpp +++ b/code/Common/Version.cpp @@ -180,7 +180,8 @@ ASSIMP_API aiScene::~aiScene() { delete[] mCameras; aiMetadata::Dealloc(mMetaData); - mMetaData = nullptr; + delete[] mSkeletons; + delete static_cast(mPrivate); } From e284fe67b835f8747ccb883557fc7856edb803a0 Mon Sep 17 00:00:00 2001 From: Kim Kulling Date: Fri, 13 May 2022 19:43:46 +0200 Subject: [PATCH 08/17] Adapt file for skeleton unittest --- test/unit/utFBXImporterExporter.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/test/unit/utFBXImporterExporter.cpp b/test/unit/utFBXImporterExporter.cpp index 9b8f2fa0d..9fa293aaf 100644 --- a/test/unit/utFBXImporterExporter.cpp +++ b/test/unit/utFBXImporterExporter.cpp @@ -423,3 +423,10 @@ TEST_F(utFBXImporterExporter, importMaxPbrMaterialsSpecularGloss) { ASSERT_EQ(mat->Get("$raw.3dsMax|main|emit_color", aiTextureType_NONE, 0, emitColor), aiReturn_SUCCESS); EXPECT_EQ(emitColor, aiColor4D(1, 0, 1, 1)); } + +TEST_F(utFBXImporterExporter, importSkeletonTest) { + Assimp::Importer importer; + const aiScene *scene = importer.ReadFile(ASSIMP_TEST_MODELS_DIR "/FBX/animation_with_skeleton.fbx", aiProcess_ValidateDataStructure); + ASSERT_NE(nullptr, scene); + ASSERT_TRUE(scene->mRootNode); +} From 1b3e9e4e01f0017fab43afa9f51314f826d22175 Mon Sep 17 00:00:00 2001 From: Kim Kulling Date: Fri, 13 May 2022 19:44:14 +0200 Subject: [PATCH 09/17] Add testmodel --- test/models/FBX/animation_with_skeleton.fbx | Bin 0 -> 195420 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 test/models/FBX/animation_with_skeleton.fbx diff --git a/test/models/FBX/animation_with_skeleton.fbx b/test/models/FBX/animation_with_skeleton.fbx new file mode 100644 index 0000000000000000000000000000000000000000..a7476bc64578a9eb7f5967148004ebcb0aba1676 GIT binary patch literal 195420 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 literal 0 HcmV?d00001 From ed3e218550f726b09a25beda5900379e1d7c9a6c Mon Sep 17 00:00:00 2001 From: Kim Kulling Date: Sat, 21 May 2022 19:08:43 +0200 Subject: [PATCH 10/17] Bugfix: fix not initialized member attributes --- code/AssetLib/FBX/FBXConverter.cpp | 10 +++------- code/AssetLib/FBX/FBXDocumentUtil.cpp | 9 +++------ code/Common/Version.cpp | 3 +++ 3 files changed, 9 insertions(+), 13 deletions(-) diff --git a/code/AssetLib/FBX/FBXConverter.cpp b/code/AssetLib/FBX/FBXConverter.cpp index 72d12c63b..19732a3b0 100644 --- a/code/AssetLib/FBX/FBXConverter.cpp +++ b/code/AssetLib/FBX/FBXConverter.cpp @@ -65,12 +65,9 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include #include #include -#include #include #include #include -#include -#include namespace Assimp { namespace FBX { @@ -187,8 +184,7 @@ std::string FBXConverter::MakeUniqueNodeName(const Model *const model, const aiN /// This struct manages nodes which may or may not end up in the node hierarchy. /// When a node becomes a child of another node, that node becomes its owner and mOwnership should be released. -struct FBXConverter::PotentialNode -{ +struct FBXConverter::PotentialNode { PotentialNode() : mOwnership(new aiNode), mNode(mOwnership.get()) {} PotentialNode(const std::string& name) : mOwnership(new aiNode(name)), mNode(mOwnership.get()) {} aiNode* operator->() { return mNode; } @@ -452,7 +448,7 @@ void FBXConverter::GetUniqueName(const std::string &name, std::string &uniqueNam auto it_pair = mNodeNames.insert({ name, 0 }); // duplicate node name instance count unsigned int &i = it_pair.first->second; while (!it_pair.second) { - i++; + ++i; std::ostringstream ext; ext << name << std::setfill('0') << std::setw(3) << i; uniqueName = ext.str(); @@ -651,7 +647,6 @@ void FBXConverter::GetRotationMatrix(Model::RotOrder mode, const aiVector3D &rot bool FBXConverter::NeedsComplexTransformationChain(const Model &model) { const PropertyTable &props = model.Props(); - bool ok; const float zero_epsilon = ai_epsilon; const aiVector3D all_ones(1.0f, 1.0f, 1.0f); @@ -665,6 +660,7 @@ bool FBXConverter::NeedsComplexTransformationChain(const Model &model) { bool scale_compare = (comp == TransformationComp_GeometricScaling || comp == TransformationComp_Scaling); + bool ok = true; const aiVector3D &v = PropertyGet(props, NameTransformationCompProperty(comp), ok); if (ok && scale_compare) { if ((v - all_ones).SquareLength() > zero_epsilon) { diff --git a/code/AssetLib/FBX/FBXDocumentUtil.cpp b/code/AssetLib/FBX/FBXDocumentUtil.cpp index 68185e564..c41eb2747 100644 --- a/code/AssetLib/FBX/FBXDocumentUtil.cpp +++ b/code/AssetLib/FBX/FBXDocumentUtil.cpp @@ -59,14 +59,12 @@ namespace Util { // ------------------------------------------------------------------------------------------------ // signal DOM construction error, this is always unrecoverable. Throws DeadlyImportError. -void DOMError(const std::string& message, const Token& token) -{ +void DOMError(const std::string& message, const Token& token) { throw DeadlyImportError("FBX-DOM", Util::GetTokenText(&token), message); } // ------------------------------------------------------------------------------------------------ -void DOMError(const std::string& message, const Element* element /*= nullptr*/) -{ +void DOMError(const std::string& message, const Element* element /*= nullptr*/) { if(element) { DOMError(message,element->KeyToken()); } @@ -76,8 +74,7 @@ void DOMError(const std::string& message, const Element* element /*= nullptr*/) // ------------------------------------------------------------------------------------------------ // print warning, do return -void DOMWarning(const std::string& message, const Token& token) -{ +void DOMWarning(const std::string& message, const Token& token) { if(DefaultLogger::get()) { ASSIMP_LOG_WARN("FBX-DOM", Util::GetTokenText(&token), message); } diff --git a/code/Common/Version.cpp b/code/Common/Version.cpp index cfb0de4e5..808c3598d 100644 --- a/code/Common/Version.cpp +++ b/code/Common/Version.cpp @@ -135,6 +135,9 @@ ASSIMP_API aiScene::aiScene() : mNumCameras(0), mCameras(nullptr), mMetaData(nullptr), + mName(), + mNumSkeletons(0), + mSkeletons(nullptr), mPrivate(new Assimp::ScenePrivateData()) { // empty } From 26233f1b77767d9a0455b02ce9bd972f36cbf7fe Mon Sep 17 00:00:00 2001 From: Matthew Clendening Date: Thu, 26 May 2022 15:06:23 -0400 Subject: [PATCH 11/17] Fixed FBXConverter build error (warning as error) when ASSIMP_DOUBLE_PRECISION is defined --- code/AssetLib/FBX/FBXConverter.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/code/AssetLib/FBX/FBXConverter.cpp b/code/AssetLib/FBX/FBXConverter.cpp index 4de142075..9ce593122 100644 --- a/code/AssetLib/FBX/FBXConverter.cpp +++ b/code/AssetLib/FBX/FBXConverter.cpp @@ -648,7 +648,7 @@ bool FBXConverter::NeedsComplexTransformationChain(const Model &model) { const PropertyTable &props = model.Props(); bool ok; - const float zero_epsilon = ai_epsilon; + const auto zero_epsilon = ai_epsilon; const aiVector3D all_ones(1.0f, 1.0f, 1.0f); for (size_t i = 0; i < TransformationComp_MAXIMUM; ++i) { const TransformationComp comp = static_cast(i); @@ -3180,7 +3180,7 @@ aiNodeAnim* FBXConverter::GenerateSimpleNodeAnim(const std::string& name, bool ok = false; - const float zero_epsilon = ai_epsilon; + const auto zero_epsilon = ai_epsilon; const aiVector3D& preRotation = PropertyGet(props, "PreRotation", ok); if (ok && preRotation.SquareLength() > zero_epsilon) { From 4961241c09ff68d7eed4e36916885ca6a1e73b48 Mon Sep 17 00:00:00 2001 From: ethaninfinity Date: Mon, 6 Jun 2022 14:53:03 -0400 Subject: [PATCH 12/17] Removed pragma warnings --- contrib/unzip/crypt.c | 9 --------- contrib/unzip/ioapi.c | 3 --- contrib/unzip/unzip.c | 11 +---------- 3 files changed, 1 insertion(+), 22 deletions(-) diff --git a/contrib/unzip/crypt.c b/contrib/unzip/crypt.c index 299ce03d2..22436baab 100644 --- a/contrib/unzip/crypt.c +++ b/contrib/unzip/crypt.c @@ -43,11 +43,6 @@ #include "crypt.h" -#ifdef _WIN32 -# pragma warning(push) -# pragma warning(disable : 4244) -#endif // _WIN32 - /***************************************************************************/ #define CRC32(c, b) ((*(pcrc_32_tab+(((uint32_t)(c) ^ (b)) & 0xff))) ^ ((c) >> 8)) @@ -164,8 +159,4 @@ int crypthead(const char *passwd, uint8_t *buf, int buf_size, uint32_t *pkeys, return n; } -#ifdef _WIN32 -# pragma warning(pop) -#endif // _WIN32 - /***************************************************************************/ diff --git a/contrib/unzip/ioapi.c b/contrib/unzip/ioapi.c index 30a296d0f..99295f0f8 100644 --- a/contrib/unzip/ioapi.c +++ b/contrib/unzip/ioapi.c @@ -23,8 +23,6 @@ #ifdef _WIN32 # define snprintf _snprintf -# pragma warning(push) -# pragma warning(disable : 4131 4100) # ifdef __clang__ # pragma clang diagnostic push # pragma clang diagnostic ignored "-Wunused-parameter" @@ -358,7 +356,6 @@ void fill_fopen64_filefunc(zlib_filefunc64_def *pzlib_filefunc_def) } #ifdef _WIN32 -# pragma warning(pop) # ifdef __clang__ # pragma clang diagnostic pop # endif diff --git a/contrib/unzip/unzip.c b/contrib/unzip/unzip.c index f1eddeeda..311a6ae03 100644 --- a/contrib/unzip/unzip.c +++ b/contrib/unzip/unzip.c @@ -73,11 +73,6 @@ # define TRYFREE(p) {if (p) free(p);} #endif -#ifdef _WIN32 -# pragma warning(push) -# pragma warning(disable : 4131 4244 4189 4245) -#endif // _WIN32 - const char unz_copyright[] = " unzip 1.01 Copyright 1998-2004 Gilles Vollant - http://www.winimage.com/zLibDll"; @@ -1993,8 +1988,4 @@ extern int ZEXPORT unzEndOfFile(unzFile file) if (s->pfile_in_zip_read->rest_read_uncompressed == 0) return 1; return 0; -} - -#ifdef _WIN32 -# pragma warning(pop) -#endif // _WIN32 \ No newline at end of file +} \ No newline at end of file From c5b6b26b8b1b19839504f8f76b2fa030c1bc9cd3 Mon Sep 17 00:00:00 2001 From: ethaninfinity Date: Tue, 7 Jun 2022 11:27:12 -0400 Subject: [PATCH 13/17] Revert "Removed pragma warnings" This reverts commit 4961241c09ff68d7eed4e36916885ca6a1e73b48. --- contrib/unzip/crypt.c | 9 +++++++++ contrib/unzip/ioapi.c | 3 +++ contrib/unzip/unzip.c | 11 ++++++++++- 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/contrib/unzip/crypt.c b/contrib/unzip/crypt.c index 22436baab..299ce03d2 100644 --- a/contrib/unzip/crypt.c +++ b/contrib/unzip/crypt.c @@ -43,6 +43,11 @@ #include "crypt.h" +#ifdef _WIN32 +# pragma warning(push) +# pragma warning(disable : 4244) +#endif // _WIN32 + /***************************************************************************/ #define CRC32(c, b) ((*(pcrc_32_tab+(((uint32_t)(c) ^ (b)) & 0xff))) ^ ((c) >> 8)) @@ -159,4 +164,8 @@ int crypthead(const char *passwd, uint8_t *buf, int buf_size, uint32_t *pkeys, return n; } +#ifdef _WIN32 +# pragma warning(pop) +#endif // _WIN32 + /***************************************************************************/ diff --git a/contrib/unzip/ioapi.c b/contrib/unzip/ioapi.c index 99295f0f8..30a296d0f 100644 --- a/contrib/unzip/ioapi.c +++ b/contrib/unzip/ioapi.c @@ -23,6 +23,8 @@ #ifdef _WIN32 # define snprintf _snprintf +# pragma warning(push) +# pragma warning(disable : 4131 4100) # ifdef __clang__ # pragma clang diagnostic push # pragma clang diagnostic ignored "-Wunused-parameter" @@ -356,6 +358,7 @@ void fill_fopen64_filefunc(zlib_filefunc64_def *pzlib_filefunc_def) } #ifdef _WIN32 +# pragma warning(pop) # ifdef __clang__ # pragma clang diagnostic pop # endif diff --git a/contrib/unzip/unzip.c b/contrib/unzip/unzip.c index 311a6ae03..f1eddeeda 100644 --- a/contrib/unzip/unzip.c +++ b/contrib/unzip/unzip.c @@ -73,6 +73,11 @@ # define TRYFREE(p) {if (p) free(p);} #endif +#ifdef _WIN32 +# pragma warning(push) +# pragma warning(disable : 4131 4244 4189 4245) +#endif // _WIN32 + const char unz_copyright[] = " unzip 1.01 Copyright 1998-2004 Gilles Vollant - http://www.winimage.com/zLibDll"; @@ -1988,4 +1993,8 @@ extern int ZEXPORT unzEndOfFile(unzFile file) if (s->pfile_in_zip_read->rest_read_uncompressed == 0) return 1; return 0; -} \ No newline at end of file +} + +#ifdef _WIN32 +# pragma warning(pop) +#endif // _WIN32 \ No newline at end of file From d018c3b55577da5cf2affc1635109690d1e6d143 Mon Sep 17 00:00:00 2001 From: ethaninfinity Date: Tue, 7 Jun 2022 11:42:09 -0400 Subject: [PATCH 14/17] Added back pragma warnings and changed to be MSVC-specific --- contrib/unzip/crypt.c | 8 ++++---- contrib/unzip/ioapi.c | 6 ++++-- contrib/unzip/unzip.c | 8 ++++---- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/contrib/unzip/crypt.c b/contrib/unzip/crypt.c index 299ce03d2..4cc731b3e 100644 --- a/contrib/unzip/crypt.c +++ b/contrib/unzip/crypt.c @@ -43,10 +43,10 @@ #include "crypt.h" -#ifdef _WIN32 +#ifdef _MSC_VER # pragma warning(push) # pragma warning(disable : 4244) -#endif // _WIN32 +#endif // _MSC_VER /***************************************************************************/ @@ -164,8 +164,8 @@ int crypthead(const char *passwd, uint8_t *buf, int buf_size, uint32_t *pkeys, return n; } -#ifdef _WIN32 +#ifdef _MSC_VER # pragma warning(pop) -#endif // _WIN32 +#endif // _MSC_VER /***************************************************************************/ diff --git a/contrib/unzip/ioapi.c b/contrib/unzip/ioapi.c index 30a296d0f..d9ae01e7d 100644 --- a/contrib/unzip/ioapi.c +++ b/contrib/unzip/ioapi.c @@ -23,8 +23,10 @@ #ifdef _WIN32 # define snprintf _snprintf +#ifdef _MSC_VER # pragma warning(push) # pragma warning(disable : 4131 4100) +#endif # ifdef __clang__ # pragma clang diagnostic push # pragma clang diagnostic ignored "-Wunused-parameter" @@ -357,9 +359,9 @@ void fill_fopen64_filefunc(zlib_filefunc64_def *pzlib_filefunc_def) pzlib_filefunc_def->opaque = NULL; } -#ifdef _WIN32 +#ifdef _MSC_VER # pragma warning(pop) # ifdef __clang__ # pragma clang diagnostic pop # endif -#endif // _WIN32 +#endif // _MSC_VER diff --git a/contrib/unzip/unzip.c b/contrib/unzip/unzip.c index f1eddeeda..b2f045b0a 100644 --- a/contrib/unzip/unzip.c +++ b/contrib/unzip/unzip.c @@ -73,10 +73,10 @@ # define TRYFREE(p) {if (p) free(p);} #endif -#ifdef _WIN32 +#ifdef _MSC_VER # pragma warning(push) # pragma warning(disable : 4131 4244 4189 4245) -#endif // _WIN32 +#endif // _MSC_VER const char unz_copyright[] = " unzip 1.01 Copyright 1998-2004 Gilles Vollant - http://www.winimage.com/zLibDll"; @@ -1995,6 +1995,6 @@ extern int ZEXPORT unzEndOfFile(unzFile file) return 0; } -#ifdef _WIN32 +#ifdef _MSC_VER # pragma warning(pop) -#endif // _WIN32 \ No newline at end of file +#endif // _MSC_VER \ No newline at end of file From 4fb5cf4ba6102c4e62113cc37b53c383c9c11708 Mon Sep 17 00:00:00 2001 From: Kim Kulling Date: Tue, 7 Jun 2022 23:33:56 +0200 Subject: [PATCH 15/17] Fix possible nullptr exception - closes https://github.com/assimp/assimp/issues/4418 --- code/AssetLib/glTF2/glTF2Exporter.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/AssetLib/glTF2/glTF2Exporter.cpp b/code/AssetLib/glTF2/glTF2Exporter.cpp index ffd8d223e..932fc4197 100644 --- a/code/AssetLib/glTF2/glTF2Exporter.cpp +++ b/code/AssetLib/glTF2/glTF2Exporter.cpp @@ -908,7 +908,7 @@ Ref FindSkeletonRootJoint(Ref &skinRef) { do { startNodeRef = parentNodeRef; parentNodeRef = startNodeRef->parent; - } while (!parentNodeRef->jointName.empty()); + } while (nullptr != parentNodeRef && !parentNodeRef->jointName.empty()); return parentNodeRef; } From 02b0c89fa4bec9358caadb77ebe8815d9104a76e Mon Sep 17 00:00:00 2001 From: Kim Kulling Date: Wed, 8 Jun 2022 00:10:13 +0200 Subject: [PATCH 16/17] Fix usage of validation --- code/AssetLib/glTF2/glTF2Exporter.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/AssetLib/glTF2/glTF2Exporter.cpp b/code/AssetLib/glTF2/glTF2Exporter.cpp index 932fc4197..d2f413932 100644 --- a/code/AssetLib/glTF2/glTF2Exporter.cpp +++ b/code/AssetLib/glTF2/glTF2Exporter.cpp @@ -908,7 +908,7 @@ Ref FindSkeletonRootJoint(Ref &skinRef) { do { startNodeRef = parentNodeRef; parentNodeRef = startNodeRef->parent; - } while (nullptr != parentNodeRef && !parentNodeRef->jointName.empty()); + } while (parentNodeRef && !parentNodeRef->jointName.empty()); return parentNodeRef; } From aaa19903c6cfbf9e507af18c7a4c5aff90ea9db5 Mon Sep 17 00:00:00 2001 From: Kim Kulling Date: Sat, 11 Jun 2022 18:50:18 +0200 Subject: [PATCH 17/17] Fix merge conflicts --- code/AssetLib/FBX/FBXConverter.cpp | 37 +++++++++++++++--------------- code/AssetLib/FBX/FBXConverter.h | 14 +++++------ 2 files changed, 25 insertions(+), 26 deletions(-) diff --git a/code/AssetLib/FBX/FBXConverter.cpp b/code/AssetLib/FBX/FBXConverter.cpp index a9875deec..9d891864a 100644 --- a/code/AssetLib/FBX/FBXConverter.cpp +++ b/code/AssetLib/FBX/FBXConverter.cpp @@ -227,7 +227,7 @@ void FBXConverter::ConvertNodes(uint64_t id, aiNode *parent, aiNode *root_node) if (nullptr != model) { nodes_chain.clear(); post_nodes_chain.clear(); - + aiMatrix4x4 new_abs_transform = parent->mTransformation; std::string node_name = FixNodeName(model->Name()); // even though there is only a single input node, the design of // assimp (or rather: the complicated transformation chain that @@ -264,7 +264,7 @@ void FBXConverter::ConvertNodes(uint64_t id, aiNode *parent, aiNode *root_node) } // attach geometry - ConvertModel(*model, nodes_chain.back().mNode, root_node); + ConvertModel(*model, nodes_chain.back().mNode, root_node, new_abs_transform); // check if there will be any child nodes const std::vector &child_conns = doc.GetConnectionsByDestinationSequenced(model->ID(), "Model"); @@ -890,7 +890,7 @@ void FBXConverter::SetupNodeMetadata(const Model &model, aiNode &nd) { } } -void FBXConverter::ConvertModel(const Model &model, aiNode *parent, aiNode *root_node) { +void FBXConverter::ConvertModel(const Model &model, aiNode *parent, aiNode *root_node, const aiMatrix4x4 &absolute_transform) { const std::vector &geos = model.GetGeometry(); std::vector meshes; @@ -900,7 +900,7 @@ void FBXConverter::ConvertModel(const Model &model, aiNode *parent, aiNode *root const MeshGeometry *const mesh = dynamic_cast(geo); const LineGeometry *const line = dynamic_cast(geo); if (mesh) { - const std::vector &indices = ConvertMesh(*mesh, model, parent, root_node); + const std::vector &indices = ConvertMesh(*mesh, model, parent, root_node, absolute_transform); std::copy(indices.begin(), indices.end(), std::back_inserter(meshes)); } else if (line) { const std::vector &indices = ConvertLine(*line, root_node); @@ -921,7 +921,7 @@ void FBXConverter::ConvertModel(const Model &model, aiNode *parent, aiNode *root } std::vector -FBXConverter::ConvertMesh(const MeshGeometry &mesh, const Model &model, aiNode *parent, aiNode *root_node) { +FBXConverter::ConvertMesh(const MeshGeometry &mesh, const Model &model, aiNode *parent, aiNode *root_node, const aiMatrix4x4 &absolute_transform) { std::vector temp; MeshMap::const_iterator it = meshes_converted.find(&mesh); @@ -944,13 +944,13 @@ FBXConverter::ConvertMesh(const MeshGeometry &mesh, const Model &model, aiNode * const MatIndexArray::value_type base = mindices[0]; for (MatIndexArray::value_type index : mindices) { if (index != base) { - return ConvertMeshMultiMaterial(mesh, model, parent, root_node); + return ConvertMeshMultiMaterial(mesh, model, absolute_transform, parent, root_node); } } } // faster code-path, just copy the data - temp.push_back(ConvertMeshSingleMaterial(mesh, model, parent, root_node)); + temp.push_back(ConvertMeshSingleMaterial(mesh, model, absolute_transform, parent, root_node)); return temp; } @@ -1046,7 +1046,7 @@ static aiSkeleton *createAiSkeleton(SkeletonBoneContainer &sbc) { return skeleton; } -unsigned int FBXConverter::ConvertMeshSingleMaterial(const MeshGeometry &mesh, const Model &model, +unsigned int FBXConverter::ConvertMeshSingleMaterial(const MeshGeometry &mesh, const Model &model, const aiMatrix4x4 &absolute_transform, aiNode *parent, aiNode *) { const MatIndexArray &mindices = mesh.GetMaterialIndices(); aiMesh *const out_mesh = SetupEmptyMesh(mesh, parent); @@ -1220,7 +1220,7 @@ unsigned int FBXConverter::ConvertMeshSingleMaterial(const MeshGeometry &mesh, c } std::vector -FBXConverter::ConvertMeshMultiMaterial(const MeshGeometry &mesh, const Model &model, aiNode *parent, +FBXConverter::ConvertMeshMultiMaterial(const MeshGeometry &mesh, const Model &model, const aiMatrix4x4 &absolute_transform, aiNode *parent, aiNode *root_node) { const MatIndexArray &mindices = mesh.GetMaterialIndices(); ai_assert(mindices.size()); @@ -1231,7 +1231,7 @@ FBXConverter::ConvertMeshMultiMaterial(const MeshGeometry &mesh, const Model &mo for (MatIndexArray::value_type index : mindices) { if (had.find(index) == had.end()) { - indices.push_back(ConvertMeshMultiMaterial(mesh, model, index, parent, root_node)); + indices.push_back(ConvertMeshMultiMaterial(mesh, model, absolute_transform, index, parent, root_node)); had.insert(index); } } @@ -1239,9 +1239,8 @@ FBXConverter::ConvertMeshMultiMaterial(const MeshGeometry &mesh, const Model &mo return indices; } -unsigned int FBXConverter::ConvertMeshMultiMaterial(const MeshGeometry &mesh, const Model &model, - MatIndexArray::value_type index, - aiNode *parent, aiNode *) { +unsigned int FBXConverter::ConvertMeshMultiMaterial(const MeshGeometry &mesh, const Model &model, const aiMatrix4x4 &absolute_transform, + MatIndexArray::value_type index, aiNode *parent, aiNode *) { aiMesh *const out_mesh = SetupEmptyMesh(mesh, parent); const MatIndexArray &mindices = mesh.GetMaterialIndices(); @@ -1404,7 +1403,7 @@ unsigned int FBXConverter::ConvertMeshMultiMaterial(const MeshGeometry &mesh, co ConvertMaterialForMesh(out_mesh, model, mesh, index); if (process_weights) { - ConvertWeights(out_mesh, mesh, parent, index, &reverseMapping); + ConvertWeights(out_mesh, mesh, absolute_transform, parent, index, &reverseMapping); } std::vector animMeshes; @@ -1485,7 +1484,7 @@ void FBXConverter::ConvertWeightsToSkeleton(aiMesh *out, const MeshGeometry &geo skeletonContainer.SkeletonBoneToMeshLookup[out] = ba; } -void FBXConverter::ConvertWeights(aiMesh *out, const MeshGeometry &geo, +void FBXConverter::ConvertWeights(aiMesh *out, const MeshGeometry &geo, const aiMatrix4x4 &absolute_transform, aiNode *parent, unsigned int materialIndex, std::vector *outputVertStartIndices) { ai_assert(geo.DeformerSkin()); @@ -1554,7 +1553,7 @@ void FBXConverter::ConvertWeights(aiMesh *out, const MeshGeometry &geo, // XXX this could be heavily simplified by collecting the bone // data in a single step. ConvertCluster(bones, cluster, out_indices, index_out_indices, - count_out_indices, parent); + count_out_indices, absolute_transform, parent); } bone_map.clear(); @@ -1576,7 +1575,7 @@ void FBXConverter::ConvertWeights(aiMesh *out, const MeshGeometry &geo, void FBXConverter::ConvertCluster(std::vector &local_mesh_bones, const Cluster *cluster, std::vector &out_indices, std::vector &index_out_indices, - std::vector &count_out_indices, const aiMatrix4x4 &absolute_transform, + std::vector &count_out_indices, const aiMatrix4x4 & /* absolute_transform*/, aiNode *) { ai_assert(cluster != nullptr); // make sure cluster valid @@ -1593,7 +1592,7 @@ void FBXConverter::ConvertCluster(std::vector &local_mesh_bones, const bone = new aiBone(); bone->mName = bone_name; - bone->mOffsetMatrix = cl->Transform(); + bone->mOffsetMatrix = cluster->Transform(); // store local transform link for post processing /* bone->mOffsetMatrix = cluster->TransformLink(); @@ -2658,7 +2657,7 @@ void FBXConverter::ConvertAnimationStack(const AnimationStack &st) { meshMorphAnim->mNumKeys = numKeys; meshMorphAnim->mKeys = new aiMeshMorphKey[numKeys]; unsigned int j = 0; - for (auto animIt : *animData) { + for (auto &animIt : *animData) { morphKeyData *keyData = animIt.second; unsigned int numValuesAndWeights = static_cast(keyData->values.size()); meshMorphAnim->mKeys[j].mNumValuesAndWeights = numValuesAndWeights; diff --git a/code/AssetLib/FBX/FBXConverter.h b/code/AssetLib/FBX/FBXConverter.h index 1c303f68d..41acb6ffe 100644 --- a/code/AssetLib/FBX/FBXConverter.h +++ b/code/AssetLib/FBX/FBXConverter.h @@ -191,12 +191,12 @@ private: void SetupNodeMetadata(const Model& model, aiNode& nd); // ------------------------------------------------------------------------------------------------ - void ConvertModel(const Model &model, aiNode *parent, aiNode *root_node); + void ConvertModel(const Model &model, aiNode *parent, aiNode *root_node, const aiMatrix4x4 &absolute_transform); // ------------------------------------------------------------------------------------------------ // MeshGeometry -> aiMesh, return mesh index + 1 or 0 if the conversion failed std::vector - ConvertMesh(const MeshGeometry &mesh, const Model &model, aiNode *parent, aiNode *root_node); + ConvertMesh(const MeshGeometry &mesh, const Model &model, aiNode *parent, aiNode *root_node, const aiMatrix4x4 &absolute_transform); // ------------------------------------------------------------------------------------------------ std::vector ConvertLine(const LineGeometry& line, aiNode *root_node); @@ -205,15 +205,15 @@ private: aiMesh* SetupEmptyMesh(const Geometry& mesh, aiNode *parent); // ------------------------------------------------------------------------------------------------ - unsigned int ConvertMeshSingleMaterial(const MeshGeometry &mesh, const Model &model, + unsigned int ConvertMeshSingleMaterial(const MeshGeometry &mesh, const Model &model, const aiMatrix4x4 &absolute_transform, aiNode *parent, aiNode *root_node); // ------------------------------------------------------------------------------------------------ std::vector - ConvertMeshMultiMaterial(const MeshGeometry &mesh, const Model &model, aiNode *parent, aiNode *root_node); + ConvertMeshMultiMaterial(const MeshGeometry &mesh, const Model &model, const aiMatrix4x4 &absolute_transform, aiNode *parent, aiNode *root_node); // ------------------------------------------------------------------------------------------------ - unsigned int ConvertMeshMultiMaterial(const MeshGeometry &mesh, const Model &model, MatIndexArray::value_type index, + unsigned int ConvertMeshMultiMaterial(const MeshGeometry &mesh, const Model &model, const aiMatrix4x4 &absolute_transform, MatIndexArray::value_type index, aiNode *parent, aiNode *root_node); // ------------------------------------------------------------------------------------------------ @@ -227,7 +227,7 @@ private: * - outputVertStartIndices is only used when a material index is specified, it gives for * each output vertex the DOM index it maps to. */ - void ConvertWeights(aiMesh *out, const MeshGeometry &geo, aiNode *parent = nullptr, + void ConvertWeights(aiMesh *out, const MeshGeometry &geo, const aiMatrix4x4 &absolute_transform, aiNode *parent = nullptr, unsigned int materialIndex = NO_MATERIAL_SEPARATION, std::vector *outputVertStartIndices = nullptr); @@ -239,7 +239,7 @@ private: // ------------------------------------------------------------------------------------------------ void ConvertCluster(std::vector &local_mesh_bones, const Cluster *cl, std::vector &out_indices, std::vector &index_out_indices, - std::vector &count_out_indices, aiNode *parent ); + std::vector &count_out_indices, const aiMatrix4x4 &absolute_transform, aiNode *parent); // ------------------------------------------------------------------------------------------------ void ConvertMaterialForMesh(aiMesh* out, const Model& model, const MeshGeometry& geo,