Coverage Report

Created: 2025-11-11 06:59

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/assimp/code/PostProcessing/TriangulateProcess.cpp
Line
Count
Source
1
/*
2
---------------------------------------------------------------------------
3
Open Asset Import Library (assimp)
4
---------------------------------------------------------------------------
5
6
Copyright (c) 2006-2025, assimp team
7
8
All rights reserved.
9
10
Redistribution and use of this software in source and binary forms,
11
with or without modification, are permitted provided that the following
12
conditions are met:
13
14
* Redistributions of source code must retain the above
15
  copyright notice, this list of conditions and the
16
  following disclaimer.
17
18
* Redistributions in binary form must reproduce the above
19
  copyright notice, this list of conditions and the
20
  following disclaimer in the documentation and/or other
21
  materials provided with the distribution.
22
23
* Neither the name of the assimp team, nor the names of its
24
  contributors may be used to endorse or promote products
25
  derived from this software without specific prior
26
  written permission of the assimp team.
27
28
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
29
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
30
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
31
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
32
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
33
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
34
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
35
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
36
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
37
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
38
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
39
---------------------------------------------------------------------------
40
*/
41
42
/** @file  TriangulateProcess.cpp
43
 *  @brief Implementation of the post processing step to split up
44
 *    all faces with more than three indices into triangles.
45
 *
46
 *
47
 *  The triangulation algorithm will handle concave or convex polygons.
48
 *  Self-intersecting or non-planar polygons are not rejected, but
49
 *  they're probably not triangulated correctly.
50
 *
51
 * DEBUG SWITCHES - do not enable any of them in release builds:
52
 *
53
 * AI_BUILD_TRIANGULATE_COLOR_FACE_WINDING
54
 *   - generates vertex colors to represent the face winding order.
55
 *     the first vertex of a polygon becomes red, the last blue.
56
 * AI_BUILD_TRIANGULATE_DEBUG_POLYS
57
 *   - dump all polygons and their triangulation sequences to
58
 *     a file
59
 */
