Coverage Report

Created: 2026-01-07 06:50

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/assimp/code/AssetLib/Unreal/UnrealLoader.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  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
327
        mConfigFrameID(0), mConfigHandleFlags(true) {
167
    // empty
168
327
}
169
170
// ------------------------------------------------------------------------------------------------
171
// Returns whether the class can handle the format of the given file.
172
159
bool UnrealImporter::CanRead(const std::string &filename, IOSystem * /*pIOHandler*/, bool /*checkSig*/) const {
173
159
    return SimpleExtensionCheck(filename, "3d", "uc");
174
159
}
175
176
// ------------------------------------------------------------------------------------------------
177
// Build a string of all file extensions supported
178
348
const aiImporterDesc *UnrealImporter::GetInfo() const {
179
348
    return &desc;
180
348
}
181
182
// ------------------------------------------------------------------------------------------------
183
// Setup configuration properties for the loader
184
15
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
15
    mConfigFrameID = pImp->GetPropertyInteger(AI_CONFIG_IMPORT_UNREAL_KEYFRAME, -1);
189
15
    if (static_cast<unsigned int>(-1) == mConfigFrameID) {
190
15
        mConfigFrameID = pImp->GetPropertyInteger(AI_CONFIG_IMPORT_GLOBAL_KEYFRAME, 0);
191
15
    }
192
193
    // AI_CONFIG_IMPORT_UNREAL_HANDLE_FLAGS, default is true
194
15
    mConfigHandleFlags = (0 != pImp->GetPropertyInteger(AI_CONFIG_IMPORT_UNREAL_HANDLE_FLAGS, 1));
