Coverage Report

Created: 2025-08-28 06:38

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