60
#ifndef ASSIMP_BUILD_NO_TRIANGULATE_PROCESS
61
62
#include "PostProcessing/TriangulateProcess.h"
63
#include "PostProcessing/ProcessHelper.h"
64
#include "Common/PolyTools.h"
65
#include "contrib/earcut-hpp/earcut.hpp"
66
67
#include <memory>
68
#include <cstdint>
69
70
//#define AI_BUILD_TRIANGULATE_COLOR_FACE_WINDING
71
//#define AI_BUILD_TRIANGULATE_DEBUG_POLYS
72
73
#define POLY_GRID_Y 40
74
#define POLY_GRID_X 70
75
#define POLY_GRID_XPAD 20
76
#define POLY_OUTPUT_FILE "assimp_polygons_debug.txt"
77
78
namespace mapbox::util {
79
80
template <>
81
struct nth<0, aiVector2D> {
82
463k
    inline static auto get(const aiVector2D& t) {
83
463k
        return t.x;
84
463k
    }
85
};
86
template <>
87
struct nth<1, aiVector2D> {
88
463k
    inline static auto get(const aiVector2D& t) {
89
463k
        return t.y;
90
463k
    }
91
};
92
93
} // namespace mapbox::util
94
95
using namespace Assimp;
96
97
namespace {
98
99
    /**
100
     * @brief Helper struct used to simplify NGON encoding functions.
101
     */
102
    struct NGONEncoder {
103
4.84k
        NGONEncoder() : mLastNGONFirstIndex((unsigned int)-1) {}
104
105
        /**
106
         * @brief Encode the current triangle, and make sure it is recognized as a triangle.
107
         *
108
         * This method will rotate indices in tri if needed in order to avoid tri to be considered
109
         * part of the previous ngon. This method is to be used whenever you want to emit a real triangle,
110
         * and make sure it is seen as a triangle.
111
         *
112
         * @param tri Triangle to encode.
113
         */
114
25.7k
        void ngonEncodeTriangle(aiFace * tri) {
115
25.7k
            ai_assert(tri->mNumIndices == 3);
116
117
            // Rotate indices in new triangle to avoid ngon encoding false ngons
118
            // Otherwise, the new triangle would be considered part of the previous NGON.
119
25.7k
            if (isConsideredSameAsLastNgon(tri)) {
120
1.16k
                std::swap(tri->mIndices[0], tri->mIndices[2]);
121
1.16k
                std::swap(tri->mIndices[1], tri->mIndices[2]);
122
1.16k
            }
123
124
25.7k
            mLastNGONFirstIndex = tri->mIndices[0];
125
25.7k
        }
126
127
        /**
128
         * @brief Encode a quad (2 triangles) in ngon encoding, and make sure they are seen as a single ngon.
129
         *
130
         * @param tri1 First quad triangle
131
         * @param tri2 Second quad triangle
132
         *
133
         * @pre Triangles must be properly fanned from the most appropriate vertex.
134
         */
135
87.3k
        void ngonEncodeQuad(aiFace *tri1, aiFace *tri2) {
136
87.3k
            ai_assert(tri1->mNumIndices == 3);
137
87.3k
            ai_assert(tri2->mNumIndices == 3);
138
87.3k
            ai_assert(tri1->mIndices[0] == tri2->mIndices[0]);
139
140
            // If the selected fanning vertex is the same as the previously
141
            // emitted ngon, we use the opposite vertex which also happens to work
142
            // for tri-fanning a concave quad.
143
            // ref: https://github.com/assimp/assimp/pull/3695#issuecomment-805999760
144
87.3k
            if (isConsideredSameAsLastNgon(tri1)) {
145
                // Right-rotate indices for tri1 (index 2 becomes the new fanning vertex)
146
0
                std::swap(tri1->mIndices[0], tri1->mIndices[2]);
147
0
                std::swap(tri1->mIndices[1], tri1->mIndices[2]);
148
149
                // Left-rotate indices for tri2 (index 2 becomes the new fanning vertex)
150
0
                std::swap(tri2->mIndices[1], tri2->mIndices[2]);
151
0
                std::swap(tri2->mIndices[0], tri2->mIndices[2]);
152
153
0
                ai_assert(tri1->mIndices[0] == tri2->mIndices[0]);
154
0
            }
155
156
87.3k
            mLastNGONFirstIndex = tri1->mIndices[0];
157
87.3k
        }
158
159
        /**
160
         * @brief Check whether this triangle would be considered part of the lastly emitted ngon or not.
161
         *
162
         * @param tri Current triangle.
163
         * @return true If used as is, this triangle will be part of last ngon.
164
         * @return false If used as is, this triangle is not considered part of the last ngon.
165
         */
166
113k
        bool isConsideredSameAsLastNgon(const aiFace * tri) const {
167
113k
            ai_assert(tri->mNumIndices == 3);
168
113k
            return tri->mIndices[0] == mLastNGONFirstIndex;
169
113k
        }
170
171
    private:
172
        unsigned int mLastNGONFirstIndex;
173
    };
174
175
}
176
177
// ------------------------------------------------------------------------------------------------
178
// Returns whether the processing step is present in the given flag field.
179
455
bool TriangulateProcess::IsActive( unsigned int pFlags) const {
180
455
    return (pFlags & aiProcess_Triangulate) != 0;
181
455
}
182
183
// ------------------------------------------------------------------------------------------------
184
// Executes the post processing step on the given imported data.
185
267
void TriangulateProcess::Execute( aiScene* pScene) {
186
267
    ASSIMP_LOG_DEBUG("TriangulateProcess begin");
187
188
267
    bool bHas = false;
189
7.65k
    for( unsigned int a = 0; a < pScene->mNumMeshes; a++)
190
7.38k
    {
191
7.38k
        if (pScene->mMeshes[ a ]) {
192
7.38k
            if ( TriangulateMesh( pScene->mMeshes[ a ] ) ) {
193
4.84k
                bHas = true;
194
4.84k
            }
195
7.38k
        }
196
7.38k
    }
197
267
    if ( bHas ) {
198
170
        ASSIMP_LOG_INFO( "TriangulateProcess finished. All polygons have been triangulated." );
199
170
    } else {
200
97
        ASSIMP_LOG_DEBUG( "TriangulateProcess finished. There was nothing to be done." );
201
97
    }
202
267
}
203
204
// ------------------------------------------------------------------------------------------------
205
// Triangulates the given mesh.
206
7.38k
bool TriangulateProcess::TriangulateMesh( aiMesh* pMesh) {
207
    // Now we have aiMesh::mPrimitiveTypes, so this is only here for test cases
208
7.38k
    if (!pMesh->mPrimitiveTypes)    {
209
0
        bool bNeed = false;
210
211
0
        for( unsigned int a = 0; a < pMesh->mNumFaces; a++) {
212
0
            const aiFace& face = pMesh->mFaces[a];
213
0
            if( face.mNumIndices != 3)  {
214
0
                bNeed = true;
215
0
            }
216
0
        }
217
0
        if (!bNeed) {
218
0
            return false;
219
0
        }
220
0
    }
221
7.38k
    else if (!(pMesh->mPrimitiveTypes & aiPrimitiveType_POLYGON)) {
222
2.53k
        return false;
223
2.53k
    }
224
225
    // Find out how many output faces we'll get
226
4.84k
    uint32_t numOut = 0, max_out = 0;
227
4.84k
    bool get_normals = true;
228
111k
    for( unsigned int a = 0; a < pMesh->mNumFaces; a++) {
229
106k
        aiFace& face = pMesh->mFaces[a];
230
106k
        if (face.mNumIndices <= 4) {
231
101k
            get_normals = false;
232
101k
        }
233
106k
        if( face.mNumIndices <= 3) {
234
13.8k
            ++numOut;
235
92.7k
        } else {
236
92.7k
            numOut += face.mNumIndices-2;
237
92.7k
            max_out = std::max(max_out,face.mNumIndices);
238
92.7k
        }
239
106k
    }
240
241
    // Just another check whether aiMesh::mPrimitiveTypes is correct
242
4.84k
    if (numOut == pMesh->mNumFaces) {
243
0
        ASSIMP_LOG_ERROR( "Invalidation detected in the number of indices: does not fit to the primitive type." );
244
0
        return false;
245
0
    }
246
247
4.84k
    aiVector3D *nor_out = nullptr;
248
249
    // if we don't have normals yet, but expect them to be a cheap side
250
    // product of triangulation anyway, allocate storage for them.
251
4.84k
    if (!pMesh->mNormals && get_normals) {
252
        // XXX need a mechanism to inform the GenVertexNormals process to treat these normals as preprocessed per-face normals
253
    //  nor_out = pMesh->mNormals = new aiVector3D[pMesh->mNumVertices];
254
314
    }
255
256
    // the output mesh will contain triangles, but no polys anymore
257
4.84k
    pMesh->mPrimitiveTypes |= aiPrimitiveType_TRIANGLE;
258
4.84k
    pMesh->mPrimitiveTypes &= ~aiPrimitiveType_POLYGON;
259
260
    // The mesh becomes NGON encoded now, during the triangulation process.
261
4.84k
    pMesh->mPrimitiveTypes |= aiPrimitiveType_NGONEncodingFlag;
262
263
4.84k
    aiFace* out = new aiFace[numOut](), *curOut = out;
264
4.84k
    std::vector<aiVector3D> temp_verts3d(max_out+2); /* temporary storage for vertices */
265
4.84k
    std::vector<std::vector<aiVector2D>> temp_poly(1); /* temporary storage for earcut.hpp */
266
4.84k
    std::vector<aiVector2D>& temp_verts = temp_poly[0];
267
4.84k
    temp_verts.reserve(max_out + 2);
268
269
4.84k
    NGONEncoder ngonEncoder;
270
271
    // Apply vertex colors to represent the face winding?
272
#ifdef AI_BUILD_TRIANGULATE_COLOR_FACE_WINDING
273
    if (!pMesh->mColors[0])
274
        pMesh->mColors[0] = new aiColor4D[pMesh->mNumVertices];
275
    else
276
        new(pMesh->mColors[0]) aiColor4D[pMesh->mNumVertices];
277
278
    aiColor4D* clr = pMesh->mColors[0];
279
#endif
280
281
#ifdef AI_BUILD_TRIANGULATE_DEBUG_POLYS
282
    FILE* fout = fopen(POLY_OUTPUT_FILE,"a");
283
#endif
284
285
4.84k
    const aiVector3D* verts = pMesh->mVertices;
286
287
111k
    for( unsigned int a = 0; a < pMesh->mNumFaces; a++) {
288
106k
        aiFace& face = pMesh->mFaces[a];
289
290
106k
        unsigned int* idx = face.mIndices;
291
106k
        unsigned int num = face.mNumIndices;
292
293
        // Apply vertex colors to represent the face winding?
294
#ifdef AI_BUILD_TRIANGULATE_COLOR_FACE_WINDING
295
        for (unsigned int i = 0; i < face.mNumIndices; ++i) {
296
            aiColor4D& c = clr[idx[i]];
297
            c.r = (i+1) / (float)max;
298
            c.b = 1.f - c.r;
299
        }
300
#endif
301
302
106k
        aiFace* const last_face = curOut;
303
304
        // if it's a simple point,line or triangle: just copy it
305
106k
        if( face.mNumIndices <= 3)
306
13.8k
        {
307
13.8k
            aiFace& nface = *curOut++;
308
13.8k
            nface.mNumIndices = face.mNumIndices;
309
13.8k
            nface.mIndices    = face.mIndices;
310
13.8k
            face.mIndices = nullptr;
311
312
            // points and lines don't require ngon encoding (and are not supported either!)
313
13.8k
            if (nface.mNumIndices == 3) ngonEncoder.ngonEncodeTriangle(&nface);
314
315
13.8k
            continue;
316
13.8k
        }
317
        // optimized code for quadrilaterals
318
92.7k
        else if ( face.mNumIndices == 4) {
319
320
            // quads can have at maximum one concave vertex. Determine
321
            // this vertex (if it exists) and start tri-fanning from
322
            // it.
323
87.3k
            unsigned int start_vertex = 0;
324
380k
            for (unsigned int i = 0; i < 4; ++i) {
325
313k
                const aiVector3D& v0 = verts[face.mIndices[(i+3) % 4]];
326
313k
                const aiVector3D& v1 = verts[face.mIndices[(i+2) % 4]];
327
313k
                const aiVector3D& v2 = verts[face.mIndices[(i+1) % 4]];
328
329
313k
                const aiVector3D& v = verts[face.mIndices[i]];
330
331
313k
                aiVector3D left = (v0-v);
332
313k
                aiVector3D diag = (v1-v);
333
313k
                aiVector3D right = (v2-v);
334
335
313k
                left.Normalize();
336
313k
                diag.Normalize();
337
313k
                right.Normalize();
338
339
313k
                const float angle = std::acos(left*diag) + std::acos(right*diag);
340
313k
                if (angle > AI_MATH_PI_F) {
341
                    // this is the concave point
342
20.5k
                    start_vertex = i;
343
20.5k
                    break;
344
20.5k
                }
345
313k
            }
346
347
87.3k
            const unsigned int temp[] = {face.mIndices[0], face.mIndices[1], face.mIndices[2], face.mIndices[3]};
348
349
87.3k
            aiFace& nface = *curOut++;
350
87.3k
            nface.mNumIndices = 3;
351
87.3k
            nface.mIndices = face.mIndices;
352
353
87.3k
            nface.mIndices[0] = temp[start_vertex];
354
87.3k
            nface.mIndices[1] = temp[(start_vertex + 1) % 4];
355
87.3k
            nface.mIndices[2] = temp[(start_vertex + 2) % 4];
356
357
87.3k
            aiFace& sface = *curOut++;
358
87.3k
            sface.mNumIndices = 3;
359
87.3k
            sface.mIndices = new unsigned int[3];
360
361
87.3k
            sface.mIndices[0] = temp[start_vertex];
362
87.3k
            sface.mIndices[1] = temp[(start_vertex + 2) % 4];
363
87.3k
            sface.mIndices[2] = temp[(start_vertex + 3) % 4];
364
365
            // prevent double deletion of the indices field
366
87.3k
            face.mIndices = nullptr;
367
368
87.3k
            ngonEncoder.ngonEncodeQuad(&nface, &sface);
369
370
87.3k
            continue;
371
87.3k
        }
372
5.36k
        else
373
5.36k
        {
374
            // A polygon with more than 3 vertices can be either concave or convex.
375
            // Usually everything we're getting is convex and we could easily
376
            // triangulate by tri-fanning. However, LightWave is probably the only
377
            // modeling suite to make extensive use of highly concave, monster polygons ...
378
            // so we need to apply the full 'ear cutting' algorithm to get it right.
379
380
            // REQUIREMENT: polygon is expected to be simple and *nearly* planar.
381
            // We project it onto a plane to get a 2d triangle.
382
383
            // Collect all vertices of of the polygon.
384
160k
            for (unsigned int tmp = 0; tmp < num; ++tmp) {
385
154k
                temp_verts3d[tmp] = verts[idx[tmp]];
386
154k
            }
387
388
            // Get newell normal of the polygon. Store it for future use if it's a polygon-only mesh
389
5.36k
            aiVector3D n;
390
5.36k
            NewellNormal<3, 3, 3>(n, num, &temp_verts3d.front().x, &temp_verts3d.front().y, &temp_verts3d.front().z);
391
5.36k
            if (nor_out) {
392
0
                for (unsigned int tmp = 0; tmp < num; ++tmp)
393
0
                    nor_out[idx[tmp]] = n;
394
0
            }
395
396
            // Select largest normal coordinate to ignore for projection
397
5.36k
            const float ax = (n.x>0 ? n.x : -n.x);
398
5.36k
            const float ay = (n.y>0 ? n.y : -n.y);
399
5.36k
            const float az = (n.z>0 ? n.z : -n.z);
400
401
5.36k
            unsigned int ac = 0, bc = 1; /* no z coord. projection to xy */
402
5.36k
            float inv = n.z;
403
5.36k
            if (ax > ay) {
404
573
                if (ax > az) { /* no x coord. projection to yz */
405
7
                    ac = 1; bc = 2;
406
7
                    inv = n.x;
407
7
                }
408
573
            }
409
4.78k
            else if (ay > az) { /* no y coord. projection to zy */
410
2.39k
                ac = 2; bc = 0;
411
2.39k
                inv = n.y;
412
2.39k
            }
413
414
            // Swap projection axes to take the negated projection vector into account
415
5.36k
            if (inv < 0.f) {
416
1.89k
                std::swap(ac,bc);
417
1.89k
            }
418
419
5.36k
            temp_verts.resize(num);
420
160k
            for (unsigned int tmp = 0; tmp < num; ++tmp) {
421
154k
                temp_verts[tmp].x = verts[idx[tmp]][ac];
422
154k
                temp_verts[tmp].y = verts[idx[tmp]][bc];
423
154k
            }
424
425
5.36k
            auto indices = mapbox::earcut(temp_poly);
426
17.4k
            for (size_t i = 0; i < indices.size(); i += 3) {
427
12.0k
                aiFace& nface = *curOut++;
428
12.0k
                nface.mIndices = new unsigned int[3];
429
12.0k
                nface.mNumIndices = 3;
430
12.0k
                nface.mIndices[0] = indices[i];
431
12.0k
                nface.mIndices[1] = indices[i + 1];
432
12.0k
                nface.mIndices[2] = indices[i + 2];
433
12.0k
            }
434
435
#ifdef AI_BUILD_TRIANGULATE_DEBUG_POLYS
436
            // plot the plane onto which we mapped the polygon to a 2D ASCII pic
437
            aiVector2D bmin,bmax;
438
            ArrayBounds(&temp_verts[0],max,bmin,bmax);
439
440
            char grid[POLY_GRID_Y][POLY_GRID_X+POLY_GRID_XPAD];
441
            std::fill_n((char*)grid,POLY_GRID_Y*(POLY_GRID_X+POLY_GRID_XPAD),' ');
442
443
            for (int i =0; i < max; ++i) {
444
                const aiVector2D& v = (temp_verts[i] - bmin) / (bmax-bmin);
445
                const size_t x = static_cast<size_t>(v.x*(POLY_GRID_X-1)), y = static_cast<size_t>(v.y*(POLY_GRID_Y-1));
446
                char* loc = grid[y]+x;
447
                if (grid[y][x] != ' ') {
448
                    for(;*loc != ' '; ++loc);
449
                    *loc++ = '_';
450
                }
451
                *(loc+::ai_snprintf(loc, POLY_GRID_XPAD,"%i",i)) = ' ';
452
            }
453
454
455
            for(size_t y = 0; y < POLY_GRID_Y; ++y) {
456
                grid[y][POLY_GRID_X+POLY_GRID_XPAD-1] = '\0';
457
                fprintf(fout,"%s\n",grid[y]);
458
            }
459
460
            fprintf(fout,"\ntriangulation sequence: ");
461
#endif
462
5.36k
        }
463
464
#ifdef AI_BUILD_TRIANGULATE_DEBUG_POLYS
465
466
        for(aiFace* f = last_face; f != curOut; ++f) {
467
            unsigned int* i = f->mIndices;
468
            fprintf(fout," (%i %i %i)",i[0],i[1],i[2]);
469
        }
470
471
        fprintf(fout,"\n*********************************************************************\n");
472
        fflush(fout);
473
474
#endif
475
476
17.4k
        for(aiFace* f = last_face; f != curOut; ) {
477
12.0k
            unsigned int* i = f->mIndices;
478
479
12.0k
            i[0] = idx[i[0]];
480
12.0k
            i[1] = idx[i[1]];
481
12.0k
            i[2] = idx[i[2]];
482
483
            // IMPROVEMENT: Polygons are not supported yet by this ngon encoding + triangulation step.
484
            //              So we encode polygons as regular triangles. No way to reconstruct the original
485
            //              polygon in this case.
486
12.0k
            ngonEncoder.ngonEncodeTriangle(f);
487
12.0k
            ++f;
488
12.0k
        }
489
490
5.36k
        delete[] face.mIndices;
491
5.36k
        face.mIndices = nullptr;
492
5.36k
    }
493
494
#ifdef AI_BUILD_TRIANGULATE_DEBUG_POLYS
495
    fclose(fout);
496
#endif
497
498
    // kill the old faces
499
4.84k
    delete [] pMesh->mFaces;
500
501
    // ... and store the new ones
502
4.84k
    pMesh->mFaces    = out;
503
4.84k
    pMesh->mNumFaces = (unsigned int)(curOut-out); /* not necessarily equal to numOut */
504
4.84k
    return true;
505
4.84k
}
506
507
#endif // !! ASSIMP_BUILD_NO_TRIANGULATE_PROCESS