Coverage Report

Created: 2026-09-14 07:44

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/assimp/code/PostProcessing/SplitByBoneCountProcess.cpp
Line
Count
Source
1
/*
2
Open Asset Import Library (assimp)
3
----------------------------------------------------------------------
4
5
Copyright (c) 2006-2026, assimp team
6
7
All rights reserved.
8
9
Redistribution and use of this software in source and binary forms,
10
with or without modification, are permitted provided that the
11
following conditions are met:
12
13
* Redistributions of source code must retain the above
14
  copyright notice, this list of conditions and the
15
  following disclaimer.
16
17
* Redistributions in binary form must reproduce the above
18
  copyright notice, this list of conditions and the
19
  following disclaimer in the documentation and/or other
20
  materials provided with the distribution.
21
22
* Neither the name of the assimp team, nor the names of its
23
  contributors may be used to endorse or promote products
24
  derived from this software without specific prior
25
  written permission of the assimp team.
26
27
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
28
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
29
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
30
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
31
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
32
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
33
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
34
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
35
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
36
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
37
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
38
39
----------------------------------------------------------------------
40
*/
41
42
/// @file SplitByBoneCountProcess.cpp
43
/// Implementation of the SplitByBoneCount postprocessing step
44
45
// internal headers of the post-processing framework
46
#include "SplitByBoneCountProcess.h"
47
#include <assimp/postprocess.h>
48
#include <assimp/DefaultLogger.hpp>
49
50
#include <limits>
51
#include <assimp/TinyFormatter.h>
52
#include <assimp/Exceptional.h>
53
#include <set>
54
55
using namespace Assimp;
56
using namespace Assimp::Formatter;
57
58
// ------------------------------------------------------------------------------------------------
59
// Constructor
60
151k
SplitByBoneCountProcess::SplitByBoneCountProcess() : mMaxBoneCount(AI_SBBC_DEFAULT_MAX_BONES) {}
61
62
// ------------------------------------------------------------------------------------------------
63
// Returns whether the processing step is present in the given flag.
64
121k
bool SplitByBoneCountProcess::IsActive( unsigned int pFlags) const {
65
121k
    return !!(pFlags & aiProcess_SplitByBoneCount);
66
121k
}
67
68
// ------------------------------------------------------------------------------------------------
69
// Updates internal properties
70
954
void SplitByBoneCountProcess::SetupProperties(const Importer* pImp) {
71
954
    mMaxBoneCount = pImp->GetPropertyInteger(AI_CONFIG_PP_SBBC_MAX_BONES,AI_SBBC_DEFAULT_MAX_BONES);
72
954
}
73
74
// ------------------------------------------------------------------------------------------------
75
// Executes the post processing step on the given imported data.
76
954
void SplitByBoneCountProcess::Execute( aiScene* pScene) {
77
954
    ASSIMP_LOG_DEBUG("SplitByBoneCountProcess begin");
78
79
    // early out
80
954
    bool isNecessary = false;
81
2.75k
    for( unsigned int a = 0; a < pScene->mNumMeshes; ++a)
82
1.92k
        if( pScene->mMeshes[a]->mNumBones > mMaxBoneCount ) {
83
124
            isNecessary = true;
84
124
            break;
85
124
        }
86
87
954
    if( !isNecessary ) {
88
830
        ASSIMP_LOG_DEBUG("SplitByBoneCountProcess early-out: no meshes with more than ", mMaxBoneCount, " bones." );
89
830
        return;
90
830
    }
91
92
    // we need to do something. Let's go.
93
124
    mSubMeshIndices.clear();
94
124
    mSubMeshIndices.resize( pScene->mNumMeshes);
95
96
    // build a new array of meshes for the scene
97
124
    std::vector<aiMesh*> meshes;
98
99
248
    for( unsigned int a = 0; a < pScene->mNumMeshes; ++a) {
100
124
        aiMesh* srcMesh = pScene->mMeshes[a];
101
102
124
        std::vector<aiMesh*> newMeshes;
103
124
        SplitMesh( pScene->mMeshes[a], newMeshes);
104
105
        // mesh was split
106
124
        if( !newMeshes.empty() ) {
107
            // store new meshes and indices of the new meshes
108
3.02k
            for( unsigned int b = 0; b < newMeshes.size(); ++b) {
109
2.89k
                mSubMeshIndices[a].push_back( static_cast<unsigned int>(meshes.size()));
110
2.89k
                meshes.push_back( newMeshes[b]);
111
2.89k
            }
112
113
            // and destroy the source mesh. It should be completely contained inside the new submeshes
114
124
            delete srcMesh;
115
124
        } else {
116
            // Mesh is kept unchanged - store it's new place in the mesh array
117
0
            mSubMeshIndices[a].push_back( static_cast<unsigned int>(meshes.size()));
118
0
            meshes.push_back( srcMesh);
119
0
        }
120
124
    }
121
122
    // rebuild the scene's mesh array
123
124
    pScene->mNumMeshes = static_cast<unsigned int>(meshes.size());
124
124
    delete [] pScene->mMeshes;
125
124
    pScene->mMeshes = new aiMesh*[pScene->mNumMeshes];
126
124
    std::copy( meshes.begin(), meshes.end(), pScene->mMeshes);
127
128
    // recurse through all nodes and translate the node's mesh indices to fit the new mesh array
129
124
    UpdateNode( pScene->mRootNode);
130
131
124
    ASSIMP_LOG_DEBUG( "SplitByBoneCountProcess end: split ", mSubMeshIndices.size(), " meshes into ", meshes.size(), " submeshes." );
132
124
}
133
134
// ------------------------------------------------------------------------------------------------
135
// Splits the given mesh by bone count.
136
124
void SplitByBoneCountProcess::SplitMesh( const aiMesh* pMesh, std::vector<aiMesh*>& poNewMeshes) const {
137
    // skip if not necessary
138
124
    if( pMesh->mNumBones <= mMaxBoneCount ) {
139
0
        return;
140
0
    }
141
142
    // necessary optimisation: build a list of all affecting bones for each vertex
143
    // TODO: (thom) maybe add a custom allocator here to avoid allocating tens of thousands of small arrays
144
124
    typedef std::pair<unsigned int, float> BoneWeight;
145
124
    std::vector< std::vector<BoneWeight> > vertexBones( pMesh->mNumVertices);
146
9.09k
    for( unsigned int a = 0; a < pMesh->mNumBones; ++a) {
147
8.97k
        const aiBone* bone = pMesh->mBones[a];
148
224k
        for( unsigned int b = 0; b < bone->mNumWeights; ++b) {
149
215k
            if (bone->mWeights[b].mWeight > 0.0f) {
150
215k
                int vertexId = bone->mWeights[b].mVertexId;
151
215k
                vertexBones[vertexId].emplace_back(a, bone->mWeights[b].mWeight);
152
215k
                if (vertexBones[vertexId].size() > mMaxBoneCount) {
153
0
                    throw DeadlyImportError("SplitByBoneCountProcess: Single face requires more bones than specified max bone count!");
154
0
                }
155
215k
            }
156
215k
        }
157
8.97k
    }
158
159
124
    unsigned int numFacesHandled = 0;
160
124
    std::vector<bool> isFaceHandled( pMesh->mNumFaces, false);
161
3.02k
    while( numFacesHandled < pMesh->mNumFaces ) {
162
        // which bones are used in the current submesh
163
2.89k
        unsigned int numBones = 0;
164
2.89k
        std::vector<bool> isBoneUsed( pMesh->mNumBones, false);
165
        // indices of the faces which are going to go into this submesh
166
2.89k
        IndexArray subMeshFaces;
167
2.89k
        subMeshFaces.reserve( pMesh->mNumFaces);
168
        // accumulated vertex count of all the faces in this submesh
169
2.89k
        unsigned int numSubMeshVertices = 0;
170
171
        // add faces to the new submesh as long as all bones affecting the faces' vertices fit in the limit
172
7.21M
        for( unsigned int a = 0; a < pMesh->mNumFaces; ++a) {
173
            // skip if the face is already stored in a submesh
174
7.21M
            if( isFaceHandled[a] ) {
175
3.58M
                continue;
176
3.58M
            }
177
            // a small local set of new bones for the current face. State of all used bones for that face
178
            // can only be updated AFTER the face is completely analysed. Thanks to imre for the fix.
179
3.62M
            std::set<unsigned int> newBonesAtCurrentFace;
180
181
3.62M
            const aiFace& face = pMesh->mFaces[a];
182
            // check every vertex if its bones would still fit into the current submesh
183
14.5M
            for( unsigned int b = 0; b < face.mNumIndices; ++b ) {
184
10.8M
                const std::vector<BoneWeight>& vb = vertexBones[face.mIndices[b]];
185
21.7M
                for( unsigned int c = 0; c < vb.size(); ++c) {
186
10.8M
                    unsigned int boneIndex = vb[c].first;
187
10.8M
                    if( !isBoneUsed[boneIndex] ) {
188
10.6M
                        newBonesAtCurrentFace.insert(boneIndex);
189
10.6M
                    }
190
10.8M
                }
191
10.8M
            }
192
193
            // leave out the face if the new bones required for this face don't fit the bone count limit anymore
194
3.62M
            if( numBones + newBonesAtCurrentFace.size() > mMaxBoneCount ) {
195
3.55M
                continue;
196
3.55M
            }
197
198
            // mark all new bones as necessary
199
81.0k
            for (std::set<unsigned int>::iterator it = newBonesAtCurrentFace.begin(); it != newBonesAtCurrentFace.end(); ++it) {
200
8.97k
                if (!isBoneUsed[*it]) {
201
8.97k
                    isBoneUsed[*it] = true;
202
8.97k
                    ++numBones;
203
8.97k
                }
204
8.97k
            }
205
206
            // store the face index and the vertex count
207
72.0k
            subMeshFaces.push_back( a);
208
72.0k
            numSubMeshVertices += face.mNumIndices;
209
210
            // remember that this face is handled
211
72.0k
            isFaceHandled[a] = true;
212
72.0k
            ++numFacesHandled;
213
72.0k
        }
214
215
        // create a new mesh to hold this subset of the source mesh
216
2.89k
        aiMesh* newMesh = new aiMesh;
217
2.89k
        if( pMesh->mName.length > 0 ) {
218
0
            newMesh->mName.Set( format() << pMesh->mName.data << "_sub" << poNewMeshes.size());
219
0
        }
220
2.89k
        newMesh->mMaterialIndex = pMesh->mMaterialIndex;
221
2.89k
        newMesh->mPrimitiveTypes = pMesh->mPrimitiveTypes;
222
2.89k
        poNewMeshes.emplace_back( newMesh);
223
224
        // create all the arrays for this mesh if the old mesh contained them
225
2.89k
        newMesh->mNumVertices = numSubMeshVertices;
226
2.89k
        newMesh->mNumFaces = static_cast<unsigned int>(subMeshFaces.size());
227
2.89k
        newMesh->mVertices = new aiVector3D[newMesh->mNumVertices];
228
2.89k
        if( pMesh->HasNormals() ) {
229
2.67k
            newMesh->mNormals = new aiVector3D[newMesh->mNumVertices];
230
2.67k
        }
231
2.89k
        if( pMesh->HasTangentsAndBitangents() ) {
232
0
            newMesh->mTangents = new aiVector3D[newMesh->mNumVertices];
233
0
            newMesh->mBitangents = new aiVector3D[newMesh->mNumVertices];
234
0
        }
235
26.0k
        for( unsigned int a = 0; a < AI_MAX_NUMBER_OF_TEXTURECOORDS; ++a ) {
236
23.1k
            if( pMesh->HasTextureCoords( a) ) {
237
17
                newMesh->mTextureCoords[a] = new aiVector3D[newMesh->mNumVertices];
238
17
            }
239
23.1k
            newMesh->mNumUVComponents[a] = pMesh->mNumUVComponents[a];
240
23.1k
        }
241
26.0k
        for( unsigned int a = 0; a < AI_MAX_NUMBER_OF_COLOR_SETS; ++a ) {
242
23.1k
            if( pMesh->HasVertexColors( a) ) {
243
0
                newMesh->mColors[a] = new aiColor4D[newMesh->mNumVertices];
244
0
            }
245
23.1k
        }
246
247
        // and copy over the data, generating faces with linear indices along the way
248
2.89k
        newMesh->mFaces = new aiFace[subMeshFaces.size()];
249
2.89k
        unsigned int nvi = 0; // next vertex index
250
2.89k
        IndexArray previousVertexIndices( numSubMeshVertices, std::numeric_limits<unsigned int>::max()); // per new vertex: its index in the source mesh
251
74.9k
        for( unsigned int a = 0; a < subMeshFaces.size(); ++a ) {
252
72.0k
            const aiFace& srcFace = pMesh->mFaces[subMeshFaces[a]];
253
72.0k
            aiFace& dstFace = newMesh->mFaces[a];
254
72.0k
            dstFace.mNumIndices = srcFace.mNumIndices;
255
72.0k
            dstFace.mIndices = new unsigned int[dstFace.mNumIndices];
256
257
            // accumulate linearly all the vertices of the source face
258
288k
            for( unsigned int b = 0; b < dstFace.mNumIndices; ++b ) {
259
216k
                unsigned int srcIndex = srcFace.mIndices[b];
260
216k
                dstFace.mIndices[b] = nvi;
261
216k
                previousVertexIndices[nvi] = srcIndex;
262
263
216k
                newMesh->mVertices[nvi] = pMesh->mVertices[srcIndex];
264
216k
                if( pMesh->HasNormals() ) {
265
203k
                    newMesh->mNormals[nvi] = pMesh->mNormals[srcIndex];
266
203k
                }
267
216k
                if( pMesh->HasTangentsAndBitangents() ) {
268
0
                    newMesh->mTangents[nvi] = pMesh->mTangents[srcIndex];
269
0
                    newMesh->mBitangents[nvi] = pMesh->mBitangents[srcIndex];
270
0
                }
271
1.94M
                for( unsigned int c = 0; c < AI_MAX_NUMBER_OF_TEXTURECOORDS; ++c ) {
272
1.72M
                    if( pMesh->HasTextureCoords( c) ) {
273
1.15k
                        newMesh->mTextureCoords[c][nvi] = pMesh->mTextureCoords[c][srcIndex];
274
1.15k
                    }
275
1.72M
                }
276
1.94M
                for( unsigned int c = 0; c < AI_MAX_NUMBER_OF_COLOR_SETS; ++c ) {
277
1.72M
                    if( pMesh->HasVertexColors( c) ) {
278
0
                        newMesh->mColors[c][nvi] = pMesh->mColors[c][srcIndex];
279
0
                    }
280
1.72M
                }
281
282
216k
                nvi++;
283
216k
            }
284
72.0k
        }
285
286
2.89k
        ai_assert( nvi == numSubMeshVertices );
287
288
        // Create the bones for the new submesh: first create the bone array
289
2.89k
        newMesh->mNumBones = 0;
290
2.89k
        newMesh->mBones = new aiBone*[numBones];
291
292
2.89k
        std::vector<unsigned int> mappedBoneIndex( pMesh->mNumBones, std::numeric_limits<unsigned int>::max());
293
904k
        for( unsigned int a = 0; a < pMesh->mNumBones; ++a ) {
294
901k
            if( !isBoneUsed[a] ) {
295
892k
                continue;
296
892k
            }
297
298
            // create the new bone
299
8.97k
            const aiBone* srcBone = pMesh->mBones[a];
300
8.97k
            aiBone* dstBone = new aiBone;
301
8.97k
            mappedBoneIndex[a] = newMesh->mNumBones;
302
8.97k
            newMesh->mBones[newMesh->mNumBones++] = dstBone;
303
8.97k
            dstBone->mName = srcBone->mName;
304
8.97k
            dstBone->mOffsetMatrix = srcBone->mOffsetMatrix;
305
8.97k
            dstBone->mNumWeights = 0;
306
8.97k
        }
307
308
2.89k
        ai_assert( newMesh->mNumBones == numBones );
309
310
        // iterate over all new vertices and count which bones affected its old vertex in the source mesh
311
219k
        for( unsigned int a = 0; a < numSubMeshVertices; ++a ) {
312
216k
            unsigned int oldIndex = previousVertexIndices[a];
313
216k
            const std::vector<BoneWeight>& bonesOnThisVertex = vertexBones[oldIndex];
314
315
432k
            for( unsigned int b = 0; b < bonesOnThisVertex.size(); ++b ) {
316
216k
                unsigned int newBoneIndex = mappedBoneIndex[ bonesOnThisVertex[b].first ];
317
216k
                if( newBoneIndex != std::numeric_limits<unsigned int>::max() ) {
318
216k
                    newMesh->mBones[newBoneIndex]->mNumWeights++;
319
216k
                }
320
216k
            }
321
216k
        }
322
323
        // allocate all bone weight arrays accordingly
324
11.8k
        for( unsigned int a = 0; a < newMesh->mNumBones; ++a ) {
325
8.97k
            aiBone* bone = newMesh->mBones[a];
326
8.97k
            ai_assert( bone->mNumWeights > 0 );
327
8.97k
            bone->mWeights = new aiVertexWeight[bone->mNumWeights];
328
8.97k
            bone->mNumWeights = 0; // for counting up in the next step
329
8.97k
        }
330
331
        // now copy all the bone vertex weights for all the vertices which made it into the new submesh
332
219k
        for( unsigned int a = 0; a < numSubMeshVertices; ++a) {
333
            // find the source vertex for it in the source mesh
334
216k
            unsigned int previousIndex = previousVertexIndices[a];
335
            // these bones were affecting it
336
216k
            const std::vector<BoneWeight>& bonesOnThisVertex = vertexBones[previousIndex];
337
            // all of the bones affecting it should be present in the new submesh, or else
338
            // the face it comprises shouldn't be present
339
432k
            for( unsigned int b = 0; b < bonesOnThisVertex.size(); ++b) {
340
216k
                unsigned int newBoneIndex = mappedBoneIndex[ bonesOnThisVertex[b].first ];
341
216k
                ai_assert( newBoneIndex != std::numeric_limits<unsigned int>::max() );
342
216k
                aiVertexWeight* dstWeight = newMesh->mBones[newBoneIndex]->mWeights + newMesh->mBones[newBoneIndex]->mNumWeights;
343
216k
                newMesh->mBones[newBoneIndex]->mNumWeights++;
344
345
216k
                dstWeight->mVertexId = a;
346
216k
                dstWeight->mWeight = bonesOnThisVertex[b].second;
347
216k
            }
348
216k
        }
349
350
        // ... and copy all the morph targets for all the vertices which made it into the new submesh
351
2.89k
        if (pMesh->mNumAnimMeshes > 0) {
352
0
            newMesh->mNumAnimMeshes = pMesh->mNumAnimMeshes;
353
0
            newMesh->mAnimMeshes = new aiAnimMesh*[newMesh->mNumAnimMeshes];
354
355
0
            for (unsigned int morphIdx = 0; morphIdx < newMesh->mNumAnimMeshes; ++morphIdx) {
356
0
                aiAnimMesh* origTarget = pMesh->mAnimMeshes[morphIdx];
357
0
                aiAnimMesh* newTarget = new aiAnimMesh;
358
0
                newTarget->mName = origTarget->mName;
359
0
                newTarget->mWeight = origTarget->mWeight;
360
0
                newTarget->mNumVertices = numSubMeshVertices;
361
0
                newTarget->mVertices = new aiVector3D[numSubMeshVertices];
362
0
                newMesh->mAnimMeshes[morphIdx] = newTarget;
363
364
0
                if (origTarget->HasNormals()) {
365
0
                    newTarget->mNormals = new aiVector3D[numSubMeshVertices];
366
0
                }
367
368
0
                if (origTarget->HasTangentsAndBitangents()) {
369
0
                    newTarget->mTangents = new aiVector3D[numSubMeshVertices];
370
0
                    newTarget->mBitangents = new aiVector3D[numSubMeshVertices];
371
0
                }
372
373
0
                for( unsigned int vi = 0; vi < numSubMeshVertices; ++vi) {
374
                    // find the source vertex for it in the source mesh
375
0
                    unsigned int previousIndex = previousVertexIndices[vi];
376
0
                    newTarget->mVertices[vi] = origTarget->mVertices[previousIndex];
377
378
0
                    if (newTarget->HasNormals()) {
379
0
                        newTarget->mNormals[vi] = origTarget->mNormals[previousIndex];
380
0
                    }
381
0
                    if (newTarget->HasTangentsAndBitangents()) {
382
0
                        newTarget->mTangents[vi] = origTarget->mTangents[previousIndex];
383
0
                        newTarget->mBitangents[vi] = origTarget->mBitangents[previousIndex];
384
0
                    }
385
0
                }
386
0
            }
387
0
        }
388
389
        // I have the strange feeling that this will break apart at some point in time...
390
2.89k
    }
391
124
}
392
393
// ------------------------------------------------------------------------------------------------
394
// Recursively updates the node's mesh list to account for the changed mesh list
395
9.28k
void SplitByBoneCountProcess::UpdateNode( aiNode* pNode) const {
396
    // rebuild the node's mesh index list
397
9.28k
    if( pNode->mNumMeshes != 0 ) {
398
124
        IndexArray newMeshList;
399
248
        for( unsigned int a = 0; a < pNode->mNumMeshes; ++a) {
400
124
            unsigned int srcIndex = pNode->mMeshes[a];
401
124
            const IndexArray& replaceMeshes = mSubMeshIndices[srcIndex];
402
124
            newMeshList.insert( newMeshList.end(), replaceMeshes.begin(), replaceMeshes.end());
403
124
        }
404
405
124
        delete [] pNode->mMeshes;
406
124
        pNode->mNumMeshes = static_cast<unsigned int>(newMeshList.size());
407
124
        pNode->mMeshes = new unsigned int[pNode->mNumMeshes];
408
124
        std::copy( newMeshList.begin(), newMeshList.end(), pNode->mMeshes);
409
124
    }
410
411
    // do that also recursively for all children
412
18.4k
    for( unsigned int a = 0; a < pNode->mNumChildren; ++a ) {
413
9.16k
        UpdateNode( pNode->mChildren[a]);
414
9.16k
    }
415
9.28k
}