Coverage Report

Created: 2025-08-28 06:38

/src/assimp/code/PostProcessing/TextureTransform.cpp
Line
Count
Source (jump to first uncovered line)
1
/*
2
Open Asset Import Library (assimp)
3
----------------------------------------------------------------------
4
Copyright (c) 2006-2025, assimp team
5
6
All rights reserved.
7
8
Redistribution and use of this software in source and binary forms,
9
with or without modification, are permitted provided that the
10
following conditions are met:
11
12
* Redistributions of source code must retain the above
13
  copyright notice, this list of conditions and the
14
  following disclaimer.
15
16
* Redistributions in binary form must reproduce the above
17
  copyright notice, this list of conditions and the
18
  following disclaimer in the documentation and/or other
19
  materials provided with the distribution.
20
21
* Neither the name of the assimp team, nor the names of its
22
  contributors may be used to endorse or promote products
23
  derived from this software without specific prior
24
  written permission of the assimp team.
25
26
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
27
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
28
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
29
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
30
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
31
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
32
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
33
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
34
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
35
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
36
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
37
----------------------------------------------------------------------
38
*/
39
40
/** @file A helper class that processes texture transformations */
41
42
#include "TextureTransform.h"
43
44
#include <assimp/Importer.hpp>
45
#include <assimp/postprocess.h>
46
#include <assimp/DefaultLogger.hpp>
47
#include <assimp/scene.h>
48
#include <assimp/StringUtils.h>
49
50
namespace Assimp {
51
52
// ------------------------------------------------------------------------------------------------
53
// Returns whether the processing step is present in the given flag field.
54
165
bool TextureTransformStep::IsActive( unsigned int pFlags) const {
55
165
    return  (pFlags & aiProcess_TransformUVCoords) != 0;
56
165
}
57
58
// ------------------------------------------------------------------------------------------------
59
// Setup properties
60
0
void TextureTransformStep::SetupProperties(const Importer* pImp) {
61
0
    configFlags = pImp->GetPropertyInteger(AI_CONFIG_PP_TUV_EVALUATE,AI_UVTRAFO_ALL);
62
0
}
63
64
// ------------------------------------------------------------------------------------------------
65
0
void TextureTransformStep::PreProcessUVTransform(STransformVecInfo& info) {
66
    /*  This function tries to simplify the input UV transformation.
67
     *  That's very important as it allows us to reduce the number
68
     *  of output UV channels. The order in which the transformations
69
     *  are applied is - as always - scaling, rotation, translation.
70
     */
71
72
0
  int rounded;
73
0
  char szTemp[512] = {};
74
75
    /* Optimize the rotation angle. That's slightly difficult as
76
     * we have an inprecise floating-point number (when comparing
77
     * UV transformations we'll take that into account by using
78
     * an epsilon of 5 degrees). If there is a rotation value, we can't
79
     * perform any further optimizations.
80
     */
81
0
    if (info.mRotation) {
82
0
        float out = info.mRotation;
83
0
        rounded = static_cast<int>((info.mRotation / static_cast<float>(AI_MATH_TWO_PI)));
84
0
        if (rounded) {
85
0
            out -= rounded * static_cast<float>(AI_MATH_PI);
86
0
            ASSIMP_LOG_INFO("Texture coordinate rotation ", info.mRotation, " can be simplified to ", out);
87
0
        }
88
89
        // Next step - convert negative rotation angles to positives
90
0
        if (out < 0.f)
91
0
            out = (float)AI_MATH_TWO_PI * 2 + out;
92
93
0
        info.mRotation = out;
94
0
        return;
95
0
    }
96
97
98
    /* Optimize UV translation in the U direction. To determine whether
99
     * or not we can optimize we need to look at the requested mapping
100
     * type (e.g. if mirroring is active there IS a difference between
101
     * offset 2 and 3)
102
     */
103
0
    rounded = (int)info.mTranslation.x;
104
0
    if (rounded) {
105
0
        float out = 0.0f;
106
0
        szTemp[0] = 0;
107
0
        if (aiTextureMapMode_Wrap == info.mapU) {
108
            // Wrap - simple take the fraction of the field
109
0
            out = info.mTranslation.x-(float)rounded;
110
0
      ai_snprintf(szTemp, 512, "[w] UV U offset %f can be simplified to %f", info.mTranslation.x, out);
111
0
        }
112
0
        else if (aiTextureMapMode_Mirror == info.mapU && 1 != rounded)  {
113
            // Mirror
114
0
            if (rounded % 2)
115
0
                rounded--;
116
0
            out = info.mTranslation.x-(float)rounded;
117
118
0
            ai_snprintf(szTemp,512,"[m/d] UV U offset %f can be simplified to %f",info.mTranslation.x,out);
119
0
        }
120
0
        else if (aiTextureMapMode_Clamp == info.mapU || aiTextureMapMode_Decal == info.mapU)    {
121
            // Clamp - translations beyond 1,1 are senseless
122
0
            ai_snprintf(szTemp,512,"[c] UV U offset %f can be clamped to 1.0f",info.mTranslation.x);
123
124
0
            out = 1.f;
125
0
        }
126
0
        if (szTemp[0])      {
127
0
            ASSIMP_LOG_INFO(szTemp);
128
0
            info.mTranslation.x = out;
129
0
        }
130
0
    }
131
132
    /* Optimize UV translation in the V direction. To determine whether
133
     * or not we can optimize we need to look at the requested mapping
134
     * type (e.g. if mirroring is active there IS a difference between
135
     * offset 2 and 3)
136
     */
137
0
    rounded = (int)info.mTranslation.y;
138
0
    if (rounded) {
139
0
        float out = 0.0f;
140
0
        szTemp[0] = 0;
141
0
        if (aiTextureMapMode_Wrap == info.mapV) {
142
            // Wrap - simple take the fraction of the field
143
0
            out = info.mTranslation.y-(float)rounded;
144
0
            ::ai_snprintf(szTemp,512,"[w] UV V offset %f can be simplified to %f",info.mTranslation.y,out);
145
0
        }
146
0
        else if (aiTextureMapMode_Mirror == info.mapV  && 1 != rounded) {
147
            // Mirror
148
0
            if (rounded % 2)
149
0
                rounded--;
150
0
            out = info.mTranslation.x-(float)rounded;
151
152
0
            ::ai_snprintf(szTemp,512,"[m/d] UV V offset %f can be simplified to %f",info.mTranslation.y,out);
153
0
        }
154
0
        else if (aiTextureMapMode_Clamp == info.mapV || aiTextureMapMode_Decal == info.mapV)    {
155
            // Clamp - translations beyond 1,1 are senseless
156
0
            ::ai_snprintf(szTemp,512,"[c] UV V offset %f can be clamped to 1.0f",info.mTranslation.y);
157
158
0
            out = 1.f;
159
0
        }
160
0
        if (szTemp[0])  {
161
0
            ASSIMP_LOG_INFO(szTemp);
162
0
            info.mTranslation.y = out;
163
0
        }
164
0
    }
165
0
}
166
167
// ------------------------------------------------------------------------------------------------
168
0
void UpdateUVIndex(const std::list<TTUpdateInfo>& l, unsigned int n) {
169
    // Don't set if == 0 && wasn't set before
170
0
    for (std::list<TTUpdateInfo>::const_iterator it = l.begin();it != l.end(); ++it) {
171
0
        const TTUpdateInfo& info = *it;
172
173
0
        if (info.directShortcut)
174
0
            *info.directShortcut = n;
175
0
        else if (!n)
176
0
        {
177
0
            info.mat->AddProperty<int>((int*)&n,1,AI_MATKEY_UVWSRC(info.semantic,info.index));
178
0
        }
179
0
    }
180
0
}
181
182
// ------------------------------------------------------------------------------------------------
183
0
inline static const char* MappingModeToChar(aiTextureMapMode map) {
184
0
    if (aiTextureMapMode_Wrap == map)
185
0
        return "-w";
186
187
0
    if (aiTextureMapMode_Mirror == map)
188
0
        return "-m";
189
190
0
    return "-c";
191
0
}
192
193
// ------------------------------------------------------------------------------------------------
194
0
void TextureTransformStep::Execute( aiScene* pScene) {
195
0
    ASSIMP_LOG_DEBUG("TransformUVCoordsProcess begin");
196
197
    /*  We build a per-mesh list of texture transformations we'll need
198
     *  to apply. To achieve this, we iterate through all materials,
199
     *  find all textures and get their transformations and UV indices.
200
     *  Then we search for all meshes using this material.
201
     */
202
0
    typedef std::list<STransformVecInfo> MeshTrafoList;
203
0
    std::vector<MeshTrafoList> meshLists(pScene->mNumMeshes);
204
205
0
    for (unsigned int i = 0; i < pScene->mNumMaterials;++i) {
206
207
0
        aiMaterial* mat = pScene->mMaterials[i];
208
0
        for (unsigned int a = 0; a < mat->mNumProperties;++a)   {
209
210
0
            aiMaterialProperty* prop = mat->mProperties[a];
211
0
            if (!::strcmp( prop->mKey.data, "$tex.file"))   {
212
0
                STransformVecInfo info;
213
214
                // Setup a shortcut structure to allow for a fast updating
215
                // of the UV index later
216
0
                TTUpdateInfo update;
217
0
                update.mat = (aiMaterial*) mat;
218
0
                update.semantic = prop->mSemantic;
219
0
                update.index = prop->mIndex;
220
221
                // Get textured properties and transform
222
0
                for (unsigned int a2 = 0; a2 < mat->mNumProperties;++a2)  {
223
0
                    aiMaterialProperty* prop2 = mat->mProperties[a2];
224
0
                    if (prop2->mSemantic != prop->mSemantic || prop2->mIndex != prop->mIndex) {
225
0
                        continue;
226
0
                    }
227
228
0
                    if ( !::strcmp( prop2->mKey.data, "$tex.uvwsrc")) {
229
0
                        info.uvIndex = *((int*)prop2->mData);
230
231
                        // Store a direct pointer for later use
232
0
                        update.directShortcut = (unsigned int*) prop2->mData;
233
0
                    }
234
235
0
                    else if ( !::strcmp( prop2->mKey.data, "$tex.mapmodeu")) {
236
0
                        info.mapU = *((aiTextureMapMode*)prop2->mData);
237
0
                    }
238
0
                    else if ( !::strcmp( prop2->mKey.data, "$tex.mapmodev")) {
239
0
                        info.mapV = *((aiTextureMapMode*)prop2->mData);
240
0
                    }
241
0
                    else if ( !::strcmp( prop2->mKey.data, "$tex.uvtrafo"))  {
242
                        // ValidateDS should check this
243
0
                        ai_assert(prop2->mDataLength >= 20);
244
0
                        ::memcpy(&info.mTranslation.x,prop2->mData,sizeof(float)*5);
245
246
                        // Directly remove this property from the list
247
0
                        mat->mNumProperties--;
248
0
                        for (unsigned int a3 = a2; a3 < mat->mNumProperties;++a3) {
249
0
                            mat->mProperties[a3] = mat->mProperties[a3+1];
250
0
                        }
251
252
0
                        delete prop2;
253
254
                        // Warn: could be an underflow, but this does not invoke undefined behaviour
255
0
                        --a2;
256
0
                    }
257
0
                }
258
259
                // Find out which transformations are to be evaluated
260
0
                if (!(configFlags & AI_UVTRAFO_ROTATION)) {
261
0
                    info.mRotation = 0.f;
262
0
                }
263
0
                if (!(configFlags & AI_UVTRAFO_SCALING)) {
264
0
                    info.mScaling = aiVector2D(1.f,1.f);
265
0
                }
266
0
                if (!(configFlags & AI_UVTRAFO_TRANSLATION)) {
267
0
                    info.mTranslation = aiVector2D(0.f,0.f);
268
0
                }
269
270
                // Do some preprocessing
271
0
                PreProcessUVTransform(info);
272
0
                info.uvIndex = std::min(info.uvIndex,AI_MAX_NUMBER_OF_TEXTURECOORDS -1u);
273
274
                // Find out whether this material is used by more than
275
                // one mesh. This will make our task much, much more difficult!
276
0
                unsigned int cnt = 0;
277
0
                for (unsigned int n = 0; n < pScene->mNumMeshes;++n)    {
278
0
                    if (pScene->mMeshes[n]->mMaterialIndex == i)
279
0
                        ++cnt;
280
0
                }
281
282
0
                if (!cnt)
283
0
                    continue;
284
0
                else if (1 != cnt)  {
285
                    // This material is referenced by more than one mesh!
286
                    // So we need to make sure the UV index for the texture
287
                    // is identical for each of it ...
288
0
                    info.lockedPos = AI_TT_UV_IDX_LOCK_TBD;
289
0
                }
290
291
                // Get all corresponding meshes
292
0
                for (unsigned int n = 0; n < pScene->mNumMeshes;++n)    {
293
0
                    aiMesh* mesh = pScene->mMeshes[n];
294
0
                    if (mesh->mMaterialIndex != i || !mesh->mTextureCoords[0])
295
0
                        continue;
296
297
0
                    unsigned int uv = info.uvIndex;
298
0
                    if (!mesh->mTextureCoords[uv])  {
299
                        // If the requested UV index is not available, take the first one instead.
300
0
                        uv = 0;
301
0
                    }
302
303
0
                    if (mesh->mNumUVComponents[info.uvIndex] >= 3){
304
0
                        ASSIMP_LOG_WARN("UV transformations on 3D mapping channels are not supported");
305
0
                        continue;
306
0
                    }
307
308
0
                    MeshTrafoList::iterator it;
309
310
                    // Check whether we have this transform setup already
311
0
                    for (it = meshLists[n].begin();it != meshLists[n].end(); ++it)  {
312
313
0
                        if ((*it) == info && (*it).uvIndex == uv)   {
314
0
                            (*it).updateList.push_back(update);
315
0
                            break;
316
0
                        }
317
0
                    }
318
319
0
                    if (it == meshLists[n].end())   {
320
0
                        meshLists[n].push_back(info);
321
0
                        meshLists[n].back().uvIndex = uv;
322
0
                        meshLists[n].back().updateList.push_back(update);
323
0
                    }
324
0
                }
325
0
            }
326
0
        }
327
0
    }
328
329
0
    char buffer[1024]; // should be sufficiently large
330
0
    unsigned int outChannels = 0, inChannels = 0, transformedChannels = 0;
331
332
    // Now process all meshes. Important: we don't remove unreferenced UV channels.
333
    // This is a job for the RemoveUnreferencedData-Step.
334
0
    for (unsigned int q = 0; q < pScene->mNumMeshes;++q)    {
335
336
0
        aiMesh* mesh = pScene->mMeshes[q];
337
0
        MeshTrafoList& trafo =  meshLists[q];
338
339
0
        inChannels += mesh->GetNumUVChannels();
340
341
0
        if (!mesh->mTextureCoords[0] || trafo.empty() ||  (trafo.size() == 1 && trafo.begin()->IsUntransformed())) {
342
0
            outChannels += mesh->GetNumUVChannels();
343
0
            continue;
344
0
        }
345
346
        // Move untransformed UV channels to the first position in the list ....
347
        // except if we need a new locked index which should be as small as possible
348
0
        bool veto = false, need = false;
349
0
        unsigned int cnt = 0;
350
0
        unsigned int untransformed = 0;
351
352
0
        MeshTrafoList::iterator it,it2;
353
0
        for (it = trafo.begin();it != trafo.end(); ++it,++cnt)  {
354
355
0
            if (!(*it).IsUntransformed()) {
356
0
                need = true;
357
0
            }
358
359
0
            if ((*it).lockedPos == AI_TT_UV_IDX_LOCK_TBD)   {
360
                // Lock this index and make sure it won't be changed
361
0
                (*it).lockedPos = cnt;
362
0
                veto = true;
363
0
                continue;
364
0
            }
365
366
0
            if (!veto && it != trafo.begin() && (*it).IsUntransformed())    {
367
0
                for (it2 = trafo.begin();it2 != it; ++it2) {
368
0
                    if (!(*it2).IsUntransformed())
369
0
                        break;
370
0
                }
371
0
                trafo.insert(it2,*it);
372
0
                trafo.erase(it);
373
0
                break;
374
0
            }
375
0
        }
376
0
        if (!need)
377
0
            continue;
378
379
        // Find all that are not at their 'locked' position and move them to it.
380
        // Conflicts are possible but quite unlikely.
381
0
        cnt = 0;
382
0
        for (it = trafo.begin();it != trafo.end(); ++it,++cnt) {
383
0
            if ((*it).lockedPos != AI_TT_UV_IDX_LOCK_NONE && (*it).lockedPos != cnt) {
384
0
                it2 = trafo.begin();
385
0
                while ((*it2).lockedPos != (*it).lockedPos)
386
0
                    ++it2;
387
388
0
                if ((*it2).lockedPos != AI_TT_UV_IDX_LOCK_NONE) {
389
0
                    ASSIMP_LOG_ERROR("Channel mismatch, can't compute all transformations properly [design bug]");
390
0
                    continue;
391
0
                }
392
393
0
                std::swap(*it2,*it);
394
0
                if ((*it).lockedPos == untransformed)
395
0
                    untransformed = cnt;
396
0
            }
397
0
        }
398
399
        // ... and add dummies for all unreferenced channels
400
        // at the end of the list
401
0
        bool ref[AI_MAX_NUMBER_OF_TEXTURECOORDS];
402
0
        for (unsigned int n = 0; n < AI_MAX_NUMBER_OF_TEXTURECOORDS;++n)
403
0
            ref[n] = !mesh->mTextureCoords[n];
404
405
0
        for (it = trafo.begin();it != trafo.end(); ++it)
406
0
            ref[(*it).uvIndex] = true;
407
408
0
        for (unsigned int n = 0; n < AI_MAX_NUMBER_OF_TEXTURECOORDS;++n) {
409
0
            if (ref[n])
410
0
                continue;
411
0
            trafo.emplace_back();
412
0
            trafo.back().uvIndex = n;
413
0
        }
414
415
        // Then check whether this list breaks the channel limit.
416
        // The unimportant ones are at the end of the list, so
417
        // it shouldn't be too worse if we remove them.
418
0
        unsigned int size = (unsigned int)trafo.size();
419
0
        if (size > AI_MAX_NUMBER_OF_TEXTURECOORDS) {
420
0
            if (!DefaultLogger::isNullLogger()) {
421
0
                ASSIMP_LOG_ERROR(static_cast<unsigned int>(trafo.size()), " UV channels required but just ",
422
0
                    AI_MAX_NUMBER_OF_TEXTURECOORDS, " available");
423
0
            }
424
0
            size = AI_MAX_NUMBER_OF_TEXTURECOORDS;
425
0
        }
426
427
0
        aiVector3D* old[AI_MAX_NUMBER_OF_TEXTURECOORDS];
428
0
        for (unsigned int n = 0; n < AI_MAX_NUMBER_OF_TEXTURECOORDS;++n)
429
0
            old[n] = mesh->mTextureCoords[n];
430
431
        // Now continue and generate the output channels. Channels
432
        // that we're not going to need later can be overridden.
433
0
        it = trafo.begin();
434
0
        for (unsigned int n = 0; n < trafo.size();++n,++it) {
435
0
            if (n >= size)  {
436
                // Try to use an untransformed channel for all channels we threw over board
437
0
                UpdateUVIndex((*it).updateList,untransformed);
438
0
                continue;
439
0
            }
440
441
0
            ++outChannels;
442
443
            // Write to the log
444
0
            if (!DefaultLogger::isNullLogger()) {
445
0
                ::ai_snprintf(buffer,1024,"Mesh %u, channel %u: t(%.3f,%.3f), s(%.3f,%.3f), r(%.3f), %s%s",
446
0
                    q,n,
447
0
                    (*it).mTranslation.x,
448
0
                    (*it).mTranslation.y,
449
0
                    (*it).mScaling.x,
450
0
                    (*it).mScaling.y,
451
0
                    AI_RAD_TO_DEG( (*it).mRotation),
452
0
                    MappingModeToChar ((*it).mapU),
453
0
                    MappingModeToChar ((*it).mapV));
454
455
0
                ASSIMP_LOG_INFO(buffer);
456
0
            }
457
458
            // Check whether we need a new buffer here
459
0
            if (mesh->mTextureCoords[n])    {
460
461
0
                it2 = it;
462
0
    ++it2;
463
0
                for (unsigned int m = n+1; m < size;++m, ++it2) {
464
0
                    if ((*it2).uvIndex == n){
465
0
                        it2 = trafo.begin();
466
0
                        break;
467
0
                    }
468
0
                }
469
0
                if (it2 == trafo.begin()) {               
470
0
        {
471
0
                        std::unique_ptr<aiVector3D[]> oldTextureCoords(mesh->mTextureCoords[n]);
472
0
                    }
473
0
                    mesh->mTextureCoords[n] = new aiVector3D[mesh->mNumVertices];
474
0
                }
475
0
            }
476
0
            else mesh->mTextureCoords[n] = new aiVector3D[mesh->mNumVertices];
477
478
0
            aiVector3D* src = old[(*it).uvIndex];
479
0
            aiVector3D* dest, *end;
480
0
            dest = mesh->mTextureCoords[n];
481
482
0
            ai_assert(nullptr != src);
483
484
            // Copy the data to the destination array
485
0
            if (dest != src) {
486
0
                ::memcpy(dest,src,sizeof(aiVector3D)*mesh->mNumVertices);
487
0
            }
488
489
0
            end = dest + mesh->mNumVertices;
490
491
            // Build a transformation matrix and transform all UV coords with it
492
0
            if (!(*it).IsUntransformed()) {
493
0
                const aiVector2D& trl = (*it).mTranslation;
494
0
                const aiVector2D& scl = (*it).mScaling;
495
496
                // fixme: simplify ..
497
0
                ++transformedChannels;
498
0
                aiMatrix3x3 matrix;
499
500
0
                aiMatrix3x3 m2,m3,m4,m5;
501
502
0
                m4.a1 = scl.x;
503
0
                m4.b2 = scl.y;
504
505
0
                m2.a3 = m2.b3 = 0.5f;
506
0
                m3.a3 = m3.b3 = -0.5f;
507
508
0
                if ((*it).mRotation > AI_TT_ROTATION_EPSILON )
509
0
                    aiMatrix3x3::RotationZ((*it).mRotation,matrix);
510
511
0
                m5.a3 += trl.x; m5.b3 += trl.y;
512
0
                matrix = m2 * m4 * matrix * m3 * m5;
513
514
0
                for (src = dest; src != end; ++src) { /* manual homogeneous divide */
515
0
                    src->z = 1.f;
516
0
                    *src = matrix * *src;
517
0
                    src->x /= src->z;
518
0
                    src->y /= src->z;
519
0
                    src->z = 0.f;
520
0
                }
521
0
            }
522
523
            // Update all UV indices
524
0
            UpdateUVIndex((*it).updateList,n);
525
0
        }
526
0
    }
527
528
    // Print some detailed statistics into the log
529
0
    if (!DefaultLogger::isNullLogger()) {
530
0
        if (transformedChannels)    {
531
0
            ASSIMP_LOG_INFO("TransformUVCoordsProcess end: ", outChannels, " output channels (in: ", inChannels, ", modified: ", transformedChannels,")");
532
0
        } else {
533
0
            ASSIMP_LOG_INFO("TransformUVCoordsProcess finished");
534
0
        }
535
0
    }
536
0
}
537
538
} // namespace Assimp