Prechádzať zdrojové kódy

Extension of data export to GLB/GLTF format

Allows to export unlimited (more than 4) bones per vertex
Use JOINTS_1,2,.. and WEIGHTS_1,2,...
Added AI_CONFIG_EXPORT_GLTF_UNLIMITED_SKINNING_BONES_PER_VERTEX flag
fvbj 1 rok pred
rodič
commit
8fcc65a8af

+ 97 - 70
code/AssetLib/glTF2/glTF2Exporter.cpp

@@ -172,22 +172,6 @@ static void IdentityMatrix4(mat4 &o) {
     o[15] = 1;
 }
 
-static bool IsBoneWeightFitted(vec4 &weight) {
-    return weight[0] + weight[1] + weight[2] + weight[3] >= 1.f;
-}
-
-static int FitBoneWeight(vec4 &weight, float value) {
-    int i = 0;
-    for (; i < 4; ++i) {
-        if (weight[i] < value) {
-            weight[i] = value;
-            return i;
-        }
-    }
-
-    return -1;
-}
-
 template <typename T>
 void SetAccessorRange(Ref<Accessor> acc, void *data, size_t count,
         unsigned int numCompsIn, unsigned int numCompsOut) {
@@ -1009,23 +993,29 @@ Ref<Node> FindSkeletonRootJoint(Ref<Skin> &skinRef) {
     return parentNodeRef;
 }
 
+struct boneIndexWeightPair {
+    unsigned int indexJoint;
+    float weight;
+    bool operator()(boneIndexWeightPair &a, boneIndexWeightPair &b) {
+        return a.weight > b.weight;
+    }
+};
+
 void ExportSkin(Asset &mAsset, const aiMesh *aimesh, Ref<Mesh> &meshRef, Ref<Buffer> &bufferRef, Ref<Skin> &skinRef,
-        std::vector<aiMatrix4x4> &inverseBindMatricesData) {
+        std::vector<aiMatrix4x4> &inverseBindMatricesData, bool unlimitedBonesPerVertex) {
     if (aimesh->mNumBones < 1) {
         return;
     }
 
     // Store the vertex joint and weight data.
     const size_t NumVerts(aimesh->mNumVertices);
-    vec4 *vertexJointData = new vec4[NumVerts];
-    vec4 *vertexWeightData = new vec4[NumVerts];
     int *jointsPerVertex = new int[NumVerts];
+    std::vector<std::vector<boneIndexWeightPair>> allVerticesPairs;
+    int maxJointsPerVertex = 0;
     for (size_t i = 0; i < NumVerts; ++i) {
         jointsPerVertex[i] = 0;
-        for (size_t j = 0; j < 4; ++j) {
-            vertexJointData[i][j] = 0;
-            vertexWeightData[i][j] = 0;
-        }
+        std::vector<boneIndexWeightPair> vertexPair;
+        allVerticesPairs.push_back(vertexPair);
     }
 
     for (unsigned int idx_bone = 0; idx_bone < aimesh->mNumBones; ++idx_bone) {
@@ -1055,61 +1045,88 @@ void ExportSkin(Asset &mAsset, const aiMesh *aimesh, Ref<Mesh> &meshRef, Ref<Buf
             jointNamesIndex = static_cast<unsigned int>(inverseBindMatricesData.size() - 1);
         }
 
-        // aib->mWeights   =====>  vertexWeightData
-        for (unsigned int idx_weights = 0; idx_weights < aib->mNumWeights; ++idx_weights) {
+        // aib->mWeights   =====>  temp pairs data
+        for (unsigned int idx_weights = 0; idx_weights < aib->mNumWeights;
+              ++idx_weights) {
             unsigned int vertexId = aib->mWeights[idx_weights].mVertexId;
             float vertWeight = aib->mWeights[idx_weights].mWeight;
+            allVerticesPairs[vertexId].push_back({jointNamesIndex, vertWeight});
+            jointsPerVertex[vertexId] += 1;
+            maxJointsPerVertex =
+                    std::max(maxJointsPerVertex, jointsPerVertex[vertexId]);
+        }
+    } // End: for-loop mNumMeshes
 
-            // A vertex can only have at most four joint weights, which ideally sum up to 1
-            if (IsBoneWeightFitted(vertexWeightData[vertexId])) {
-                continue;
-            }
-            if (jointsPerVertex[vertexId] > 3) {
-                int boneIndexFitted = FitBoneWeight(vertexWeightData[vertexId], vertWeight);
-                if (boneIndexFitted != -1) {
-                    vertexJointData[vertexId][boneIndexFitted] = static_cast<float>(jointNamesIndex);
-                }
-            } else {
-                vertexJointData[vertexId][jointsPerVertex[vertexId]] = static_cast<float>(jointNamesIndex);
-                vertexWeightData[vertexId][jointsPerVertex[vertexId]] = vertWeight;
+    if (!unlimitedBonesPerVertex){
+        // skinning limited only for 4 bones per vertex, default
+        maxJointsPerVertex = 4;
+    }
 
-                jointsPerVertex[vertexId] += 1;
+    // temp pairs data  =====>  vertexWeightData
+    size_t numGroups = (maxJointsPerVertex - 1) / 4 + 1;
+    vec4 *vertexJointData = new vec4[NumVerts * numGroups];
+    vec4 *vertexWeightData = new vec4[NumVerts * numGroups];
+    for (size_t indexVertex = 0; indexVertex < NumVerts; ++indexVertex) {
+        // order pairs by weight for each vertex
+        std::sort(allVerticesPairs[indexVertex].begin(),
+                allVerticesPairs[indexVertex].end(),
+                boneIndexWeightPair());
+        for (size_t indexGroup = 0; indexGroup < numGroups; ++indexGroup) {
+            for (size_t indexJoint = 0; indexJoint < 4; ++indexJoint) {
+                size_t indexBone = indexGroup * 4 + indexJoint;
+                size_t indexData = indexVertex + NumVerts * indexGroup;
+                if (indexBone >= allVerticesPairs[indexVertex].size()) {
+                    vertexJointData[indexData][indexJoint] = 0.f;
+                    vertexWeightData[indexData][indexJoint] = 0.f;
+                } else {
+                    vertexJointData[indexData][indexJoint] =
+                    static_cast<float>(
+                            allVerticesPairs[indexVertex][indexBone].indexJoint);
+                    vertexWeightData[indexData][indexJoint] =
+                            allVerticesPairs[indexVertex][indexBone].weight;
+                }
             }
         }
+    }
 
-    } // End: for-loop mNumMeshes
+    for (size_t idx_group = 0; idx_group < numGroups; ++idx_group) {
+        Mesh::Primitive &p = meshRef->primitives.back();
+        Ref<Accessor> vertexJointAccessor = ExportData(
+            mAsset, skinRef->id, bufferRef, aimesh->mNumVertices,
+            vertexJointData + idx_group * NumVerts,
+            AttribType::VEC4, AttribType::VEC4, ComponentType_FLOAT);
+        if (vertexJointAccessor) {
+            size_t offset = vertexJointAccessor->bufferView->byteOffset;
+            size_t bytesLen = vertexJointAccessor->bufferView->byteLength;
+            unsigned int s_bytesPerComp =
+                ComponentTypeSize(ComponentType_UNSIGNED_SHORT);
+            unsigned int bytesPerComp =
+                ComponentTypeSize(vertexJointAccessor->componentType);
+            size_t s_bytesLen = bytesLen * s_bytesPerComp / bytesPerComp;
+            Ref<Buffer> buf = vertexJointAccessor->bufferView->buffer;
+            uint8_t *arrys = new uint8_t[bytesLen];
+            unsigned int i = 0;
+            for (unsigned int j = 0; j < bytesLen; j += bytesPerComp) {
+                size_t len_p = offset + j;
+                float f_value = *(float *)&buf->GetPointer()[len_p];
+                unsigned short c = static_cast<unsigned short>(f_value);
+                memcpy(&arrys[i * s_bytesPerComp], &c, s_bytesPerComp);
+                ++i;
+            }
+            buf->ReplaceData_joint(offset, bytesLen, arrys, bytesLen);
+            vertexJointAccessor->componentType = ComponentType_UNSIGNED_SHORT;
+            vertexJointAccessor->bufferView->byteLength = s_bytesLen;
 
-    Mesh::Primitive &p = meshRef->primitives.back();
-    Ref<Accessor> vertexJointAccessor = ExportData(mAsset, skinRef->id, bufferRef, aimesh->mNumVertices,
-            vertexJointData, AttribType::VEC4, AttribType::VEC4, ComponentType_FLOAT);
-    if (vertexJointAccessor) {
-        size_t offset = vertexJointAccessor->bufferView->byteOffset;
-        size_t bytesLen = vertexJointAccessor->bufferView->byteLength;
-        unsigned int s_bytesPerComp = ComponentTypeSize(ComponentType_UNSIGNED_SHORT);
-        unsigned int bytesPerComp = ComponentTypeSize(vertexJointAccessor->componentType);
-        size_t s_bytesLen = bytesLen * s_bytesPerComp / bytesPerComp;
-        Ref<Buffer> buf = vertexJointAccessor->bufferView->buffer;
-        uint8_t *arrys = new uint8_t[bytesLen];
-        unsigned int i = 0;
-        for (unsigned int j = 0; j < bytesLen; j += bytesPerComp) {
-            size_t len_p = offset + j;
-            float f_value = *(float *)&buf->GetPointer()[len_p];
-            unsigned short c = static_cast<unsigned short>(f_value);
-            memcpy(&arrys[i * s_bytesPerComp], &c, s_bytesPerComp);
-            ++i;
+            p.attributes.joint.push_back(vertexJointAccessor);
+            delete[] arrys;
+        }
+        Ref<Accessor> vertexWeightAccessor = ExportData(
+            mAsset, skinRef->id, bufferRef, aimesh->mNumVertices,
+            vertexWeightData + idx_group * NumVerts,
+            AttribType::VEC4, AttribType::VEC4, ComponentType_FLOAT);
+        if (vertexWeightAccessor) {
+            p.attributes.weight.push_back(vertexWeightAccessor);
         }
-        buf->ReplaceData_joint(offset, bytesLen, arrys, bytesLen);
-        vertexJointAccessor->componentType = ComponentType_UNSIGNED_SHORT;
-        vertexJointAccessor->bufferView->byteLength = s_bytesLen;
-
-        p.attributes.joint.push_back(vertexJointAccessor);
-        delete[] arrys;
-    }
-
-    Ref<Accessor> vertexWeightAccessor = ExportData(mAsset, skinRef->id, bufferRef, aimesh->mNumVertices,
-            vertexWeightData, AttribType::VEC4, AttribType::VEC4, ComponentType_FLOAT);
-    if (vertexWeightAccessor) {
-        p.attributes.weight.push_back(vertexWeightAccessor);
     }
     delete[] jointsPerVertex;
     delete[] vertexWeightData;
@@ -1247,9 +1264,19 @@ void glTF2Exporter::ExportMeshes() {
             break;
         }
 
+//        /*************** Skins ****************/
+//        if (aim->HasBones()) {
+//            ExportSkin(*mAsset, aim, m, b, skinRef, inverseBindMatricesData);
+//        }
         /*************** Skins ****************/
         if (aim->HasBones()) {
-            ExportSkin(*mAsset, aim, m, b, skinRef, inverseBindMatricesData);
+            bool unlimitedBonesPerVertex =
+                this->mProperties->HasPropertyBool(
+                        AI_CONFIG_EXPORT_GLTF_UNLIMITED_SKINNING_BONES_PER_VERTEX) &&
+                this->mProperties->GetPropertyBool(
+                        AI_CONFIG_EXPORT_GLTF_UNLIMITED_SKINNING_BONES_PER_VERTEX);
+            ExportSkin(*mAsset, aim, m, b, skinRef, inverseBindMatricesData,
+                    unlimitedBonesPerVertex);
         }
 
         /*************** Targets for blendshapes ****************/

+ 13 - 0
include/assimp/config.h.in

@@ -1085,6 +1085,19 @@ enum aiComponent
  */
 #define AI_CONFIG_USE_GLTF_PBR_SPECULAR_GLOSSINESS "USE_GLTF_PBR_SPECULAR_GLOSSINESS"
 
+/** @brief Specifies whether to apply a limit on the number of four bones per vertex in skinning
+ *
+ * When this flag is not defined, all bone weights and indices are limited to a
+ * maximum of four bones for each vertex (attributes JOINT_0 and WEIGHT_0 only).
+ * By enabling this flag, the number of bones per vertex is unlimited.
+ * In both cases, indices and bone weights are sorted by weight in descending order.
+ * In the case of the limit of up to four bones, a maximum of the four largest values are exported.
+ * Weights are not normalized.
+ * Property type: Bool. Default value: false.
+ */
+#define AI_CONFIG_EXPORT_GLTF_UNLIMITED_SKINNING_BONES_PER_VERTEX \
+        "USE_UNLIMITED_BONES_PER VERTEX"
+
 /**
  * @brief Specifies the blob name, assimp uses for exporting.
  * 

BIN
test/models/glTF2/simple_skin/quad_skin.glb


+ 139 - 0
test/unit/utglTF2ImportExport.cpp

@@ -43,6 +43,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
 #include <assimp/commonMetaData.h>
 #include <assimp/postprocess.h>
+#include <assimp/config.h>
 #include <assimp/scene.h>
 #include <assimp/Exporter.hpp>
 #include <assimp/Importer.hpp>
@@ -504,6 +505,144 @@ TEST_F(utglTF2ImportExport, bug_import_simple_skin) {
     EXPECT_NE(nullptr, scene);
 }
 
+bool checkSkinnedScene(const aiScene *scene){
+    float eps = 0.001;
+    bool result = true;
+    EXPECT_EQ(scene->mNumMeshes, 1u);
+    EXPECT_EQ(scene->mMeshes[0]->mNumBones, 10u);
+    EXPECT_EQ(scene->mMeshes[0]->mNumVertices, 4u);
+    EXPECT_LT(abs(scene->mMeshes[0]->mVertices[0].x - -1), eps);
+    EXPECT_LT(abs(scene->mMeshes[0]->mVertices[0].y - -1), eps);
+    EXPECT_LT(abs(scene->mMeshes[0]->mVertices[0].z - 0), eps);
+    EXPECT_LT(abs(scene->mMeshes[0]->mVertices[1].x - 1), eps);
+    EXPECT_LT(abs(scene->mMeshes[0]->mVertices[1].y - -1), eps);
+    EXPECT_LT(abs(scene->mMeshes[0]->mVertices[1].z - 0), eps);
+    EXPECT_LT(abs(scene->mMeshes[0]->mVertices[2].x - 1), eps);
+    EXPECT_LT(abs(scene->mMeshes[0]->mVertices[2].y - 1), eps);
+    EXPECT_LT(abs(scene->mMeshes[0]->mVertices[2].z - 0), eps);
+    EXPECT_LT(abs(scene->mMeshes[0]->mVertices[3].x - -1), eps);
+    EXPECT_LT(abs(scene->mMeshes[0]->mVertices[3].y - 1), eps);
+    EXPECT_LT(abs(scene->mMeshes[0]->mVertices[3].z - 0), eps);
+
+    uint numWeights[] = {4u, 4u, 4u, 4u, 2u , 1u, 1u, 2u, 1u, 1u};
+    float weights[10][4] = {{0.207, 0.291, 0.057, 0.303},
+                        {0.113, 0.243, 0.499, 0.251},
+                        {0.005, 0.010, 0.041, 0.093},
+                        {0.090, 0.234, 0.404, 0.243},
+                        {0.090, 0.222, 0.000, 0.000},
+                        {0.216, 0.000, 0.000, 0.000},
+                        {0.058, 0.000, 0.000, 0.000},
+                        {0.086, 0.000, 0.000, 0.111},
+                        {0.088, 0.000, 0.000, 0.000},
+                        {0.049, 0.000, 0.000, 0.000}};
+    for (size_t boneIndex = 0; boneIndex < 10u; ++boneIndex) {
+        EXPECT_EQ(scene->mMeshes[0]->mBones[boneIndex]->mNumWeights, numWeights[boneIndex]);
+        std::map<uint, float> map;
+        for (size_t jointIndex = 0; jointIndex < scene->mMeshes[0]->mBones[boneIndex]->mNumWeights; ++jointIndex){
+            auto key = scene->mMeshes[0]->mBones[boneIndex]->mWeights[jointIndex].mVertexId;
+            auto weight = scene->mMeshes[0]->mBones[boneIndex]->mWeights[jointIndex].mWeight;
+            map[key] = weight;
+        }
+
+        for (size_t jointIndex = 0; jointIndex < scene->mMeshes[0]->mBones[boneIndex]->mNumWeights; ++jointIndex) {
+            auto weight = map[jointIndex];
+            EXPECT_LT(abs(ai_real(weight) - ai_real(weights[boneIndex][jointIndex])), 0.002);
+        }
+
+    }
+
+    return result;
+}
+
+void checkSkinnedSceneLimited(const aiScene *scene){
+    float eps = 0.001;
+    EXPECT_EQ(scene->mNumMeshes, 1u);
+    EXPECT_EQ(scene->mMeshes[0]->mNumBones, 10u);
+    EXPECT_EQ(scene->mMeshes[0]->mNumVertices, 4u);
+    EXPECT_LT(abs(scene->mMeshes[0]->mVertices[0].x - -1), eps);
+    EXPECT_LT(abs(scene->mMeshes[0]->mVertices[0].y - -1), eps);
+    EXPECT_LT(abs(scene->mMeshes[0]->mVertices[0].z - 0), eps);
+    EXPECT_LT(abs(scene->mMeshes[0]->mVertices[1].x - 1), eps);
+    EXPECT_LT(abs(scene->mMeshes[0]->mVertices[1].y - -1), eps);
+    EXPECT_LT(abs(scene->mMeshes[0]->mVertices[1].z - 0), eps);
+    EXPECT_LT(abs(scene->mMeshes[0]->mVertices[2].x - 1), eps);
+    EXPECT_LT(abs(scene->mMeshes[0]->mVertices[2].y - 1), eps);
+    EXPECT_LT(abs(scene->mMeshes[0]->mVertices[2].z - 0), eps);
+    EXPECT_LT(abs(scene->mMeshes[0]->mVertices[3].x - -1), eps);
+    EXPECT_LT(abs(scene->mMeshes[0]->mVertices[3].y - 1), eps);
+    EXPECT_LT(abs(scene->mMeshes[0]->mVertices[3].z - 0), eps);
+
+    uint numWeights[] = {4u, 4u, 1u, 4u, 1u , 1u, 1u, 1u, 1u, 1u};
+    float weights[10][4] = {{0.207, 0.291, 0.057, 0.303},
+                            {0.113, 0.243, 0.499, 0.251},
+                            {0.000, 0.000, 0.041, 0.000},
+                            {0.090, 0.234, 0.404, 0.243},
+                            {0.000, 0.222, 0.000, 0.000},
+                            {0.216, 0.000, 0.000, 0.000},
+                            {0.000, 0.000, 0.000, 0.000},
+                            {0.000, 0.000, 0.000, 0.111},
+                            {0.000, 0.000, 0.000, 0.000},
+                            {0.000, 0.000, 0.000, 0.000}};
+    for (size_t boneIndex = 0; boneIndex < 10u; ++boneIndex) {
+        EXPECT_EQ(scene->mMeshes[0]->mBones[boneIndex]->mNumWeights, numWeights[boneIndex]);
+        std::map<uint, float> map;
+        for (size_t jointIndex = 0; jointIndex < scene->mMeshes[0]->mBones[boneIndex]->mNumWeights; ++jointIndex){
+            auto key = scene->mMeshes[0]->mBones[boneIndex]->mWeights[jointIndex].mVertexId;
+            auto weight = scene->mMeshes[0]->mBones[boneIndex]->mWeights[jointIndex].mWeight;
+            map[key] = weight;
+        }
+        for (size_t jointIndex = 0; jointIndex < scene->mMeshes[0]->mBones[boneIndex]->mNumWeights; ++jointIndex) {
+            auto weight = map[jointIndex];
+            EXPECT_LT(std::abs(ai_real(weight) - ai_real(weights[boneIndex][jointIndex])), 0.002);
+        }
+    }
+}
+
+TEST_F(utglTF2ImportExport, bug_import_simple_skin2) {
+    Assimp::Importer importer;
+    Assimp::Exporter exporter;
+    const aiScene *scene = importer.ReadFile(
+            ASSIMP_TEST_MODELS_DIR "/glTF2/simple_skin/quad_skin.glb",
+            aiProcess_ValidateDataStructure);
+    checkSkinnedScene(scene);
+
+    ASSERT_EQ(aiReturn_SUCCESS, exporter.Export(scene, "glb2",
+            ASSIMP_TEST_MODELS_DIR "/glTF2/simple_skin/quad_four_out.glb"));
+    ASSERT_EQ(aiReturn_SUCCESS, exporter.Export(scene, "gltf2",
+            ASSIMP_TEST_MODELS_DIR "/glTF2/simple_skin/quad_four_out.gltf"));
+
+    // enable more than four bones per vertex
+    Assimp::ExportProperties properties = Assimp::ExportProperties();
+    properties.SetPropertyBool(
+      AI_CONFIG_EXPORT_GLTF_UNLIMITED_SKINNING_BONES_PER_VERTEX, true);
+    ASSERT_EQ(aiReturn_SUCCESS, exporter.Export(scene, "glb2",
+            ASSIMP_TEST_MODELS_DIR "/glTF2/simple_skin/quad_all_out.glb", 0u, &properties));
+    ASSERT_EQ(aiReturn_SUCCESS, exporter.Export(scene, "gltf2",
+            ASSIMP_TEST_MODELS_DIR "/glTF2/simple_skin/quad_all_out.gltf", 0u, &properties));
+
+    // check skinning data of both exported files for limited number bones per vertex
+    const aiScene *limitedSceneImported = importer.ReadFile(
+            ASSIMP_TEST_MODELS_DIR "/glTF2/simple_skin/quad_four_out.gltf",
+            aiProcess_ValidateDataStructure);
+    checkSkinnedSceneLimited(limitedSceneImported);
+    limitedSceneImported = importer.ReadFile(
+            ASSIMP_TEST_MODELS_DIR "/glTF2/simple_skin/quad_four_out.glb",
+            aiProcess_ValidateDataStructure);
+    checkSkinnedSceneLimited(limitedSceneImported);
+
+    // check skinning data of both exported files for unlimited number bones per vertex
+    const aiScene *sceneImported = importer.ReadFile(
+            ASSIMP_TEST_MODELS_DIR "/glTF2/simple_skin/quad_all_out.gltf",
+            aiProcess_ValidateDataStructure);
+    checkSkinnedScene(sceneImported);
+    sceneImported = importer.ReadFile(
+            ASSIMP_TEST_MODELS_DIR "/glTF2/simple_skin/quad_all_out.glb",
+            aiProcess_ValidateDataStructure);
+    checkSkinnedScene(sceneImported);
+
+
+}
+
 TEST_F(utglTF2ImportExport, import_cameras) {
     Assimp::Importer importer;
     const aiScene *scene = importer.ReadFile(ASSIMP_TEST_MODELS_DIR "/glTF2/cameras/Cameras.gltf",