Coverage Report

Created: 2025-06-22 07:30

/src/assimp/code/AssetLib/Unreal/UnrealLoader.cpp
Line
Count
Source (jump to first uncovered line)
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  UnrealLoader.cpp
43
 *  @brief Implementation of the UNREAL (*.3D) importer class
44
 *
45
 *  Sources:
46
 *    http://local.wasp.uwa.edu.au/~pbourke/dataformats/unreal/
47
 */
48
49
#ifndef ASSIMP_BUILD_NO_3D_IMPORTER
50
51
#include "AssetLib/Unreal/UnrealLoader.h"
52
#include "PostProcessing/ConvertToLHProcess.h"
53
54
#include <assimp/ParsingUtils.h>
55
#include <assimp/StreamReader.h>
56
#include <assimp/fast_atof.h>
57
#include <assimp/importerdesc.h>
58
#include <assimp/scene.h>
59
#include <assimp/DefaultLogger.hpp>
60
#include <assimp/IOSystem.hpp>
61
#include <assimp/Importer.hpp>
62
63
#include <cstdint>
64
#include <memory>
65
66
namespace Assimp {
67
68
namespace Unreal {
69
70
// Mesh-specific fags.
71
enum MeshFlags {
72
    MF_INVALID = -1,            // Not set
73
    MF_NORMAL_OS = 0,           // Normal one-sided
74
    MF_NORMAL_TS = 1,           // Normal two-sided
75
    MF_NORMAL_TRANS_TS = 2,     // Translucent two-sided
76
    MF_NORMAL_MASKED_TS = 3,    // Masked two-sided
77
    MF_NORMAL_MOD_TS = 4,       // Modulation blended two-sided
78
    MF_WEAPON_PLACEHOLDER = 8   // Placeholder triangle for weapon positioning (invisible)
79
};
80
81
// a single triangle
82
struct Triangle {
83
    uint16_t mVertex[3];        // Vertex indices
84
    char mType;                 // James' Mesh Type
85
    char mColor;                // Color for flat and Gourand Shaded
86
    unsigned char mTex[3][2];   // Texture UV coordinates
87
    unsigned char mTextureNum;  // Source texture offset
88
    char mFlags;                // Unreal Mesh Flags (unused)
89
    unsigned int matIndex;      // Material index
90
};
91
92
// temporary representation for a material
93
struct TempMat {
94
    TempMat() :
95
0
            type(MF_NORMAL_OS), tex(), numFaces(0) {}
96
97
    explicit TempMat(const Triangle &in) :
98
0
            type((Unreal::MeshFlags)in.mType), tex(in.mTextureNum), numFaces(0) {}
99
100
    // type of mesh
101
    Unreal::MeshFlags type;
102
103
    // index of texture
104
    unsigned int tex;
105
106
    // number of faces using us
107
    unsigned int numFaces;
108
109
    // for std::find
110
0
    bool operator==(const TempMat &o) {
111
0
        return (tex == o.tex && type == o.type);
112
0
    }
113
};
114
115
// A single vertex in an unsigned int 32 bit
116
struct Vertex {
117
    int32_t X : 11;
118
    int32_t Y : 11;
119
    int32_t Z : 10;
120
};
121
122
// UNREAL vertex compression
123
0
inline void CompressVertex(const aiVector3D &v, uint32_t &out) {
124
0
    union {
125
0
        Vertex n;
126
0
        int32_t t;
127
0
    };
128
0
    t = 0;
129
0
    n.X = (int32_t)v.x;
130
0
    n.Y = (int32_t)v.y;
131
0
    n.Z = (int32_t)v.z;
132
0
    ::memcpy(&out, &t, sizeof(int32_t));
133
0
}
134
135
// UNREAL vertex decompression
136
0
inline void DecompressVertex(aiVector3D &v, int32_t in) {
137
0
    union {
138
0
        Vertex n;
139
0
        int32_t i;
140
0
    };
141
0
    i = in;
142
143
0
    v.x = (float)n.X;
144
0
    v.y = (float)n.Y;
145
0
    v.z = (float)n.Z;
146
0
}
147
148
} // end namespace Unreal
149
150
static constexpr aiImporterDesc desc = {
151
    "Unreal Mesh Importer",
152
    "",
153
    "",
154
    "",
155
    aiImporterFlags_SupportTextFlavour,
156
    0,
157
    0,
158
    0,
159
    0,
160
    "3d uc"
161
};
162
163
// ------------------------------------------------------------------------------------------------
164
// Constructor to be privately used by Importer
165
UnrealImporter::UnrealImporter() :
166
1.50k
        mConfigFrameID(0), mConfigHandleFlags(true) {
167
    // empty
168
1.50k
}
169
170
// ------------------------------------------------------------------------------------------------
171
// Returns whether the class can handle the format of the given file.
172
758
bool UnrealImporter::CanRead(const std::string &filename, IOSystem * /*pIOHandler*/, bool /*checkSig*/) const {
173
758
    return SimpleExtensionCheck(filename, "3d", "uc");
174
758
}
175
176
// ------------------------------------------------------------------------------------------------
177
// Build a string of all file extensions supported
178
2.38k
const aiImporterDesc *UnrealImporter::GetInfo() const {
179
2.38k
    return &desc;
180
2.38k
}
181
182
// ------------------------------------------------------------------------------------------------
183
// Setup configuration properties for the loader
184
29
void UnrealImporter::SetupProperties(const Importer *pImp) {
185
    // The
186
    // AI_CONFIG_IMPORT_UNREAL_KEYFRAME option overrides the
187
    // AI_CONFIG_IMPORT_GLOBAL_KEYFRAME option.
188
29
    mConfigFrameID = pImp->GetPropertyInteger(AI_CONFIG_IMPORT_UNREAL_KEYFRAME, -1);
189
29
    if (static_cast<unsigned int>(-1) == mConfigFrameID) {
190
29
        mConfigFrameID = pImp->GetPropertyInteger(AI_CONFIG_IMPORT_GLOBAL_KEYFRAME, 0);
191
29
    }
192
193
    // AI_CONFIG_IMPORT_UNREAL_HANDLE_FLAGS, default is true
194
29
    mConfigHandleFlags = (0 != pImp->GetPropertyInteger(AI_CONFIG_IMPORT_UNREAL_HANDLE_FLAGS, 1));
195
29
}
196
197
// ------------------------------------------------------------------------------------------------
198
// Imports the given file into the given scene structure.
199
void UnrealImporter::InternReadFile(const std::string &pFile,
200
29
        aiScene *pScene, IOSystem *pIOHandler) {
201
    // For any of the 3 files being passed get the three correct paths
202
    // First of all, determine file extension
203
29
    std::string::size_type pos = pFile.find_last_of('.');
204
29
    std::string extension = GetExtension(pFile);
205
206
29
    std::string d_path, a_path, uc_path;
207
29
    if (extension == "3d") {
208
        // jjjj_d.3d
209
        // jjjj_a.3d
210
26
        pos = pFile.find_last_of('_');
211
26
        if (std::string::npos == pos) {
212
0
            throw DeadlyImportError("UNREAL: Unexpected naming scheme");
213
0
        }
214
26
        extension = pFile.substr(0, pos);
215
26
    } else {
216
3
        extension = pFile.substr(0, pos);
217
3
    }
218
219
    // build proper paths
220
29
    d_path = extension + "_d.3d";
221
29
    a_path = extension + "_a.3d";
222
29
    uc_path = extension + ".uc";
223
224
29
    ASSIMP_LOG_DEBUG("UNREAL: data file is ", d_path);
225
29
    ASSIMP_LOG_DEBUG("UNREAL: aniv file is ", a_path);
226
29
    ASSIMP_LOG_DEBUG("UNREAL: uc file is ", uc_path);
227
228
    // and open the files ... we can't live without them
229
29
    std::unique_ptr<IOStream> p(pIOHandler->Open(d_path));
230
29
    if (!p)
231
1
        throw DeadlyImportError("UNREAL: Unable to open _d file");
232
28
    StreamReaderLE d_reader(pIOHandler->Open(d_path));
233
234
28
    const uint16_t numTris = d_reader.GetI2();
235
28
    const uint16_t numVert = d_reader.GetI2();
236
28
    d_reader.IncPtr(44);
237
28
    if (!numTris || numVert < 3) {
238
0
        throw DeadlyImportError("UNREAL: Invalid number of vertices/triangles");
239
0
    }
240
241
    // maximum texture index
242
28
    unsigned int maxTexIdx = 0;
243
244
    // collect triangles
245
28
    std::vector<Unreal::Triangle> triangles(numTris);
246
49.0k
    for (auto &tri : triangles) {
247
196k
        for (unsigned int i = 0; i < 3; ++i) {
248
147k
            tri.mVertex[i] = d_reader.GetI2();
249
147k
            if (tri.mVertex[i] >= numTris) {
250
90.0k
                ASSIMP_LOG_WARN("UNREAL: vertex index out of range");
251
90.0k
                tri.mVertex[i] = 0;
252
90.0k
            }
253
147k
        }
254
49.0k
        tri.mType = d_reader.GetI1();
255
256
        // handle mesh flagss?
257
49.0k
        if (mConfigHandleFlags) {
258
49.0k
            tri.mType = Unreal::MF_NORMAL_OS;
259
49.0k
        } else {
260
            // ignore MOD and MASKED for the moment, treat them as two-sided
261
12
            if (tri.mType == Unreal::MF_NORMAL_MOD_TS || tri.mType == Unreal::MF_NORMAL_MASKED_TS)
262
0
                tri.mType = Unreal::MF_NORMAL_TS;
263
12
        }
264
49.0k
        d_reader.IncPtr(1);
265
266
196k
        for (unsigned int i = 0; i < 3; ++i) {
267
441k
            for (unsigned int i2 = 0; i2 < 2; ++i2) {
268
294k
                tri.mTex[i][i2] = d_reader.GetI1();
269
294k
            }
270
147k
        }
271
272
49.0k
        tri.mTextureNum = d_reader.GetI1();
273
49.0k
        maxTexIdx = std::max(maxTexIdx, (unsigned int)tri.mTextureNum);
274
49.0k
        d_reader.IncPtr(1);
275
49.0k
    }
276
277
28
    p.reset(pIOHandler->Open(a_path));
278
28
    if (!p) {
279
0
        throw DeadlyImportError("UNREAL: Unable to open _a file");
280
0
    }
281
28
    StreamReaderLE a_reader(pIOHandler->Open(a_path));
282
283
    // read number of frames
284
28
    const uint32_t numFrames = a_reader.GetI2();
285
28
    if (mConfigFrameID >= numFrames) {
286
0
        throw DeadlyImportError("UNREAL: The requested frame does not exist");
287
0
    }
288
289
    // read aniv file length
290
28
    if (uint32_t st = a_reader.GetI2(); st != numVert * 4u) {
291
0
        throw DeadlyImportError("UNREAL: Unexpected aniv file length");
292
0
    }
293
294
    // skip to our frame
295
28
    a_reader.IncPtr(mConfigFrameID * numVert * 4);
296
297
    // collect vertices
298
28
    std::vector<aiVector3D> vertices(numVert);
299
28
    for (auto &vertex : vertices) {
300
0
        int32_t val = a_reader.GetI4();
301
0
        Unreal::DecompressVertex(vertex, val);
302
0
    }
303
304
    // list of textures.
305
28
    std::vector<std::pair<unsigned int, std::string>> textures;
306
307
    // allocate the output scene
308
28
    aiNode *nd = pScene->mRootNode = new aiNode();
309
28
    nd->mName.Set("<UnrealRoot>");
310
311
    // we can live without the uc file if necessary
312
28
    std::unique_ptr<IOStream> pb(pIOHandler->Open(uc_path));
313
28
    if (pb) {
314
315
0
        std::vector<char> _data;
316
0
        TextFileToBuffer(pb.get(), _data);
317
0
        const char *data = &_data[0];
318
0
        const char *end = &_data[_data.size() - 1] + 1;
319
320
0
        std::vector<std::pair<std::string, std::string>> tempTextures;
321
322
        // do a quick search in the UC file for some known, usually texture-related, tags
323
0
        for (; *data; ++data) {
324
0
            if (TokenMatchI(data, "#exec", 5)) {
325
0
                SkipSpacesAndLineEnd(&data, end);
326
327
                // #exec TEXTURE IMPORT [...] NAME=jjjjj [...] FILE=jjjj.pcx [...]
328
0
                if (TokenMatchI(data, "TEXTURE", 7)) {
329
0
                    SkipSpacesAndLineEnd(&data, end);
330
331
0
                    if (TokenMatchI(data, "IMPORT", 6)) {
332
0
                        tempTextures.emplace_back();
333
0
                        std::pair<std::string, std::string> &me = tempTextures.back();
334
0
                        for (; !IsLineEnd(*data); ++data) {
335
0
                            if (!ASSIMP_strincmp(data, "NAME=", 5)) {
336
0
                                const char *d = data += 5;
337
0
                                for (; !IsSpaceOrNewLine(*data); ++data)
338
0
                                    ;
339
0
                                me.first = std::string(d, (size_t)(data - d));
340
0
                            } else if (!ASSIMP_strincmp(data, "FILE=", 5)) {
341
0
                                const char *d = data += 5;
342
0
                                for (; !IsSpaceOrNewLine(*data); ++data)
343
0
                                    ;
344
0
                                me.second = std::string(d, (size_t)(data - d));
345
0
                            }
346
0
                        }
347
0
                        if (!me.first.length() || !me.second.length()) {
348
0
                            tempTextures.pop_back();
349
0
                        }
350
0
                    }
351
0
                }
352
                // #exec MESHMAP SETTEXTURE MESHMAP=box NUM=1 TEXTURE=Jtex1
353
                // #exec MESHMAP SCALE MESHMAP=box X=0.1 Y=0.1 Z=0.2
354
0
                else if (TokenMatchI(data, "MESHMAP", 7)) {
355
0
                    SkipSpacesAndLineEnd(&data, end);
356
357
0
                    if (TokenMatchI(data, "SETTEXTURE", 10)) {
358
359
0
                        textures.emplace_back();
360
0
                        std::pair<unsigned int, std::string> &me = textures.back();
361
362
0
                        for (; !IsLineEnd(*data); ++data) {
363
0
                            if (!ASSIMP_strincmp(data, "NUM=", 4)) {
364
0
                                data += 4;
365
0
                                me.first = strtoul10(data, &data);
366
0
                            } else if (!ASSIMP_strincmp(data, "TEXTURE=", 8)) {
367
0
                                data += 8;
368
0
                                const char *d = data;
369
0
                                for (; !IsSpaceOrNewLine(*data); ++data);
370
0
                                me.second = std::string(d, (size_t)(data - d));
371
372
                                // try to find matching path names, doesn't care if we don't find them
373
0
                                for (std::vector<std::pair<std::string, std::string>>::const_iterator it = tempTextures.begin();
374
0
                                        it != tempTextures.end(); ++it) {
375
0
                                    if ((*it).first == me.second) {
376
0
                                        me.second = (*it).second;
377
0
                                        break;
378
0
                                    }
379
0
                                }
380
0
                            }
381
0
                        }
382
0
                    } else if (TokenMatchI(data, "SCALE", 5)) {
383
384
0
                        for (; !IsLineEnd(*data); ++data) {
385
0
                            if (data[0] == 'X' && data[1] == '=') {
386
0
                                data = fast_atoreal_move<float>(data + 2, (float &)nd->mTransformation.a1);
387
0
                            } else if (data[0] == 'Y' && data[1] == '=') {
388
0
                                data = fast_atoreal_move<float>(data + 2, (float &)nd->mTransformation.b2);
389
0
                            } else if (data[0] == 'Z' && data[1] == '=') {
390
0
                                data = fast_atoreal_move<float>(data + 2, (float &)nd->mTransformation.c3);
391
0
                            }
392
0
                        }
393
0
                    }
394
0
                }
395
0
            }
396
0
        }
397
28
    } else {
398
28
        ASSIMP_LOG_ERROR("Unable to open .uc file");
399
28
    }
400
401
28
    std::vector<Unreal::TempMat> materials;
402
28
    materials.reserve(textures.size() * 2 + 5);
403
404
    // find out how many output meshes and materials we'll have and build material indices
405
28
    for (auto &tri : triangles) {
406
0
        Unreal::TempMat mat(tri);
407
0
        auto nt = std::find(materials.begin(), materials.end(), mat);
408
0
        if (nt == materials.end()) {
409
            // add material
410
0
            tri.matIndex = static_cast<unsigned int>(materials.size());
411
0
            mat.numFaces = 1;
412
0
            materials.push_back(mat);
413
414
0
            ++pScene->mNumMeshes;
415
0
        } else {
416
0
            tri.matIndex = static_cast<unsigned int>(nt - materials.begin());
417
0
            ++nt->numFaces;
418
0
        }
419
0
    }
420
421
28
    if (!pScene->mNumMeshes) {
422
0
        throw DeadlyImportError("UNREAL: Unable to find valid mesh data");
423
0
    }
424
425
    // allocate meshes and bind them to the node graph
426
28
    pScene->mMeshes = new aiMesh *[pScene->mNumMeshes];
427
28
    pScene->mMaterials = new aiMaterial *[pScene->mNumMaterials = pScene->mNumMeshes];
428
429
28
    nd->mNumMeshes = pScene->mNumMeshes;
430
28
    nd->mMeshes = new unsigned int[nd->mNumMeshes];
431
28
    for (unsigned int i = 0; i < pScene->mNumMeshes; ++i) {
432
0
        aiMesh *m = pScene->mMeshes[i] = new aiMesh();
433
0
        m->mPrimitiveTypes = aiPrimitiveType_TRIANGLE;
434
435
0
        const unsigned int num = materials[i].numFaces;
436
0
        m->mFaces = new aiFace[num];
437
0
        m->mVertices = new aiVector3D[num * 3];
438
0
        m->mTextureCoords[0] = new aiVector3D[num * 3];
439
440
0
        nd->mMeshes[i] = i;
441
442
        // create materials, too
443
0
        aiMaterial *mat = new aiMaterial();
444
0
        pScene->mMaterials[i] = mat;
445
446
        // all white by default - texture rulez
447
0
        aiColor3D color(1.f, 1.f, 1.f);
448
449
0
        aiString s;
450
0
        ::ai_snprintf(s.data, AI_MAXLEN, "mat%u_tx%u_", i, materials[i].tex);
451
452
        // set the two-sided flag
453
0
        if (materials[i].type == Unreal::MF_NORMAL_TS) {
454
0
            const int twosided = 1;
455
0
            mat->AddProperty(&twosided, 1, AI_MATKEY_TWOSIDED);
456
0
            ::strcat(s.data, "ts_");
457
0
        } else
458
0
            ::strcat(s.data, "os_");
459
460
        // make TRANS faces 90% opaque that RemRedundantMaterials won't catch us
461
0
        if (materials[i].type == Unreal::MF_NORMAL_TRANS_TS) {
462
0
            const float opac = 0.9f;
463
0
            mat->AddProperty(&opac, 1, AI_MATKEY_OPACITY);
464
0
            ::strcat(s.data, "tran_");
465
0
        } else
466
0
            ::strcat(s.data, "opaq_");
467
468
        // a special name for the weapon attachment point
469
0
        if (materials[i].type == Unreal::MF_WEAPON_PLACEHOLDER) {
470
0
            s.length = ::ai_snprintf(s.data, AI_MAXLEN, "$WeaponTag$");
471
0
            color = aiColor3D(0.f, 0.f, 0.f);
472
0
        }
473
474
        // set color and name
475
0
        mat->AddProperty(&color, 1, AI_MATKEY_COLOR_DIFFUSE);
476
0
        s.length = static_cast<ai_uint32>(::strlen(s.data));
477
0
        mat->AddProperty(&s, AI_MATKEY_NAME);
478
479
        // set texture, if any
480
0
        const unsigned int tex = materials[i].tex;
481
0
        for (auto it = textures.begin(); it != textures.end(); ++it) {
482
0
            if ((*it).first == tex) {
483
0
                s.Set((*it).second);
484
0
                mat->AddProperty(&s, AI_MATKEY_TEXTURE_DIFFUSE(0));
485
0
                break;
486
0
            }
487
0
        }
488
0
    }
489
490
    // fill them.
491
28
    for (const Unreal::Triangle &tri : triangles) {
492
0
        Unreal::TempMat mat(tri);
493
0
        auto nt = std::find(materials.begin(), materials.end(), mat);
494
495
0
        aiMesh *mesh = pScene->mMeshes[nt - materials.begin()];
496
0
        aiFace &f = mesh->mFaces[mesh->mNumFaces++];
497
0
        f.mIndices = new unsigned int[f.mNumIndices = 3];
498
499
0
        for (unsigned int i = 0; i < 3; ++i, mesh->mNumVertices++) {
500
0
            f.mIndices[i] = mesh->mNumVertices;
501
502
0
            mesh->mVertices[mesh->mNumVertices] = vertices[tri.mVertex[i]];
503
0
            mesh->mTextureCoords[0][mesh->mNumVertices] = aiVector3D(tri.mTex[i][0] / 255.f, 1.f - tri.mTex[i][1] / 255.f, 0.f);
504
0
        }
505
0
    }
506
507
    // convert to RH
508
28
    MakeLeftHandedProcess hero;
509
28
    hero.Execute(pScene);
510
511
28
    FlipWindingOrderProcess flipper;
512
28
    flipper.Execute(pScene);
513
28
}
514
515
} // namespace Assimp
516
517
#endif // !! ASSIMP_BUILD_NO_3D_IMPORTER