Coverage Report

Created: 2026-01-25 07:15

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/assimp/code/PostProcessing/ComputeUVMappingProcess.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 GenUVCoords step */
43
44
#include "ComputeUVMappingProcess.h"
45
#include "Geometry/GeometryUtils.h"
46
#include "ProcessHelper.h"
47
#include <assimp/Exceptional.h>
48
49
using namespace Assimp;
50
51
namespace {
52
53
const static aiVector3D base_axis_y(0.0, 1.0, 0.0);
54
const static aiVector3D base_axis_x(1.0, 0.0, 0.0);
55
const static aiVector3D base_axis_z(0.0, 0.0, 1.0);
56
const static ai_real angle_epsilon = ai_real(0.95);
57
} // namespace
58
59
// ------------------------------------------------------------------------------------------------
60
// Returns whether the processing step is present in the given flag field.
61
8.63k
bool ComputeUVMappingProcess::IsActive(unsigned int pFlags) const {
62
8.63k
    return (pFlags & aiProcess_GenUVCoords) != 0;
63
8.63k
}
64
65
// ------------------------------------------------------------------------------------------------
66
// Find the first empty UV channel in a mesh
67
0
inline unsigned int FindEmptyUVChannel(aiMesh *mesh) {
68
0
    for (unsigned int m = 0; m < AI_MAX_NUMBER_OF_TEXTURECOORDS; ++m)
69
0
        if (!mesh->mTextureCoords[m]) {
70
0
            return m;
71
0
        }
72
73
0
    ASSIMP_LOG_ERROR("Unable to compute UV coordinates, no free UV slot found");
74
0
    return UINT_MAX;
75
0
}
76
77
// ------------------------------------------------------------------------------------------------
78
// Try to remove UV seams
79
0
void RemoveUVSeams(aiMesh *mesh, aiVector3D *out) {
80
    // TODO: just a very rough algorithm. I think it could be done
81
    // much easier, but I don't know how and am currently too tired to
82
    // to think about a better solution.
83
84
0
    const static ai_real LOWER_LIMIT = ai_real(0.1);
85
0
    const static ai_real UPPER_LIMIT = ai_real(0.9);
86
87
0
    const static ai_real LOWER_EPSILON = ai_real(10e-3);
88
0
    const static ai_real UPPER_EPSILON = ai_real(1.0 - 10e-3);
89
90
0
    for (unsigned int fidx = 0; fidx < mesh->mNumFaces; ++fidx) {
91
0
        const aiFace &face = mesh->mFaces[fidx];
92
0
        if (face.mNumIndices < 3) {
93
0
            continue; // triangles and polygons only, please
94
0
        }
95
96
0
        unsigned int smallV = face.mNumIndices, large = smallV;
97
0
        bool zero = false, one = false, round_to_zero = false;
98
99
        // Check whether this face lies on a UV seam. We can just guess,
100
        // but the assumption that a face with at least one very small
101
        // on the one side and one very large U coord on the other side
102
        // lies on a UV seam should work for most cases.
103
0
        for (unsigned int n = 0; n < face.mNumIndices; ++n) {
104
0
            if (out[face.mIndices[n]].x < LOWER_LIMIT) {
105
0
                smallV = n;
106
107
                // If we have a U value very close to 0 we can't
108
                // round the others to 0, too.
109
0
                if (out[face.mIndices[n]].x <= LOWER_EPSILON)
110
0
                    zero = true;
111
0
                else
112
0
                    round_to_zero = true;
113
0
            }
114
0
            if (out[face.mIndices[n]].x > UPPER_LIMIT) {
115
0
                large = n;
116
117
                // If we have a U value very close to 1 we can't
118
                // round the others to 1, too.
119
0
                if (out[face.mIndices[n]].x >= UPPER_EPSILON)
120
0
                    one = true;
121
0
            }
122
0
        }
123
0
        if (smallV != face.mNumIndices && large != face.mNumIndices) {
124
0
            for (unsigned int n = 0; n < face.mNumIndices; ++n) {
125
                // If the u value is over the upper limit and no other u
126
                // value of that face is 0, round it to 0
127
0
                if (out[face.mIndices[n]].x > UPPER_LIMIT && !zero)
128
0
                    out[face.mIndices[n]].x = 0.0;
129
130
                // If the u value is below the lower limit and no other u
131
                // value of that face is 1, round it to 1
132
0
                else if (out[face.mIndices[n]].x < LOWER_LIMIT && !one)
133
0
                    out[face.mIndices[n]].x = 1.0;
134
135
                // The face contains both 0 and 1 as UV coords. This can occur
136
                // for faces which have an edge that lies directly on the seam.
137
                // Due to numerical inaccuracies one U coord becomes 0, the
138
                // other 1. But we do still have a third UV coord to determine
139
                // to which side we must round to.
140
0
                else if (one && zero) {
141
0
                    if (round_to_zero && out[face.mIndices[n]].x >= UPPER_EPSILON)
142
0
                        out[face.mIndices[n]].x = 0.0;
143
0
                    else if (!round_to_zero && out[face.mIndices[n]].x <= LOWER_EPSILON)
144
0
                        out[face.mIndices[n]].x = 1.0;
145
0
                }
146
0
            }
147
0
        }
148
0
    }
149
0
}
150
151
// ------------------------------------------------------------------------------------------------
152
0
void ComputeUVMappingProcess::ComputeSphereMapping(aiMesh *mesh, const aiVector3D &axis, aiVector3D *out) {
153
0
    aiVector3D center, min, max;
154
0
    FindMeshCenter(mesh, center, min, max);
155
156
    // If the axis is one of x,y,z run a faster code path. It's worth the extra effort ...
157
    // currently the mapping axis will always be one of x,y,z, except if the
158
    // PretransformVertices step is used (it transforms the meshes into worldspace,
159
    // thus changing the mapping axis)
160
0
    if (axis * base_axis_x >= angle_epsilon) {
161
162
        // For each point get a normalized projection vector in the sphere,
163
        // get its longitude and latitude and map them to their respective
164
        // UV axes. Problems occur around the poles ... unsolvable.
165
        //
166
        // The spherical coordinate system looks like this:
167
        // x = cos(lon)*cos(lat)
168
        // y = sin(lon)*cos(lat)
169
        // z = sin(lat)
170
        //
171
        // Thus we can derive:
172
        // lat  = arcsin (z)
173
        // lon  = arctan (y/x)
174
0
        for (unsigned int pnt = 0; pnt < mesh->mNumVertices; ++pnt) {
175
0
            const aiVector3D diff = (mesh->mVertices[pnt] - center).Normalize();
176
0
            out[pnt] = aiVector3D((std::atan2(diff.z, diff.y) + AI_MATH_PI_F) / AI_MATH_TWO_PI_F,
177
0
                    (std::asin(diff.x) + AI_MATH_HALF_PI_F) / AI_MATH_PI_F, 0.0);
178
0
        }
179
0
    } else if (axis * base_axis_y >= angle_epsilon) {
180
        // ... just the same again
181
0
        for (unsigned int pnt = 0; pnt < mesh->mNumVertices; ++pnt) {
182
0
            const aiVector3D diff = (mesh->mVertices[pnt] - center).Normalize();
183
0
            out[pnt] = aiVector3D((std::atan2(diff.x, diff.z) + AI_MATH_PI_F) / AI_MATH_TWO_PI_F,
184
0
                    (std::asin(diff.y) + AI_MATH_HALF_PI_F) / AI_MATH_PI_F, 0.0);
185
0
        }
186
0
    } else if (axis * base_axis_z >= angle_epsilon) {
187
        // ... just the same again
188
0
        for (unsigned int pnt = 0; pnt < mesh->mNumVertices; ++pnt) {
189
0
            const aiVector3D diff = (mesh->mVertices[pnt] - center).Normalize();
190
0
            out[pnt] = aiVector3D((std::atan2(diff.y, diff.x) + AI_MATH_PI_F) / AI_MATH_TWO_PI_F,
191
0
                    (std::asin(diff.z) + AI_MATH_HALF_PI_F) / AI_MATH_PI_F, 0.0);
192
0
        }
193
0
    }
194
    // slower code path in case the mapping axis is not one of the coordinate system axes
195
0
    else {
196
0
        aiMatrix4x4 mTrafo;
197
0
        aiMatrix4x4::FromToMatrix(axis, base_axis_y, mTrafo);
198
199
        // again the same, except we're applying a transformation now
200
0
        for (unsigned int pnt = 0; pnt < mesh->mNumVertices; ++pnt) {
201
0
            const aiVector3D diff = ((mTrafo * mesh->mVertices[pnt]) - center).Normalize();
202
0
            out[pnt] = aiVector3D((std::atan2(diff.y, diff.x) + AI_MATH_PI_F) / AI_MATH_TWO_PI_F,
203
0
                    (std::asin(diff.z) + AI_MATH_HALF_PI_F) / AI_MATH_PI_F, 0.0);
204
0
        }
205
0
    }
206
207
    // Now find and remove UV seams. A seam occurs if a face has a tcoord
208
    // close to zero on the one side, and a tcoord close to one on the
209
    // other side.
210
0
    RemoveUVSeams(mesh, out);
211
0
}
212
213
// ------------------------------------------------------------------------------------------------
214
0
void ComputeUVMappingProcess::ComputeCylinderMapping(aiMesh *mesh, const aiVector3D &axis, aiVector3D *out) {
215
0
    aiVector3D center, min, max;
216
217
    // If the axis is one of x,y,z run a faster code path. It's worth the extra effort ...
218
    // currently the mapping axis will always be one of x,y,z, except if the
219
    // PretransformVertices step is used (it transforms the meshes into worldspace,
220
    // thus changing the mapping axis)
221
0
    if (axis * base_axis_x >= angle_epsilon) {
222
0
        FindMeshCenter(mesh, center, min, max);
223
0
        const ai_real diff = max.x - min.x;
224
225
        // If the main axis is 'z', the z coordinate of a point 'p' is mapped
226
        // directly to the texture V axis. The other axis is derived from
227
        // the angle between ( p.x - c.x, p.y - c.y ) and (1,0), where
228
        // 'c' is the center point of the mesh.
229
0
        for (unsigned int pnt = 0; pnt < mesh->mNumVertices; ++pnt) {
230
0
            const aiVector3D &pos = mesh->mVertices[pnt];
231
0
            aiVector3D &uv = out[pnt];
232
233
0
            uv.y = (pos.x - min.x) / diff;
234
0
            uv.x = (std::atan2(pos.z - center.z, pos.y - center.y) + (ai_real)AI_MATH_PI) / (ai_real)AI_MATH_TWO_PI;
235
0
        }
236
0
    } else if (axis * base_axis_y >= angle_epsilon) {
237
0
        FindMeshCenter(mesh, center, min, max);
238
0
        const ai_real diff = max.y - min.y;
239
240
        // just the same ...
241
0
        for (unsigned int pnt = 0; pnt < mesh->mNumVertices; ++pnt) {
242
0
            const aiVector3D &pos = mesh->mVertices[pnt];
243
0
            aiVector3D &uv = out[pnt];
244
245
0
            uv.y = (pos.y - min.y) / diff;
246
0
            uv.x = (std::atan2(pos.x - center.x, pos.z - center.z) + (ai_real)AI_MATH_PI) / (ai_real)AI_MATH_TWO_PI;
247
0
        }
248
0
    } else if (axis * base_axis_z >= angle_epsilon) {
249
0
        FindMeshCenter(mesh, center, min, max);
250
0
        const ai_real diff = max.z - min.z;
251
252
        // just the same ...
253
0
        for (unsigned int pnt = 0; pnt < mesh->mNumVertices; ++pnt) {
254
0
            const aiVector3D &pos = mesh->mVertices[pnt];
255
0
            aiVector3D &uv = out[pnt];
256
257
0
            uv.y = (pos.z - min.z) / diff;
258
0
            uv.x = (std::atan2(pos.y - center.y, pos.x - center.x) + (ai_real)AI_MATH_PI) / (ai_real)AI_MATH_TWO_PI;
259
0
        }
260
0
    }
261
    // slower code path in case the mapping axis is not one of the coordinate system axes
262
0
    else {
263
0
        aiMatrix4x4 mTrafo;
264
0
        aiMatrix4x4::FromToMatrix(axis, base_axis_y, mTrafo);
265
0
        FindMeshCenterTransformed(mesh, center, min, max, mTrafo);
266
0
        const ai_real diff = max.y - min.y;
267
268
        // again the same, except we're applying a transformation now
269
0
        for (unsigned int pnt = 0; pnt < mesh->mNumVertices; ++pnt) {
270
0
            const aiVector3D pos = mTrafo * mesh->mVertices[pnt];
271
0
            aiVector3D &uv = out[pnt];
272
273
0
            uv.y = (pos.y - min.y) / diff;
274
0
            uv.x = (std::atan2(pos.x - center.x, pos.z - center.z) + (ai_real)AI_MATH_PI) / (ai_real)AI_MATH_TWO_PI;
275
0
        }
276
0
    }
277
278
    // Now find and remove UV seams. A seam occurs if a face has a tcoord
279
    // close to zero on the one side, and a tcoord close to one on the
280
    // other side.
281
0
    RemoveUVSeams(mesh, out);
282
0
}
283
284
// ------------------------------------------------------------------------------------------------
285
0
void ComputeUVMappingProcess::ComputePlaneMapping(aiMesh *mesh, const aiVector3D &axis, aiVector3D *out) {
286
0
    ai_real diffu, diffv;
287
0
    aiVector3D center, min, max;
288
289
    // If the axis is one of x,y,z run a faster code path. It's worth the extra effort ...
290
    // currently the mapping axis will always be one of x,y,z, except if the
291
    // PretransformVertices step is used (it transforms the meshes into worldspace,
292
    // thus changing the mapping axis)
293
0
    if (axis * base_axis_x >= angle_epsilon) {
294
0
        FindMeshCenter(mesh, center, min, max);
295
0
        diffu = max.z - min.z;
296
0
        diffv = max.y - min.y;
297
298
0
        for (unsigned int pnt = 0; pnt < mesh->mNumVertices; ++pnt) {
299
0
            const aiVector3D &pos = mesh->mVertices[pnt];
300
0
            out[pnt].Set((pos.z - min.z) / diffu, (pos.y - min.y) / diffv, 0.0);
301
0
        }
302
0
    } else if (axis * base_axis_y >= angle_epsilon) {
303
0
        FindMeshCenter(mesh, center, min, max);
304
0
        diffu = max.x - min.x;
305
0
        diffv = max.z - min.z;
306
307
0
        for (unsigned int pnt = 0; pnt < mesh->mNumVertices; ++pnt) {
308
0
            const aiVector3D &pos = mesh->mVertices[pnt];
309
0
            out[pnt].Set((pos.x - min.x) / diffu, (pos.z - min.z) / diffv, 0.0);
310
0
        }
311
0
    } else if (axis * base_axis_z >= angle_epsilon) {
312
0
        FindMeshCenter(mesh, center, min, max);
313
0
        diffu = max.x - min.x;
314
0
        diffv = max.y - min.y;
315
316
0
        for (unsigned int pnt = 0; pnt < mesh->mNumVertices; ++pnt) {
317
0
            const aiVector3D &pos = mesh->mVertices[pnt];
318
0
            out[pnt].Set((pos.x - min.x) / diffu, (pos.y - min.y) / diffv, 0.0);
319
0
        }
320
0
    }
321
    // slower code path in case the mapping axis is not one of the coordinate system axes
322
0
    else {
323
0
        aiMatrix4x4 mTrafo;
324
0
        aiMatrix4x4::FromToMatrix(axis, base_axis_y, mTrafo);
325
0
        FindMeshCenterTransformed(mesh, center, min, max, mTrafo);
326
0
        diffu = max.x - min.x;
327
0
        diffv = max.z - min.z;
328
329
        // again the same, except we're applying a transformation now
330
0
        for (unsigned int pnt = 0; pnt < mesh->mNumVertices; ++pnt) {
331
0
            const aiVector3D pos = mTrafo * mesh->mVertices[pnt];
332
0
            out[pnt].Set((pos.x - min.x) / diffu, (pos.z - min.z) / diffv, 0.0);
333
0
        }
334
0
    }
335
336
    // shouldn't be necessary to remove UV seams ...
337
0
}
338
339
// ------------------------------------------------------------------------------------------------
340
0
void ComputeUVMappingProcess::ComputeBoxMapping(aiMesh *, aiVector3D *) {
341
0
    ASSIMP_LOG_ERROR("Mapping type currently not implemented");
342
0
}
343
344
// ------------------------------------------------------------------------------------------------
345
8.52k
void ComputeUVMappingProcess::Execute(aiScene *pScene) {
346
8.52k
    ASSIMP_LOG_DEBUG("GenUVCoordsProcess begin");
347
8.52k
    char buffer[1024];
348
349
8.52k
    if (pScene->mFlags & AI_SCENE_FLAGS_NON_VERBOSE_FORMAT) {
350
0
        throw DeadlyImportError("Post-processing order mismatch: expecting pseudo-indexed (\"verbose\") vertices here");
351
0
    }
352
353
8.52k
    std::list<MappingInfo> mappingStack;
354
355
    // Iterate through all materials and search for non-UV mapped textures
356
17.3k
    for (unsigned int i = 0; i < pScene->mNumMaterials; ++i) {
357
8.81k
        mappingStack.clear();
358
8.81k
        aiMaterial *mat = pScene->mMaterials[i];
359
8.81k
        if (mat == nullptr) {
360
0
            ASSIMP_LOG_INFO("Material pointer in nullptr, skipping.");
361
0
            continue;
362
0
        }
363
129k
        for (unsigned int a = 0; a < mat->mNumProperties; ++a) {
364
120k
            aiMaterialProperty *prop = mat->mProperties[a];
365
120k
            if (!::strcmp(prop->mKey.data, "$tex.mapping")) {
366
0
                aiTextureMapping &mapping = *((aiTextureMapping *)prop->mData);
367
0
                if (aiTextureMapping_UV != mapping) {
368
0
                    if (!DefaultLogger::isNullLogger()) {
369
0
                        ai_snprintf(buffer, 1024, "Found non-UV mapped texture (%s,%u). Mapping type: %s",
370
0
                                aiTextureTypeToString((aiTextureType)prop->mSemantic), prop->mIndex,
371
0
                                MappingTypeToString(mapping));
372
373
0
                        ASSIMP_LOG_INFO(buffer);
374
0
                    }
375
376
0
                    if (aiTextureMapping_OTHER == mapping)
377
0
                        continue;
378
379
0
                    MappingInfo info(mapping);
380
381
                    // Get further properties - currently only the major axis
382
0
                    for (unsigned int a2 = 0; a2 < mat->mNumProperties; ++a2) {
383
0
                        aiMaterialProperty *prop2 = mat->mProperties[a2];
384
0
                        if (prop2->mSemantic != prop->mSemantic || prop2->mIndex != prop->mIndex)
385
0
                            continue;
386
387
0
                        if (!::strcmp(prop2->mKey.data, "$tex.mapaxis")) {
388
0
                            info.axis = *((aiVector3D *)prop2->mData);
389
0
                            break;
390
0
                        }
391
0
                    }
392
393
0
                    unsigned int idx(99999999);
394
395
                    // Check whether we have this mapping mode already
396
0
                    std::list<MappingInfo>::iterator it = std::find(mappingStack.begin(), mappingStack.end(), info);
397
0
                    if (mappingStack.end() != it) {
398
0
                        idx = (*it).uv;
399
0
                    } else {
400
                        /*  We have found a non-UV mapped texture. Now
401
                         *   we need to find all meshes using this material
402
                         *   that we can compute UV channels for them.
403
                         */
404
0
                        for (unsigned int m = 0; m < pScene->mNumMeshes; ++m) {
405
0
                            aiMesh *mesh = pScene->mMeshes[m];
406
0
                            unsigned int outIdx = 0;
407
0
                            if (mesh->mMaterialIndex != i || (outIdx = FindEmptyUVChannel(mesh)) == UINT_MAX ||
408
0
                                    !mesh->mNumVertices) {
409
0
                                continue;
410
0
                            }
411
412
                            // Allocate output storage
413
0
                            aiVector3D *p = mesh->mTextureCoords[outIdx] = new aiVector3D[mesh->mNumVertices];
414
415
0
                            switch (mapping) {
416
0
                            case aiTextureMapping_SPHERE:
417
0
                                ComputeSphereMapping(mesh, info.axis, p);
418
0
                                break;
419
0
                            case aiTextureMapping_CYLINDER:
420
0
                                ComputeCylinderMapping(mesh, info.axis, p);
421
0
                                break;
422
0
                            case aiTextureMapping_PLANE:
423
0
                                ComputePlaneMapping(mesh, info.axis, p);
424
0
                                break;
425
0
                            case aiTextureMapping_BOX:
426
0
                                ComputeBoxMapping(mesh, p);
427
0
                                break;
428
0
                            default:
429
0
                                ai_assert(false);
430
0
                            }
431
0
                            if (m && idx != outIdx) {
432
0
                                ASSIMP_LOG_WARN("UV index mismatch. Not all meshes assigned to "
433
0
                                                "this material have equal numbers of UV channels. The UV index stored in  "
434
0
                                                "the material structure does therefore not apply for all meshes. ");
435
0
                            }
436
0
                            idx = outIdx;
437
0
                        }
438
0
                        info.uv = idx;
439
0
                        mappingStack.push_back(info);
440
0
                    }
441
442
                    // Update the material property list
443
0
                    mapping = aiTextureMapping_UV;
444
0
                    ((aiMaterial *)mat)->AddProperty(&idx, 1, AI_MATKEY_UVWSRC(prop->mSemantic, prop->mIndex));
445
0
                }
446
0
            }
447
120k
        }
448
8.81k
    }
449
8.52k
    ASSIMP_LOG_DEBUG("GenUVCoordsProcess finished");
450
8.52k
}