195
15
}
196
197
// ------------------------------------------------------------------------------------------------
198
// Imports the given file into the given scene structure.
199
void UnrealImporter::InternReadFile(const std::string &pFile,
200
15
        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
15
    std::string::size_type pos = pFile.find_last_of('.');
204
15
    std::string extension = GetExtension(pFile);
205
206
15
    std::string d_path, a_path, uc_path;
207
15
    if (extension == "3d") {
208
        // jjjj_d.3d
209
        // jjjj_a.3d
210
15
        pos = pFile.find_last_of('_');
211
15
        if (std::string::npos == pos) {
212
0
            throw DeadlyImportError("UNREAL: Unexpected naming scheme");
213
0
        }
214
15
        extension = pFile.substr(0, pos);
215
15
    } else {
216
0
        extension = pFile.substr(0, pos);
217
0
    }
218
219
    // build proper paths
220
15
    d_path = extension + "_d.3d";
221
15
    a_path = extension + "_a.3d";
222
15
    uc_path = extension + ".uc";
223
224
15
    ASSIMP_LOG_DEBUG("UNREAL: data file is ", d_path);
225
15
    ASSIMP_LOG_DEBUG("UNREAL: aniv file is ", a_path);
226
15
    ASSIMP_LOG_DEBUG("UNREAL: uc file is ", uc_path);
227
228
    // and open the files ... we can't live without them
229
15
    std::unique_ptr<IOStream> p(pIOHandler->Open(d_path));
230
15
    if (!p)
231
0
        throw DeadlyImportError("UNREAL: Unable to open _d file");
232
15
    StreamReaderLE d_reader(pIOHandler->Open(d_path));
233
234
15
    const uint16_t numTris = d_reader.GetI2();
235
15
    const uint16_t numVert = d_reader.GetI2();
236
15
    d_reader.IncPtr(44);
237
15
    if (!numTris || numVert < 3) {
238
0
        throw DeadlyImportError("UNREAL: Invalid number of vertices/triangles");
239
0
    }
240
241
    // maximum texture index
242
15
    unsigned int maxTexIdx = 0;
243
244
    // collect triangles
245
15
    std::vector<Unreal::Triangle> triangles(numTris);
246
20.5k
    for (auto &tri : triangles) {
247
82.1k
        for (unsigned int i = 0; i < 3; ++i) {
248
61.6k
            tri.mVertex[i] = d_reader.GetI2();
249
61.6k
            if (tri.mVertex[i] >= numTris) {
250
32.4k
                ASSIMP_LOG_WARN("UNREAL: vertex index out of range");
251
32.4k
                tri.mVertex[i] = 0;
252
32.4k
            }
253
61.6k
        }
254
20.5k
        tri.mType = d_reader.GetI1();
255
256
        // handle mesh flagss?
257
20.5k
        if (mConfigHandleFlags) {
258
20.5k
            tri.mType = Unreal::MF_NORMAL_OS;
259
20.5k
        } else {
260
            // ignore MOD and MASKED for the moment, treat them as two-sided
261
10
            if (tri.mType == Unreal::MF_NORMAL_MOD_TS || tri.mType == Unreal::MF_NORMAL_MASKED_TS)
262
0
                tri.mType = Unreal::MF_NORMAL_TS;
263
10
        }
264
20.5k
        d_reader.IncPtr(1);
265
266
82.1k
        for (unsigned int i = 0; i < 3; ++i) {
267
184k
            for (unsigned int i2 = 0; i2 < 2; ++i2) {
268
123k
                tri.mTex[i][i2] = d_reader.GetI1();
269
123k
            }
270
61.5k
        }
271
272
20.5k
        tri.mTextureNum = d_reader.GetI1();
273
20.5k
        maxTexIdx = std::max(maxTexIdx, (unsigned int)tri.mTextureNum);
274
20.5k
        d_reader.IncPtr(1);
275
20.5k
    }
276
277
15
    p.reset(pIOHandler->Open(a_path));
278
15
    if (!p) {
279
0
        throw DeadlyImportError("UNREAL: Unable to open _a file");
280
0
    }
281
15
    StreamReaderLE a_reader(pIOHandler->Open(a_path));
282
283
    // read number of frames
284
15
    const uint32_t numFrames = a_reader.GetI2();
285
15
    if (mConfigFrameID >= numFrames) {
286
0
        throw DeadlyImportError("UNREAL: The requested frame does not exist");
287
0
    }
288
289
    // read aniv file length
290
15
    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
15
    a_reader.IncPtr(mConfigFrameID * numVert * 4);
296
297
    // collect vertices
298
15
    std::vector<aiVector3D> vertices(numVert);
299
15
    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
15
    std::vector<std::pair<unsigned int, std::string>> textures;
306
307
    // allocate the output scene
308
15
    aiNode *nd = pScene->mRootNode = new aiNode();
309
15
    nd->mName.Set("<UnrealRoot>");
310
311
    // we can live without the uc file if necessary
312
15
    std::unique_ptr<IOStream> pb(pIOHandler->Open(uc_path));
313
15
    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(data + 2, nd->mTransformation.a1);
387
0
                            } else if (data[0] == 'Y' && data[1] == '=') {
388
0
                                data = fast_atoreal_move(data + 2, nd->mTransformation.b2);
389
0
                            } else if (data[0] == 'Z' && data[1] == '=') {
390
0
                                data = fast_atoreal_move(data + 2, nd->mTransformation.c3);
391
0
                            }
392
0
                        }
393
0
                    }
394
0
                }
395
0
            }
396
0
        }
397
15
    } else {
398
15
        ASSIMP_LOG_ERROR("Unable to open .uc file");
399
15
    }
400
401
15
    std::vector<Unreal::TempMat> materials;
402
15
    materials.reserve(textures.size() * 2 + 5);
403
404
    // find out how many output meshes and materials we'll have and build material indices
405
15
    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
15
    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
15
    pScene->mMeshes = new aiMesh *[pScene->mNumMeshes];
427
15
    pScene->mMaterials = new aiMaterial *[pScene->mNumMaterials = pScene->mNumMeshes];
428
429
15
    nd->mNumMeshes = pScene->mNumMeshes;
430
15
    nd->mMeshes = new unsigned int[nd->mNumMeshes];
431
15
    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
15
    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
15
    MakeLeftHandedProcess hero;
509
15
    hero.Execute(pScene);
510
511
15
    FlipWindingOrderProcess flipper;
512
15
    flipper.Execute(pScene);
513
15
}
514
515
} // namespace Assimp
516
517
#endif // !! ASSIMP_BUILD_NO_3D_IMPORTER