/src/assimp/code/AssetLib/Obj/ObjExporter.cpp
Line | Count | Source |
1 | | /* |
2 | | Open Asset Import Library (assimp) |
3 | | ---------------------------------------------------------------------- |
4 | | |
5 | | Copyright (c) 2006-2026, assimp team |
6 | | |
7 | | All rights reserved. |
8 | | |
9 | | Redistribution and use of this software in source and binary forms, |
10 | | with or without modification, are permitted provided that the |
11 | | following conditions are met: |
12 | | |
13 | | * Redistributions of source code must retain the above |
14 | | copyright notice, this list of conditions and the |
15 | | following disclaimer. |
16 | | |
17 | | * Redistributions in binary form must reproduce the above |
18 | | copyright notice, this list of conditions and the |
19 | | following disclaimer in the documentation and/or other |
20 | | materials provided with the distribution. |
21 | | |
22 | | * Neither the name of the assimp team, nor the names of its |
23 | | contributors may be used to endorse or promote products |
24 | | derived from this software without specific prior |
25 | | written permission of the assimp team. |
26 | | |
27 | | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
28 | | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
29 | | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
30 | | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
31 | | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
32 | | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
33 | | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
34 | | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
35 | | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
36 | | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
37 | | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
38 | | |
39 | | ---------------------------------------------------------------------- |
40 | | */ |
41 | | |
42 | | #ifndef ASSIMP_BUILD_NO_EXPORT |
43 | | #ifndef ASSIMP_BUILD_NO_OBJ_EXPORTER |
44 | | |
45 | | #include "ObjExporter.h" |
46 | | #include <assimp/Exceptional.h> |
47 | | #include <assimp/StringComparison.h> |
48 | | #include <assimp/version.h> |
49 | | #include <assimp/IOSystem.hpp> |
50 | | #include <assimp/Exporter.hpp> |
51 | | #include <assimp/material.h> |
52 | | #include <assimp/scene.h> |
53 | | #include <memory> |
54 | | |
55 | | using namespace Assimp; |
56 | | |
57 | | namespace Assimp { |
58 | | |
59 | | // ------------------------------------------------------------------------------------------------ |
60 | | // Worker function for exporting a scene to Wavefront OBJ. Prototyped and registered in Exporter.cpp |
61 | 1.81k | void ExportSceneObj(const char* pFile,IOSystem* pIOSystem, const aiScene* pScene, const ExportProperties* props) { |
62 | | // invoke the exporter |
63 | 1.81k | ObjExporter exporter(pFile, pScene, false, props); |
64 | | |
65 | 1.81k | if (exporter.mOutput.fail() || exporter.mOutputMat.fail()) { |
66 | 0 | throw DeadlyExportError("output data creation failed. Most likely the file became too large: " + std::string(pFile)); |
67 | 0 | } |
68 | | |
69 | | // we're still here - export successfully completed. Write both the main OBJ file and the material script |
70 | 1.81k | { |
71 | 1.81k | std::unique_ptr<IOStream> outfile (pIOSystem->Open(pFile,"wt")); |
72 | 1.81k | if (outfile == nullptr) { |
73 | 0 | throw DeadlyExportError("could not open output .obj file: " + std::string(pFile)); |
74 | 0 | } |
75 | 1.81k | outfile->Write( exporter.mOutput.str().c_str(), static_cast<size_t>(exporter.mOutput.tellp()),1); |
76 | 1.81k | } |
77 | 0 | { |
78 | 1.81k | std::unique_ptr<IOStream> outfile (pIOSystem->Open(exporter.GetMaterialLibFileName(),"wt")); |
79 | 1.81k | if (outfile == nullptr) { |
80 | 0 | throw DeadlyExportError("could not open output .mtl file: " + std::string(exporter.GetMaterialLibFileName())); |
81 | 0 | } |
82 | 1.81k | outfile->Write( exporter.mOutputMat.str().c_str(), static_cast<size_t>(exporter.mOutputMat.tellp()),1); |
83 | 1.81k | } |
84 | 1.81k | } |
85 | | |
86 | | // ------------------------------------------------------------------------------------------------ |
87 | | // Worker function for exporting a scene to Wavefront OBJ without the material file. Prototyped and registered in Exporter.cpp |
88 | 1.81k | void ExportSceneObjNoMtl(const char* pFile,IOSystem* pIOSystem, const aiScene* pScene, const ExportProperties* props) { |
89 | | // invoke the exporter |
90 | 1.81k | ObjExporter exporter(pFile, pScene, true, props); |
91 | | |
92 | 1.81k | if (exporter.mOutput.fail() || exporter.mOutputMat.fail()) { |
93 | 0 | throw DeadlyExportError("output data creation failed. Most likely the file became too large: " + std::string(pFile)); |
94 | 0 | } |
95 | | |
96 | | // we're still here - export successfully completed. Write both the main OBJ file and the material script |
97 | 1.81k | { |
98 | 1.81k | std::unique_ptr<IOStream> outfile (pIOSystem->Open(pFile,"wt")); |
99 | 1.81k | if (outfile == nullptr) { |
100 | 0 | throw DeadlyExportError("could not open output .obj file: " + std::string(pFile)); |
101 | 0 | } |
102 | 1.81k | outfile->Write( exporter.mOutput.str().c_str(), static_cast<size_t>(exporter.mOutput.tellp()),1); |
103 | 1.81k | } |
104 | | |
105 | | |
106 | 1.81k | } |
107 | | |
108 | | } // end of namespace Assimp |
109 | | |
110 | | static const std::string MaterialExt = ".mtl"; |
111 | | |
112 | | // ------------------------------------------------------------------------------------------------ |
113 | | ObjExporter::ObjExporter(const char* _filename, const aiScene* pScene, bool noMtl, const ExportProperties* props) |
114 | 3.63k | : filename(_filename) |
115 | 3.63k | , pScene(pScene) |
116 | 3.63k | , vn() |
117 | 3.63k | , vt() |
118 | 3.63k | , vp() |
119 | 3.63k | , useVc(false) |
120 | 3.63k | , mVnMap() |
121 | 3.63k | , mVtMap() |
122 | 3.63k | , mVpMap() |
123 | 3.63k | , mMeshes() |
124 | 3.63k | , endl("\n") { |
125 | | // make sure that all formatting happens using the standard, C locale and not the user's current locale |
126 | 3.63k | const std::locale& l = std::locale("C"); |
127 | 3.63k | mOutput.imbue(l); |
128 | 3.63k | mOutput.precision(ASSIMP_AI_REAL_TEXT_PRECISION); |
129 | 3.63k | mOutputMat.imbue(l); |
130 | 3.63k | mOutputMat.precision(ASSIMP_AI_REAL_TEXT_PRECISION); |
131 | | |
132 | 3.63k | WriteGeometryFile( |
133 | 3.63k | noMtl, |
134 | 3.63k | props == nullptr ? true : props->GetPropertyBool("bJoinIdenticalVertices", true) |
135 | 3.63k | ); |
136 | 3.63k | if ( !noMtl ) { |
137 | 1.81k | WriteMaterialFile(); |
138 | 1.81k | } |
139 | 3.63k | } |
140 | | |
141 | | // ------------------------------------------------------------------------------------------------ |
142 | 3.63k | ObjExporter::~ObjExporter() = default; |
143 | | |
144 | | // ------------------------------------------------------------------------------------------------ |
145 | 1.81k | std::string ObjExporter::GetMaterialLibName() { |
146 | | // within the Obj file, we use just the relative file name with the path stripped |
147 | 1.81k | const std::string& s = GetMaterialLibFileName(); |
148 | 1.81k | std::string::size_type il = s.find_last_of("/\\"); |
149 | 1.81k | if (il != std::string::npos) { |
150 | 0 | return s.substr(il + 1); |
151 | 0 | } |
152 | | |
153 | 1.81k | return s; |
154 | 1.81k | } |
155 | | |
156 | | // ------------------------------------------------------------------------------------------------ |
157 | 3.63k | std::string ObjExporter::GetMaterialLibFileName() { |
158 | | // Remove existing .obj file extension so that the final material file name will be fileName.mtl and not fileName.obj.mtl |
159 | 3.63k | size_t lastdot = filename.find_last_of('.'); |
160 | 3.63k | if ( lastdot != std::string::npos ) { |
161 | 0 | return filename.substr( 0, lastdot ) + MaterialExt; |
162 | 0 | } |
163 | | |
164 | 3.63k | return filename + MaterialExt; |
165 | 3.63k | } |
166 | | |
167 | | // ------------------------------------------------------------------------------------------------ |
168 | 5.45k | void ObjExporter::WriteHeader(std::ostringstream& out) { |
169 | 5.45k | out << "# File produced by Open Asset Import Library (https://www.assimp.org)" << endl; |
170 | 5.45k | out << "# (assimp v" << aiGetVersionMajor() << '.' << aiGetVersionMinor() << '.' |
171 | 5.45k | << aiGetVersionRevision() << ")" << endl << endl; |
172 | 5.45k | } |
173 | | |
174 | | // ------------------------------------------------------------------------------------------------ |
175 | 43.4k | std::string ObjExporter::GetMaterialName(unsigned int index) { |
176 | 43.4k | static const std::string EmptyStr; |
177 | 43.4k | if ( nullptr == pScene->mMaterials ) { |
178 | 0 | return EmptyStr; |
179 | 0 | } |
180 | 43.4k | const aiMaterial* const mat = pScene->mMaterials[index]; |
181 | 43.4k | if ( nullptr == mat ) { |
182 | 0 | return EmptyStr; |
183 | 0 | } |
184 | | |
185 | 43.4k | aiString s; |
186 | 43.4k | if(AI_SUCCESS == mat->Get(AI_MATKEY_NAME,s)) { |
187 | 41.0k | return std::string(s.data,s.length); |
188 | 41.0k | } |
189 | | |
190 | 2.33k | char number[ sizeof(unsigned int) * 3 + 1 ]; |
191 | 2.33k | ASSIMP_itoa10(number,index); |
192 | 2.33k | return "$Material_" + std::string(number); |
193 | 43.4k | } |
194 | | |
195 | | // ------------------------------------------------------------------------------------------------ |
196 | 1.81k | void ObjExporter::WriteMaterialFile() { |
197 | 1.81k | WriteHeader(mOutputMat); |
198 | | |
199 | 3.82k | for(unsigned int i = 0; i < pScene->mNumMaterials; ++i) { |
200 | 2.00k | const aiMaterial* const mat = pScene->mMaterials[i]; |
201 | | |
202 | 2.00k | int illum = 1; |
203 | 2.00k | mOutputMat << "newmtl " << GetMaterialName(i) << endl; |
204 | | |
205 | 2.00k | aiColor4D c; |
206 | 2.00k | if(AI_SUCCESS == mat->Get(AI_MATKEY_COLOR_DIFFUSE,c)) { |
207 | 1.80k | mOutputMat << "Kd " << c.r << " " << c.g << " " << c.b << endl; |
208 | 1.80k | } |
209 | 2.00k | if(AI_SUCCESS == mat->Get(AI_MATKEY_COLOR_AMBIENT,c)) { |
210 | 1.39k | mOutputMat << "Ka " << c.r << " " << c.g << " " << c.b << endl; |
211 | 1.39k | } |
212 | 2.00k | if(AI_SUCCESS == mat->Get(AI_MATKEY_COLOR_SPECULAR,c)) { |
213 | 1.58k | mOutputMat << "Ks " << c.r << " " << c.g << " " << c.b << endl; |
214 | 1.58k | } |
215 | 2.00k | if(AI_SUCCESS == mat->Get(AI_MATKEY_COLOR_EMISSIVE,c)) { |
216 | 1.44k | mOutputMat << "Ke " << c.r << " " << c.g << " " << c.b << endl; |
217 | 1.44k | } |
218 | 2.00k | if(AI_SUCCESS == mat->Get(AI_MATKEY_COLOR_TRANSPARENT,c)) { |
219 | 685 | mOutputMat << "Tf " << c.r << " " << c.g << " " << c.b << endl; |
220 | 685 | } |
221 | | |
222 | 2.00k | ai_real o; |
223 | 2.00k | if(AI_SUCCESS == mat->Get(AI_MATKEY_OPACITY,o)) { |
224 | 1.21k | mOutputMat << "d " << o << endl; |
225 | 1.21k | } |
226 | 2.00k | if(AI_SUCCESS == mat->Get(AI_MATKEY_REFRACTI,o)) { |
227 | 885 | mOutputMat << "Ni " << o << endl; |
228 | 885 | } |
229 | | |
230 | 2.00k | if(AI_SUCCESS == mat->Get(AI_MATKEY_SHININESS,o) && o) { |
231 | 472 | mOutputMat << "Ns " << o << endl; |
232 | 472 | illum = 2; |
233 | 472 | } |
234 | | |
235 | 2.00k | mOutputMat << "illum " << illum << endl; |
236 | | |
237 | 2.00k | aiString s; |
238 | 2.00k | if(AI_SUCCESS == mat->Get(AI_MATKEY_TEXTURE_DIFFUSE(0),s)) { |
239 | 584 | mOutputMat << "map_Kd " << s.data << endl; |
240 | 584 | } |
241 | 2.00k | if(AI_SUCCESS == mat->Get(AI_MATKEY_TEXTURE_AMBIENT(0),s)) { |
242 | 0 | mOutputMat << "map_Ka " << s.data << endl; |
243 | 0 | } |
244 | 2.00k | if(AI_SUCCESS == mat->Get(AI_MATKEY_TEXTURE_SPECULAR(0),s)) { |
245 | 2 | mOutputMat << "map_Ks " << s.data << endl; |
246 | 2 | } |
247 | 2.00k | if(AI_SUCCESS == mat->Get(AI_MATKEY_TEXTURE_SHININESS(0),s)) { |
248 | 7 | mOutputMat << "map_Ns " << s.data << endl; |
249 | 7 | } |
250 | 2.00k | if(AI_SUCCESS == mat->Get(AI_MATKEY_TEXTURE_OPACITY(0),s)) { |
251 | 82 | mOutputMat << "map_d " << s.data << endl; |
252 | 82 | } |
253 | 2.00k | if(AI_SUCCESS == mat->Get(AI_MATKEY_TEXTURE_HEIGHT(0),s) || AI_SUCCESS == mat->Get(AI_MATKEY_TEXTURE_NORMALS(0),s)) { |
254 | | // implementations seem to vary here, so write both variants |
255 | 60 | mOutputMat << "bump " << s.data << endl; |
256 | 60 | mOutputMat << "map_bump " << s.data << endl; |
257 | 60 | } |
258 | | |
259 | 2.00k | mOutputMat << endl; |
260 | 2.00k | } |
261 | 1.81k | } |
262 | | |
263 | 3.63k | void ObjExporter::WriteGeometryFile(bool noMtl, bool merge_identical_vertices) { |
264 | 3.63k | WriteHeader(mOutput); |
265 | 3.63k | if (!noMtl) |
266 | 1.81k | mOutput << "mtllib " << GetMaterialLibName() << endl << endl; |
267 | | |
268 | | // collect mesh geometry |
269 | 3.63k | aiMatrix4x4 mBase; |
270 | 3.63k | AddNode(pScene->mRootNode, mBase, merge_identical_vertices); |
271 | | |
272 | | // write vertex positions with colors, if any |
273 | 3.63k | mVpMap.getKeys( vp ); |
274 | 3.63k | if ( !useVc ) { |
275 | 2.97k | mOutput << "# " << vp.size() << " vertex positions" << endl; |
276 | 664k | for ( const vertexData& v : vp ) { |
277 | 664k | mOutput << "v " << v.vp.x << " " << v.vp.y << " " << v.vp.z << endl; |
278 | 664k | } |
279 | 2.97k | } else { |
280 | 666 | mOutput << "# " << vp.size() << " vertex positions and colors" << endl; |
281 | 45.1k | for ( const vertexData& v : vp ) { |
282 | 45.1k | mOutput << "v " << v.vp.x << " " << v.vp.y << " " << v.vp.z << " " << v.vc.r << " " << v.vc.g << " " << v.vc.b << endl; |
283 | 45.1k | } |
284 | 666 | } |
285 | 3.63k | mOutput << endl; |
286 | | |
287 | | // write uv coordinates |
288 | 3.63k | mVtMap.getKeys(vt); |
289 | 3.63k | mOutput << "# " << vt.size() << " UV coordinates" << endl; |
290 | 94.6k | for(const aiVector3D& v : vt) { |
291 | 94.6k | mOutput << "vt " << v.x << " " << v.y << " " << v.z << endl; |
292 | 94.6k | } |
293 | 3.63k | mOutput << endl; |
294 | | |
295 | | // write vertex normals |
296 | 3.63k | mVnMap.getKeys(vn); |
297 | 3.63k | mOutput << "# " << vn.size() << " vertex normals" << endl; |
298 | 289k | for(const aiVector3D& v : vn) { |
299 | 289k | mOutput << "vn " << v.x << " " << v.y << " " << v.z << endl; |
300 | 289k | } |
301 | 3.63k | mOutput << endl; |
302 | | |
303 | | // now write all mesh instances |
304 | 41.4k | for(const MeshInstance& m : mMeshes) { |
305 | 41.4k | mOutput << "# Mesh \'" << m.name << "\' with " << m.faces.size() << " faces" << endl; |
306 | 41.4k | if (!m.name.empty()) { |
307 | 36.9k | mOutput << "g " << m.name << endl; |
308 | 36.9k | } |
309 | 41.4k | if ( !noMtl ) { |
310 | 20.7k | mOutput << "usemtl " << m.matname << endl; |
311 | 20.7k | } |
312 | | |
313 | 1.06M | for(const Face& f : m.faces) { |
314 | 1.06M | mOutput << f.kind << ' '; |
315 | 2.89M | for(const FaceVertex& fv : f.indices) { |
316 | 2.89M | mOutput << ' ' << fv.vp; |
317 | | |
318 | 2.89M | if (f.kind != 'p') { |
319 | 2.81M | if (fv.vt || f.kind == 'f') { |
320 | 2.53M | mOutput << '/'; |
321 | 2.53M | } |
322 | 2.81M | if (fv.vt) { |
323 | 858k | mOutput << fv.vt; |
324 | 858k | } |
325 | 2.81M | if (f.kind == 'f' && fv.vn) { |
326 | 2.52M | mOutput << '/' << fv.vn; |
327 | 2.52M | } |
328 | 2.81M | } |
329 | 2.89M | } |
330 | | |
331 | 1.06M | mOutput << endl; |
332 | 1.06M | } |
333 | 41.4k | mOutput << endl; |
334 | 41.4k | } |
335 | 3.63k | } |
336 | | |
337 | | // ------------------------------------------------------------------------------------------------ |
338 | 41.4k | void ObjExporter::AddMesh(const aiString& name, const aiMesh* m, const aiMatrix4x4& mat, bool merge_identical_vertices) { |
339 | 41.4k | mMeshes.emplace_back(); |
340 | 41.4k | MeshInstance& mesh = mMeshes.back(); |
341 | | |
342 | 41.4k | if ( nullptr != m->mColors[ 0 ] ) { |
343 | 10.7k | useVc = true; |
344 | 10.7k | } |
345 | | |
346 | 41.4k | mesh.name = std::string( name.data, name.length ); |
347 | 41.4k | mesh.matname = GetMaterialName(m->mMaterialIndex); |
348 | | |
349 | 41.4k | mesh.faces.resize(m->mNumFaces); |
350 | | |
351 | 1.11M | for(unsigned int i = 0; i < m->mNumFaces; ++i) { |
352 | 1.06M | const aiFace& f = m->mFaces[i]; |
353 | | |
354 | 1.06M | Face& face = mesh.faces[i]; |
355 | 1.06M | switch (f.mNumIndices) { |
356 | 82.1k | case 1: |
357 | 82.1k | face.kind = 'p'; |
358 | 82.1k | break; |
359 | 145k | case 2: |
360 | 145k | face.kind = 'l'; |
361 | 145k | break; |
362 | 841k | default: |
363 | 841k | face.kind = 'f'; |
364 | 1.06M | } |
365 | 1.06M | face.indices.resize(f.mNumIndices); |
366 | | |
367 | 3.96M | for(unsigned int a = 0; a < f.mNumIndices; ++a) { |
368 | 2.89M | const unsigned int idx = f.mIndices[a]; |
369 | | |
370 | 2.89M | const unsigned int fi = merge_identical_vertices ? 0 : idx; |
371 | 2.89M | aiVector3D vert = mat * m->mVertices[idx]; |
372 | | |
373 | 2.89M | if ( nullptr != m->mColors[ 0 ] ) { |
374 | 245k | aiColor4D col4 = m->mColors[ 0 ][ idx ]; |
375 | 245k | face.indices[a].vp = mVpMap.getIndex({vert, aiColor3D(col4.r, col4.g, col4.b), fi}); |
376 | 2.65M | } else { |
377 | 2.65M | face.indices[a].vp = mVpMap.getIndex({vert, aiColor3D(0,0,0), fi}); |
378 | 2.65M | } |
379 | | |
380 | 2.89M | if (m->mNormals) { |
381 | 2.86M | aiVector3D norm = aiMatrix3x3(mat) * m->mNormals[idx]; |
382 | 2.86M | face.indices[a].vn = mVnMap.getIndex(norm); |
383 | 2.86M | } else { |
384 | 37.0k | face.indices[a].vn = 0; |
385 | 37.0k | } |
386 | | |
387 | 2.89M | if ( m->mTextureCoords[ 0 ] ) { |
388 | 864k | face.indices[a].vt = mVtMap.getIndex(m->mTextureCoords[0][idx]); |
389 | 2.03M | } else { |
390 | 2.03M | face.indices[a].vt = 0; |
391 | 2.03M | } |
392 | 2.89M | } |
393 | 1.06M | } |
394 | 41.4k | } |
395 | | |
396 | | // ------------------------------------------------------------------------------------------------ |
397 | 198k | void ObjExporter::AddNode(const aiNode* nd, const aiMatrix4x4& mParent, bool merge_identical_vertices) { |
398 | 198k | if (nd == nullptr) { |
399 | 0 | return; |
400 | 0 | } |
401 | | |
402 | 198k | const aiMatrix4x4& mAbs = mParent * nd->mTransformation; |
403 | 198k | aiMesh *cm{nullptr}; |
404 | 239k | for(unsigned int i = 0; i < nd->mNumMeshes; ++i) { |
405 | 41.4k | cm = pScene->mMeshes[nd->mMeshes[i]]; |
406 | 41.4k | if (nullptr != cm) { |
407 | 41.4k | AddMesh(cm->mName, pScene->mMeshes[nd->mMeshes[i]], mAbs, merge_identical_vertices); |
408 | 41.4k | } else { |
409 | 0 | AddMesh(nd->mName, pScene->mMeshes[nd->mMeshes[i]], mAbs, merge_identical_vertices); |
410 | 0 | } |
411 | 41.4k | } |
412 | | |
413 | 393k | for(unsigned int i = 0; i < nd->mNumChildren; ++i) { |
414 | 194k | AddNode(nd->mChildren[i], mAbs, merge_identical_vertices); |
415 | 194k | } |
416 | 198k | } |
417 | | |
418 | | // ------------------------------------------------------------------------------------------------ |
419 | | |
420 | | #endif // ASSIMP_BUILD_NO_OBJ_EXPORTER |
421 | | #endif // ASSIMP_BUILD_NO_EXPORT |