Coverage Report

Created: 2026-07-30 07:21

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-2026, 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
103M
    inline static auto get(const aiVector2D& t) {
83
103M
        return t.x;
84
103M
    }
85
};
86
template <>
87
struct nth<1, aiVector2D> {
88
103M
    inline static auto get(const aiVector2D& t) {
89
103M
        return t.y;
90
103M
    }
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
125k
        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
11.2M
        void ngonEncodeTriangle(aiFace * tri) {
115
11.2M
            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
11.2M
            if (isConsideredSameAsLastNgon(tri)) {
120
94.1k
                std::swap(tri->mIndices[0], tri->mIndices[2]);
121
94.1k
                std::swap(tri->mIndices[1], tri->mIndices[2]);
122
94.1k
            }
123
124
11.2M
            mLastNGONFirstIndex = tri->mIndices[0];
125
11.2M
        }
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
3.17M
        void ngonEncodeQuad(aiFace *tri1, aiFace *tri2) {
136
3.17M
            ai_assert(tri1->mNumIndices == 3);
137
3.17M
            ai_assert(tri2->mNumIndices == 3);
138
3.17M
            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
3.17M
            if (isConsideredSameAsLastNgon(tri1)) {
145
                // Right-rotate indices for tri1 (index 2 becomes the new fanning vertex)
146
7
                std::swap(tri1->mIndices[0], tri1->mIndices[2]);
147
7
                std::swap(tri1->mIndices[1], tri1->mIndices[2]);
148
149
                // Left-rotate indices for tri2 (index 2 becomes the new fanning vertex)
150
7
                std::swap(tri2->mIndices[1], tri2->mIndices[2]);
151
7
                std::swap(tri2->mIndices[0], tri2->mIndices[2]);
152
153
7
                ai_assert(tri1->mIndices[0] == tri2->mIndices[0]);
154
7
            }
155
156
3.17M
            mLastNGONFirstIndex = tri1->mIndices[0];
157
3.17M
        }
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
14.4M
        bool isConsideredSameAsLastNgon(const aiFace * tri) const {
167
14.4M
            ai_assert(tri->mNumIndices == 3);
168
14.4M
            return tri->mIndices[0] == mLastNGONFirstIndex;
169
14.4M
        }
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
69.5k
bool TriangulateProcess::IsActive( unsigned int pFlags) const {
180
69.5k
    return (pFlags & aiProcess_Triangulate) != 0;
181
69.5k
}
182
183
// ------------------------------------------------------------------------------------------------
184
// Executes the post processing step on the given imported data.
185
33.9k
void TriangulateProcess::Execute( aiScene* pScene) {
186
33.9k
    ASSIMP_LOG_DEBUG("TriangulateProcess begin");
187
188
33.9k
    bool bHas = false;
189
386k
    for( unsigned int a = 0; a < pScene->mNumMeshes; a++)
190
352k
    {
191
352k
        if (pScene->mMeshes[ a ]) {
192
352k
            if ( TriangulateMesh( pScene->mMeshes[ a ] ) ) {
193
125k
                bHas = true;
194
125k
            }
195
352k
        }
196
352k
    }
197
33.9k
    if ( bHas ) {
198
8.71k
        ASSIMP_LOG_INFO( "TriangulateProcess finished. All polygons have been triangulated." );
199
25.2k
    } else {
200
25.2k
        ASSIMP_LOG_DEBUG( "TriangulateProcess finished. There was nothing to be done." );
201
25.2k
    }
202
33.9k
}
203
204
// ------------------------------------------------------------------------------------------------
205
// Triangulates the given mesh.
206
352k
bool TriangulateProcess::TriangulateMesh( aiMesh* pMesh) {
207
    // Now we have aiMesh::mPrimitiveTypes, so this is only here for test cases
208
352k
    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
352k
    else if (!(pMesh->mPrimitiveTypes & aiPrimitiveType_POLYGON)) {
222
227k
        return false;
223
227k
    }
224
225
    // Find out how many output faces we'll get
226
125k
    uint32_t numOut = 0, max_out = 0;
227
125k
    bool get_normals = true;
228
4.30M
    for( unsigned int a = 0; a < pMesh->mNumFaces; a++) {
229
4.17M
        aiFace& face = pMesh->mFaces[a];
230
4.17M
        if (face.mNumIndices <= 4) {
231
4.04M
            get_normals = false;
232
4.04M
        }
233
4.17M
        if( face.mNumIndices <= 3) {
234
871k
            ++numOut;
235
3.30M
        } else {
236
3.30M
            numOut += face.mNumIndices-2;
237
3.30M
            max_out = std::max<uint32_t>(max_out,face.mNumIndices);
238
3.30M
        }
239
4.17M
    }
240
241
    // Just another check whether aiMesh::mPrimitiveTypes is correct
242
125k
    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
125k
    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
125k
    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
9.49k
    }
255
256
    // the output mesh will contain triangles, but no polys anymore
257
125k
    pMesh->mPrimitiveTypes |= aiPrimitiveType_TRIANGLE;
258
125k
    pMesh->mPrimitiveTypes &= ~aiPrimitiveType_POLYGON;
259
260
    // The mesh becomes NGON encoded now, during the triangulation process.
261
125k
    pMesh->mPrimitiveTypes |= aiPrimitiveType_NGONEncodingFlag;
262
263
125k
    aiFace* out = new aiFace[numOut](), *curOut = out;
264
125k
    std::vector<aiVector3D> temp_verts3d(max_out+2); /* temporary storage for vertices */
265
125k
    std::vector<std::vector<aiVector2D>> temp_poly(1); /* temporary storage for earcut.hpp */
266
125k
    std::vector<aiVector2D>& temp_verts = temp_poly[0];
267
125k
    temp_verts.reserve(max_out + 2);
268
269
125k
    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
125k
    const aiVector3D* verts = pMesh->mVertices;
286
287
4.30M
    for( unsigned int a = 0; a < pMesh->mNumFaces; a++) {
288
4.17M
        aiFace& face = pMesh->mFaces[a];
289
290
4.17M
        unsigned int* idx = face.mIndices;
291
4.17M
        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
4.17M
        aiFace* const last_face = curOut;
303
304
        // if it's a simple point,line or triangle: just copy it
305
4.17M
        if( face.mNumIndices <= 3)
306
871k
        {
307
871k
            aiFace& nface = *curOut++;
308
871k
            nface.mNumIndices = face.mNumIndices;
309
871k
            nface.mIndices    = face.mIndices;
310
871k
            face.mIndices = nullptr;
311
312
            // points and lines don't require ngon encoding (and are not supported either!)
313
871k
            if (nface.mNumIndices == 3) ngonEncoder.ngonEncodeTriangle(&nface);
314
315
871k
            continue;
316
871k
        }
317
        // optimized code for quadrilaterals
318
3.30M
        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
3.17M
            unsigned int start_vertex = 0;
324
14.7M
            for (unsigned int i = 0; i < 4; ++i) {
325
12.0M
                const aiVector3D& v0 = verts[face.mIndices[(i+3) % 4]];
326
12.0M
                const aiVector3D& v1 = verts[face.mIndices[(i+2) % 4]];
327
12.0M
                const aiVector3D& v2 = verts[face.mIndices[(i+1) % 4]];
328
329
12.0M
                const aiVector3D& v = verts[face.mIndices[i]];
330
331
12.0M
                aiVector3D left = (v0-v);
332
12.0M
                aiVector3D diag = (v1-v);
333
12.0M
                aiVector3D right = (v2-v);
334
335
12.0M
                left.Normalize();
336
12.0M
                diag.Normalize();
337
12.0M
                right.Normalize();
338
339
12.0M
                const float angle = std::acos(left*diag) + std::acos(right*diag);
340
12.0M
                if (angle > AI_MATH_PI_F) {
341
                    // this is the concave point
342
382k
                    start_vertex = i;
343
382k
                    break;
344
382k
                }
345
12.0M
            }
346
347
3.17M
            const unsigned int temp[] = {face.mIndices[0], face.mIndices[1], face.mIndices[2], face.mIndices[3]};
348
349
3.17M
            aiFace& nface = *curOut++;
350
3.17M
            nface.mNumIndices = 3;
351
3.17M
            nface.mIndices = face.mIndices;
352
353
3.17M
            nface.mIndices[0] = temp[start_vertex];
354
3.17M
            nface.mIndices[1] = temp[(start_vertex + 1) % 4];
355
3.17M
            nface.mIndices[2] = temp[(start_vertex + 2) % 4];
356
357
3.17M
            aiFace& sface = *curOut++;
358
3.17M
            sface.mNumIndices = 3;
359
3.17M
            sface.mIndices = new unsigned int[3];
360
361
3.17M
            sface.mIndices[0] = temp[start_vertex];
362
3.17M
            sface.mIndices[1] = temp[(start_vertex + 2) % 4];
363
3.17M
            sface.mIndices[2] = temp[(start_vertex + 3) % 4];
364
365
            // prevent double deletion of the indices field
366
3.17M
            face.mIndices = nullptr;
367
368
3.17M
            ngonEncoder.ngonEncodeQuad(&nface, &sface);
369
370
3.17M
            continue;
371
3.17M
        }
372
128k
        else
373
128k
        {
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
34.5M
            for (unsigned int tmp = 0; tmp < num; ++tmp) {
385
34.4M
                temp_verts3d[tmp] = verts[idx[tmp]];
386
34.4M
            }
387
388
            // Get newell normal of the polygon. Store it for future use if it's a polygon-only mesh
389
128k
            aiVector3D n;
390
128k
            NewellNormal<3, 3, 3>(n, num, &temp_verts3d.front().x, &temp_verts3d.front().y, &temp_verts3d.front().z);
391
128k
            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
128k
            const float ax = (n.x>0 ? n.x : -n.x);
398
128k
            const float ay = (n.y>0 ? n.y : -n.y);
399
128k
            const float az = (n.z>0 ? n.z : -n.z);
400
401
128k
            unsigned int ac = 0, bc = 1; /* no z coord. projection to xy */
402
128k
            float inv = n.z;
403
128k
            if (ax > ay) {
404
68.0k
                if (ax > az) { /* no x coord. projection to yz */
405
52.5k
                    ac = 1; bc = 2;
406
52.5k
                    inv = n.x;
407
52.5k
                }
408
68.0k
            }
409
60.9k
            else if (ay > az) { /* no y coord. projection to zy */
410
41.5k
                ac = 2; bc = 0;
411
41.5k
                inv = n.y;
412
41.5k
            }
413
414
            // Swap projection axes to take the negated projection vector into account
415
128k
            if (inv < 0.f) {
416
62.7k
                std::swap(ac,bc);
417
62.7k
            }
418
419
128k
            temp_verts.resize(num);
420
34.5M
            for (unsigned int tmp = 0; tmp < num; ++tmp) {
421
34.4M
                temp_verts[tmp].x = verts[idx[tmp]][ac];
422
34.4M
                temp_verts[tmp].y = verts[idx[tmp]][bc];
423
34.4M
            }
424
425
128k
            auto indices = mapbox::earcut(temp_poly);
426
11.2M
            for (size_t i = 0; i < indices.size(); i += 3) {
427
11.0M
                aiFace& nface = *curOut++;
428
11.0M
                nface.mIndices = new unsigned int[3];
429
11.0M
                nface.mNumIndices = 3;
430
11.0M
                nface.mIndices[0] = indices[i];
431
11.0M
                nface.mIndices[1] = indices[i + 1];
432
11.0M
                nface.mIndices[2] = indices[i + 2];
433
11.0M
            }
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
128k
        }
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
11.2M
        for(aiFace* f = last_face; f != curOut; ) {
477
11.0M
            unsigned int* i = f->mIndices;
478
479
11.0M
            i[0] = idx[i[0]];
480
11.0M
            i[1] = idx[i[1]];
481
11.0M
            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
11.0M
            ngonEncoder.ngonEncodeTriangle(f);
487
11.0M
            ++f;
488
11.0M
        }
489
490
128k
        delete[] face.mIndices;
491
128k
        face.mIndices = nullptr;
492
128k
    }
493
494
#ifdef AI_BUILD_TRIANGULATE_DEBUG_POLYS
495
    fclose(fout);
496
#endif
497
498
    // kill the old faces
499
125k
    delete [] pMesh->mFaces;
500
501
    // ... and store the new ones
502
125k
    pMesh->mFaces    = out;
503
125k
    pMesh->mNumFaces = (unsigned int)(curOut-out); /* not necessarily equal to numOut */
504
125k
    return true;
505
125k
}
506
507
#endif // !! ASSIMP_BUILD_NO_TRIANGULATE_PROCESS