Coverage Report

Created: 2025-08-26 06:41

/src/assimp/code/AssetLib/Obj/ObjExporter.cpp
Line
Count
Source (jump to first uncovered line)
1
/*
2
Open Asset Import Library (assimp)
3
----------------------------------------------------------------------
4
5
Copyright (c) 2006-2025, assimp team
6
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
12
following 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
43
#ifndef ASSIMP_BUILD_NO_EXPORT
44
#ifndef ASSIMP_BUILD_NO_OBJ_EXPORTER
45
46
#include "ObjExporter.h"
47
#include <assimp/Exceptional.h>
48
#include <assimp/StringComparison.h>
49
#include <assimp/version.h>
50
#include <assimp/IOSystem.hpp>
51
#include <assimp/Exporter.hpp>
52
#include <assimp/material.h>
53
#include <assimp/scene.h>
54
#include <memory>
55
56
using namespace Assimp;
57
58
namespace Assimp {
59
60
// ------------------------------------------------------------------------------------------------
61
// Worker function for exporting a scene to Wavefront OBJ. Prototyped and registered in Exporter.cpp
62
0
void ExportSceneObj(const char* pFile,IOSystem* pIOSystem, const aiScene* pScene, const ExportProperties* props) {
63
    // invoke the exporter
64
0
    ObjExporter exporter(pFile, pScene, false, props);
65
66
0
    if (exporter.mOutput.fail() || exporter.mOutputMat.fail()) {
67
0
        throw DeadlyExportError("output data creation failed. Most likely the file became too large: " + std::string(pFile));
68
0
    }
69
70
    // we're still here - export successfully completed. Write both the main OBJ file and the material script
71
0
    {
72
0
        std::unique_ptr<IOStream> outfile (pIOSystem->Open(pFile,"wt"));
73
0
        if (outfile == nullptr) {
74
0
            throw DeadlyExportError("could not open output .obj file: " + std::string(pFile));
75
0
        }
76
0
        outfile->Write( exporter.mOutput.str().c_str(), static_cast<size_t>(exporter.mOutput.tellp()),1);
77
0
    }
78
0
    {
79
0
        std::unique_ptr<IOStream> outfile (pIOSystem->Open(exporter.GetMaterialLibFileName(),"wt"));
80
0
        if (outfile == nullptr) {
81
0
            throw DeadlyExportError("could not open output .mtl file: " + std::string(exporter.GetMaterialLibFileName()));
82
0
        }
83
0
        outfile->Write( exporter.mOutputMat.str().c_str(), static_cast<size_t>(exporter.mOutputMat.tellp()),1);
84
0
    }
85
0
}
86
87
// ------------------------------------------------------------------------------------------------
88
// Worker function for exporting a scene to Wavefront OBJ without the material file. Prototyped and registered in Exporter.cpp
89
0
void ExportSceneObjNoMtl(const char* pFile,IOSystem* pIOSystem, const aiScene* pScene, const ExportProperties* props) {
90
    // invoke the exporter
91
0
    ObjExporter exporter(pFile, pScene, true, props);
92
93
0
    if (exporter.mOutput.fail() || exporter.mOutputMat.fail()) {
94
0
        throw DeadlyExportError("output data creation failed. Most likely the file became too large: " + std::string(pFile));
95
0
    }
96
97
    // we're still here - export successfully completed. Write both the main OBJ file and the material script
98
0
    {
99
0
        std::unique_ptr<IOStream> outfile (pIOSystem->Open(pFile,"wt"));
100
0
        if (outfile == nullptr) {
101
0
            throw DeadlyExportError("could not open output .obj file: " + std::string(pFile));
102
0
        }
103
0
        outfile->Write( exporter.mOutput.str().c_str(), static_cast<size_t>(exporter.mOutput.tellp()),1);
104
0
    }
105
106
107
0
}
108
109
} // end of namespace Assimp
110
111
static const std::string MaterialExt = ".mtl";
112
113
// ------------------------------------------------------------------------------------------------
114
ObjExporter::ObjExporter(const char* _filename, const aiScene* pScene, bool noMtl, const ExportProperties* props)
115
0
: filename(_filename)
116
0
, pScene(pScene)
117
0
, vn()
118
0
, vt()
119
0
, vp()
120
0
, useVc(false)
121
0
, mVnMap()
122
0
, mVtMap()
123
0
, mVpMap()
124
0
, mMeshes()
125
0
, endl("\n") {
126
    // make sure that all formatting happens using the standard, C locale and not the user's current locale
127
0
    const std::locale& l = std::locale("C");
128
0
    mOutput.imbue(l);
129
0
    mOutput.precision(ASSIMP_AI_REAL_TEXT_PRECISION);
130
0
    mOutputMat.imbue(l);
131
0
    mOutputMat.precision(ASSIMP_AI_REAL_TEXT_PRECISION);
132
133
0
    WriteGeometryFile(
134
0
        noMtl,
135
0
        props == nullptr ? true : props->GetPropertyBool("bJoinIdenticalVertices", true)
136
0
    );
137
0
    if ( !noMtl ) {
138
0
        WriteMaterialFile();
139
0
    }
140
0
}
141
142
// ------------------------------------------------------------------------------------------------
143
0
ObjExporter::~ObjExporter() = default;
144
145
// ------------------------------------------------------------------------------------------------
146
0
std::string ObjExporter::GetMaterialLibName() {
147
    // within the Obj file, we use just the relative file name with the path stripped
148
0
    const std::string& s = GetMaterialLibFileName();
149
0
    std::string::size_type il = s.find_last_of("/\\");
150
0
    if (il != std::string::npos) {
151
0
        return s.substr(il + 1);
152
0
    }
153
154
0
    return s;
155
0
}
156
157
// ------------------------------------------------------------------------------------------------
158
0
std::string ObjExporter::GetMaterialLibFileName() {
159
    // Remove existing .obj file extension so that the final material file name will be fileName.mtl and not fileName.obj.mtl
160
0
    size_t lastdot = filename.find_last_of('.');
161
0
    if ( lastdot != std::string::npos ) {
162
0
        return filename.substr( 0, lastdot ) + MaterialExt;
163
0
    }
164
165
0
    return filename + MaterialExt;
166
0
}
167
168
// ------------------------------------------------------------------------------------------------
169
0
void ObjExporter::WriteHeader(std::ostringstream& out) {
170
0
    out << "# File produced by Open Asset Import Library (http://www.assimp.sf.net)" << endl;
171
0
    out << "# (assimp v" << aiGetVersionMajor() << '.' << aiGetVersionMinor() << '.'
172
0
        << aiGetVersionRevision() << ")" << endl  << endl;
173
0
}
174
175
// ------------------------------------------------------------------------------------------------
176
0
std::string ObjExporter::GetMaterialName(unsigned int index) {
177
0
    static const std::string EmptyStr;
178
0
    if ( nullptr == pScene->mMaterials ) {
179
0
        return EmptyStr;
180
0
    }
181
0
    const aiMaterial* const mat = pScene->mMaterials[index];
182
0
    if ( nullptr == mat ) {
183
0
        return EmptyStr;
184
0
    }
185
186
0
    aiString s;
187
0
    if(AI_SUCCESS == mat->Get(AI_MATKEY_NAME,s)) {
188
0
        return std::string(s.data,s.length);
189
0
    }
190
191
0
    char number[ sizeof(unsigned int) * 3 + 1 ];
192
0
    ASSIMP_itoa10(number,index);
193
0
    return "$Material_" + std::string(number);
194
0
}
195
196
// ------------------------------------------------------------------------------------------------
197
0
void ObjExporter::WriteMaterialFile() {
198
0
    WriteHeader(mOutputMat);
199
200
0
    for(unsigned int i = 0; i < pScene->mNumMaterials; ++i) {
201
0
        const aiMaterial* const mat = pScene->mMaterials[i];
202
203
0
        int illum = 1;
204
0
        mOutputMat << "newmtl " << GetMaterialName(i)  << endl;
205
206
0
        aiColor4D c;
207
0
        if(AI_SUCCESS == mat->Get(AI_MATKEY_COLOR_DIFFUSE,c)) {
208
0
            mOutputMat << "Kd " << c.r << " " << c.g << " " << c.b << endl;
209
0
        }
210
0
        if(AI_SUCCESS == mat->Get(AI_MATKEY_COLOR_AMBIENT,c)) {
211
0
            mOutputMat << "Ka " << c.r << " " << c.g << " " << c.b << endl;
212
0
        }
213
0
        if(AI_SUCCESS == mat->Get(AI_MATKEY_COLOR_SPECULAR,c)) {
214
0
            mOutputMat << "Ks " << c.r << " " << c.g << " " << c.b << endl;
215
0
        }
216
0
        if(AI_SUCCESS == mat->Get(AI_MATKEY_COLOR_EMISSIVE,c)) {
217
0
            mOutputMat << "Ke " << c.r << " " << c.g << " " << c.b << endl;
218
0
        }
219
0
        if(AI_SUCCESS == mat->Get(AI_MATKEY_COLOR_TRANSPARENT,c)) {
220
0
            mOutputMat << "Tf " << c.r << " " << c.g << " " << c.b << endl;
221
0
        }
222
223
0
        ai_real o;
224
0
        if(AI_SUCCESS == mat->Get(AI_MATKEY_OPACITY,o)) {
225
0
            mOutputMat << "d " << o << endl;
226
0
        }
227
0
        if(AI_SUCCESS == mat->Get(AI_MATKEY_REFRACTI,o)) {
228
0
            mOutputMat << "Ni " << o << endl;
229
0
        }
230
231
0
        if(AI_SUCCESS == mat->Get(AI_MATKEY_SHININESS,o) && o) {
232
0
            mOutputMat << "Ns " << o << endl;
233
0
            illum = 2;
234
0
        }
235
236
0
        mOutputMat << "illum " << illum << endl;
237
238
0
        aiString s;
239
0
        if(AI_SUCCESS == mat->Get(AI_MATKEY_TEXTURE_DIFFUSE(0),s)) {
240
0
            mOutputMat << "map_Kd " << s.data << endl;
241
0
        }
242
0
        if(AI_SUCCESS == mat->Get(AI_MATKEY_TEXTURE_AMBIENT(0),s)) {
243
0
            mOutputMat << "map_Ka " << s.data << endl;
244
0
        }
245
0
        if(AI_SUCCESS == mat->Get(AI_MATKEY_TEXTURE_SPECULAR(0),s)) {
246
0
            mOutputMat << "map_Ks " << s.data << endl;
247
0
        }
248
0
        if(AI_SUCCESS == mat->Get(AI_MATKEY_TEXTURE_SHININESS(0),s)) {
249
0
            mOutputMat << "map_Ns " << s.data << endl;
250
0
        }
251
0
        if(AI_SUCCESS == mat->Get(AI_MATKEY_TEXTURE_OPACITY(0),s)) {
252
0
            mOutputMat << "map_d " << s.data << endl;
253
0
        }
254
0
        if(AI_SUCCESS == mat->Get(AI_MATKEY_TEXTURE_HEIGHT(0),s) || AI_SUCCESS == mat->Get(AI_MATKEY_TEXTURE_NORMALS(0),s)) {
255
            // implementations seem to vary here, so write both variants
256
0
            mOutputMat << "bump " << s.data << endl;
257
0
            mOutputMat << "map_bump " << s.data << endl;
258
0
        }
259
260
0
        mOutputMat << endl;
261
0
    }
262
0
}
263
264
0
void ObjExporter::WriteGeometryFile(bool noMtl, bool merge_identical_vertices) {
265
0
    WriteHeader(mOutput);
266
0
    if (!noMtl)
267
0
        mOutput << "mtllib "  << GetMaterialLibName() << endl << endl;
268
269
    // collect mesh geometry
270
0
    aiMatrix4x4 mBase;
271
0
    AddNode(pScene->mRootNode, mBase, merge_identical_vertices);
272
273
    // write vertex positions with colors, if any
274
0
    mVpMap.getKeys( vp );
275
0
    if ( !useVc ) {
276
0
        mOutput << "# " << vp.size() << " vertex positions" << endl;
277
0
        for ( const vertexData& v : vp ) {
278
0
            mOutput << "v " << v.vp.x << " " << v.vp.y << " " << v.vp.z << endl;
279
0
        }
280
0
    } else {
281
0
        mOutput << "# " << vp.size() << " vertex positions and colors" << endl;
282
0
        for ( const vertexData& v : vp ) {
283
0
            mOutput << "v " << v.vp.x << " " << v.vp.y << " " << v.vp.z << " " << v.vc.r << " " << v.vc.g << " " << v.vc.b << endl;
284
0
        }
285
0
    }
286
0
    mOutput << endl;
287
288
    // write uv coordinates
289
0
    mVtMap.getKeys(vt);
290
0
    mOutput << "# " << vt.size() << " UV coordinates" << endl;
291
0
    for(const aiVector3D& v : vt) {
292
0
        mOutput << "vt " << v.x << " " << v.y << " " << v.z << endl;
293
0
    }
294
0
    mOutput << endl;
295
296
    // write vertex normals
297
0
    mVnMap.getKeys(vn);
298
0
    mOutput << "# " << vn.size() << " vertex normals" << endl;
299
0
    for(const aiVector3D& v : vn) {
300
0
        mOutput << "vn " << v.x << " " << v.y << " " << v.z << endl;
301
0
    }
302
0
    mOutput << endl;
303
304
    // now write all mesh instances
305
0
    for(const MeshInstance& m : mMeshes) {
306
0
        mOutput << "# Mesh \'" << m.name << "\' with " << m.faces.size() << " faces" << endl;
307
0
        if (!m.name.empty()) {
308
0
            mOutput << "g " << m.name << endl;
309
0
        }
310
0
        if ( !noMtl ) {
311
0
            mOutput << "usemtl " << m.matname << endl;
312
0
        }
313
314
0
        for(const Face& f : m.faces) {
315
0
            mOutput << f.kind << ' ';
316
0
            for(const FaceVertex& fv : f.indices) {
317
0
                mOutput << ' ' << fv.vp;
318
319
0
                if (f.kind != 'p') {
320
0
                    if (fv.vt || f.kind == 'f') {
321
0
                        mOutput << '/';
322
0
                    }
323
0
                    if (fv.vt) {
324
0
                        mOutput << fv.vt;
325
0
                    }
326
0
                    if (f.kind == 'f' && fv.vn) {
327
0
                        mOutput << '/' << fv.vn;
328
0
                    }
329
0
                }
330
0
            }
331
332
0
            mOutput << endl;
333
0
        }
334
0
        mOutput << endl;
335
0
    }
336
0
}
337
338
// ------------------------------------------------------------------------------------------------
339
0
void ObjExporter::AddMesh(const aiString& name, const aiMesh* m, const aiMatrix4x4& mat, bool merge_identical_vertices) {
340
0
    mMeshes.emplace_back();
341
0
    MeshInstance& mesh = mMeshes.back();
342
343
0
    if ( nullptr != m->mColors[ 0 ] ) {
344
0
        useVc = true;
345
0
    }
346
347
0
    mesh.name = std::string( name.data, name.length );
348
0
    mesh.matname = GetMaterialName(m->mMaterialIndex);
349
350
0
    mesh.faces.resize(m->mNumFaces);
351
352
0
    for(unsigned int i = 0; i < m->mNumFaces; ++i) {
353
0
        const aiFace& f = m->mFaces[i];
354
355
0
        Face& face = mesh.faces[i];
356
0
        switch (f.mNumIndices) {
357
0
            case 1:
358
0
                face.kind = 'p';
359
0
                break;
360
0
            case 2:
361
0
                face.kind = 'l';
362
0
                break;
363
0
            default:
364
0
                face.kind = 'f';
365
0
        }
366
0
        face.indices.resize(f.mNumIndices);
367
368
0
        for(unsigned int a = 0; a < f.mNumIndices; ++a) {
369
0
            const unsigned int idx = f.mIndices[a];
370
371
0
            const unsigned int fi = merge_identical_vertices ? 0 : idx;
372
0
            aiVector3D vert = mat * m->mVertices[idx];
373
374
0
            if ( nullptr != m->mColors[ 0 ] ) {
375
0
                aiColor4D col4 = m->mColors[ 0 ][ idx ];
376
0
                face.indices[a].vp = mVpMap.getIndex({vert, aiColor3D(col4.r, col4.g, col4.b), fi});
377
0
            } else {
378
0
                face.indices[a].vp = mVpMap.getIndex({vert, aiColor3D(0,0,0), fi});
379
0
            }
380
381
0
            if (m->mNormals) {
382
0
                aiVector3D norm = aiMatrix3x3(mat) * m->mNormals[idx];
383
0
                face.indices[a].vn = mVnMap.getIndex(norm);
384
0
            } else {
385
0
                face.indices[a].vn = 0;
386
0
            }
387
388
0
            if ( m->mTextureCoords[ 0 ] ) {
389
0
                face.indices[a].vt = mVtMap.getIndex(m->mTextureCoords[0][idx]);
390
0
            } else {
391
0
                face.indices[a].vt = 0;
392
0
            }
393
0
        }
394
0
    }
395
0
}
396
397
// ------------------------------------------------------------------------------------------------
398
0
void ObjExporter::AddNode(const aiNode* nd, const aiMatrix4x4& mParent, bool merge_identical_vertices) {
399
0
    const aiMatrix4x4& mAbs = mParent * nd->mTransformation;
400
401
0
    aiMesh *cm( nullptr );
402
0
    for(unsigned int i = 0; i < nd->mNumMeshes; ++i) {
403
0
        cm = pScene->mMeshes[nd->mMeshes[i]];
404
0
        if (nullptr != cm) {
405
0
            AddMesh(cm->mName, pScene->mMeshes[nd->mMeshes[i]], mAbs, merge_identical_vertices);
406
0
        } else {
407
0
            AddMesh(nd->mName, pScene->mMeshes[nd->mMeshes[i]], mAbs, merge_identical_vertices);
408
0
        }
409
0
    }
410
411
0
    for(unsigned int i = 0; i < nd->mNumChildren; ++i) {
412
0
        AddNode(nd->mChildren[i], mAbs, merge_identical_vertices);
413
0
    }
414
0
}
415
416
// ------------------------------------------------------------------------------------------------
417
418
#endif // ASSIMP_BUILD_NO_OBJ_EXPORTER
419
#endif // ASSIMP_BUILD_NO_EXPORT