1 // Copyright (C) 2002-2012 Nikolaus Gebhardt
\r
2 // This file is part of the "Irrlicht Engine".
\r
3 // For conditions of distribution and use, see copyright notice in irrlicht.h
\r
5 #include "IrrCompileConfig.h"
\r
6 #ifdef _IRR_COMPILE_WITH_OBJ_LOADER_
\r
8 #include "COBJMeshFileLoader.h"
\r
9 #include "IMeshManipulator.h"
\r
10 #include "IVideoDriver.h"
\r
12 #include "SMeshBuffer.h"
\r
13 #include "SAnimatedMesh.h"
\r
14 #include "IReadFile.h"
\r
15 #include "IAttributes.h"
\r
16 #include "fast_atof.h"
\r
17 #include "coreutil.h"
\r
26 #define _IRR_DEBUG_OBJ_LOADER_
\r
30 COBJMeshFileLoader::COBJMeshFileLoader(scene::ISceneManager* smgr, io::IFileSystem* fs)
\r
31 : SceneManager(smgr), FileSystem(fs)
\r
34 setDebugName("COBJMeshFileLoader");
\r
43 COBJMeshFileLoader::~COBJMeshFileLoader()
\r
50 //! returns true if the file maybe is able to be loaded by this class
\r
51 //! based on the file extension (e.g. ".bsp")
\r
52 bool COBJMeshFileLoader::isALoadableFileExtension(const io::path& filename) const
\r
54 return core::hasFileExtension ( filename, "obj" );
\r
58 //! creates/loads an animated mesh from the file.
\r
59 //! \return Pointer to the created mesh. Returns 0 if loading failed.
\r
60 //! If you no longer need the mesh, you should call IAnimatedMesh::drop().
\r
61 //! See IReferenceCounted::drop() for more information.
\r
62 IAnimatedMesh* COBJMeshFileLoader::createMesh(io::IReadFile* file)
\r
67 const long filesize = file->getSize();
\r
71 const u32 WORD_BUFFER_LENGTH = 512;
\r
73 core::array<core::vector3df> vertexBuffer(1000);
\r
74 core::array<core::vector3df> normalsBuffer(1000);
\r
75 core::array<core::vector2df> textureCoordBuffer(1000);
\r
77 SObjMtl * currMtl = new SObjMtl();
\r
78 Materials.push_back(currMtl);
\r
79 u32 smoothingGroup=0;
\r
81 const io::path fullName = file->getFileName();
\r
82 const io::path relPath = FileSystem->getFileDir(fullName)+"/";
\r
84 c8* buf = new c8[filesize];
\r
85 memset(buf, 0, filesize);
\r
86 file->read((void*)buf, filesize);
\r
87 const c8* const bufEnd = buf+filesize;
\r
89 // Process obj information
\r
90 const c8* bufPtr = buf;
\r
91 core::stringc grpName, mtlName;
\r
92 bool mtlChanged=false;
\r
93 bool useGroups = !SceneManager->getParameters()->getAttributeAsBool(OBJ_LOADER_IGNORE_GROUPS);
\r
94 bool useMaterials = !SceneManager->getParameters()->getAttributeAsBool(OBJ_LOADER_IGNORE_MATERIAL_FILES);
\r
95 irr::u32 lineNr = 1; // only counts non-empty lines, still useful in debugging to locate errors
\r
96 core::array<int> faceCorners;
\r
97 faceCorners.reallocate(32); // should be large enough
\r
98 const core::stringc TAG_OFF = "off";
\r
99 irr::u32 degeneratedFaces = 0;
\r
101 while(bufPtr != bufEnd)
\r
105 case 'm': // mtllib (material)
\r
109 c8 name[WORD_BUFFER_LENGTH];
\r
110 bufPtr = goAndCopyNextWord(name, bufPtr, WORD_BUFFER_LENGTH, bufEnd);
\r
111 #ifdef _IRR_DEBUG_OBJ_LOADER_
\r
112 os::Printer::log("Reading material file",name);
\r
118 case 'v': // v, vn, vt
\r
121 case ' ': // vertex
\r
123 core::vector3df vec;
\r
124 bufPtr = readVec3(bufPtr, vec, bufEnd);
\r
125 vertexBuffer.push_back(vec);
\r
129 case 'n': // normal
\r
131 core::vector3df vec;
\r
132 bufPtr = readVec3(bufPtr, vec, bufEnd);
\r
133 normalsBuffer.push_back(vec);
\r
137 case 't': // texcoord
\r
139 core::vector2df vec;
\r
140 bufPtr = readUV(bufPtr, vec, bufEnd);
\r
141 textureCoordBuffer.push_back(vec);
\r
147 case 'g': // group name
\r
149 c8 grp[WORD_BUFFER_LENGTH];
\r
150 bufPtr = goAndCopyNextWord(grp, bufPtr, WORD_BUFFER_LENGTH, bufEnd);
\r
151 #ifdef _IRR_DEBUG_OBJ_LOADER_
\r
152 os::Printer::log("Loaded group start",grp, ELL_DEBUG);
\r
159 grpName = "default";
\r
165 case 's': // smoothing can be a group or off (equiv. to 0)
\r
167 c8 smooth[WORD_BUFFER_LENGTH];
\r
168 bufPtr = goAndCopyNextWord(smooth, bufPtr, WORD_BUFFER_LENGTH, bufEnd);
\r
169 #ifdef _IRR_DEBUG_OBJ_LOADER_
\r
170 os::Printer::log("Loaded smoothing group start",smooth, ELL_DEBUG);
\r
172 if (TAG_OFF==smooth)
\r
175 smoothingGroup=core::strtoul10(smooth);
\r
177 (void)smoothingGroup; // disable unused variable warnings
\r
181 case 'u': // usemtl
\r
182 // get name of material
\r
184 c8 matName[WORD_BUFFER_LENGTH];
\r
185 bufPtr = goAndCopyNextWord(matName, bufPtr, WORD_BUFFER_LENGTH, bufEnd);
\r
186 #ifdef _IRR_DEBUG_OBJ_LOADER_
\r
187 os::Printer::log("Loaded material start",matName, ELL_DEBUG);
\r
196 c8 vertexWord[WORD_BUFFER_LENGTH]; // for retrieving vertex data
\r
197 video::S3DVertex v;
\r
198 // Assign vertex color from currently active material's diffuse color
\r
201 // retrieve the material
\r
202 SObjMtl *useMtl = findMtl(mtlName, grpName);
\r
203 // only change material if we found it
\r
209 v.Color = currMtl->Meshbuffer->Material.DiffuseColor;
\r
211 // get all vertices data in this face (current line of obj file)
\r
212 const core::stringc wordBuffer = copyLine(bufPtr, bufEnd);
\r
213 const c8* linePtr = wordBuffer.c_str();
\r
214 const c8* const endPtr = linePtr+wordBuffer.size();
\r
216 faceCorners.set_used(0); // fast clear
\r
218 // read in all vertices
\r
219 linePtr = goNextWord(linePtr, endPtr);
\r
220 while (0 != linePtr[0])
\r
222 // Array to communicate with retrieveVertexIndices()
\r
223 // sends the buffer sizes and gets the actual indices
\r
224 // if index not set returns -1
\r
226 Idx[0] = Idx[1] = Idx[2] = -1;
\r
228 // read in next vertex's data
\r
229 u32 wlength = copyWord(vertexWord, linePtr, WORD_BUFFER_LENGTH, endPtr);
\r
230 // this function will also convert obj's 1-based index to c++'s 0-based index
\r
231 retrieveVertexIndices(vertexWord, Idx, vertexWord+wlength+1, vertexBuffer.size(), textureCoordBuffer.size(), normalsBuffer.size());
\r
232 if ( -1 != Idx[0] && Idx[0] < (irr::s32)vertexBuffer.size() )
\r
233 v.Pos = vertexBuffer[Idx[0]];
\r
236 os::Printer::log("Invalid vertex index in this line:", wordBuffer.c_str(), ELL_ERROR);
\r
240 if ( -1 != Idx[1] && Idx[1] < (irr::s32)textureCoordBuffer.size() )
\r
241 v.TCoords = textureCoordBuffer[Idx[1]];
\r
243 v.TCoords.set(0.0f,0.0f);
\r
244 if ( -1 != Idx[2] && Idx[2] < (irr::s32)normalsBuffer.size() )
\r
245 v.Normal = normalsBuffer[Idx[2]];
\r
248 v.Normal.set(0.0f,0.0f,0.0f);
\r
249 currMtl->RecalculateNormals=true;
\r
253 auto n = currMtl->VertMap.find(v);
\r
254 if (n != currMtl->VertMap.end())
\r
256 vertLocation = n->second;
\r
260 currMtl->Meshbuffer->Vertices.push_back(v);
\r
261 vertLocation = currMtl->Meshbuffer->Vertices.size() -1;
\r
262 currMtl->VertMap.emplace(v, vertLocation);
\r
265 faceCorners.push_back(vertLocation);
\r
267 // go to next vertex
\r
268 linePtr = goNextWord(linePtr, endPtr);
\r
271 // triangulate the face
\r
272 const int c = faceCorners[0];
\r
273 for ( u32 i = 1; i < faceCorners.size() - 1; ++i )
\r
276 const int a = faceCorners[i + 1];
\r
277 const int b = faceCorners[i];
\r
278 if (a != b && a != c && b != c) // ignore degenerated faces. We can get them when we merge vertices above in the VertMap.
\r
280 currMtl->Meshbuffer->Indices.push_back(a);
\r
281 currMtl->Meshbuffer->Indices.push_back(b);
\r
282 currMtl->Meshbuffer->Indices.push_back(c);
\r
286 ++degeneratedFaces;
\r
292 case '#': // comment
\r
295 } // end switch(bufPtr[0])
\r
296 // eat up rest of line
\r
297 bufPtr = goNextLine(bufPtr, bufEnd);
\r
299 } // end while(bufPtr && (bufPtr-buf<filesize))
\r
301 if ( degeneratedFaces > 0 )
\r
303 irr::core::stringc log(degeneratedFaces);
\r
304 log += " degenerated faces removed in ";
\r
305 log += irr::core::stringc(fullName);
\r
306 os::Printer::log(log.c_str(), ELL_INFORMATION);
\r
309 SMesh* mesh = new SMesh();
\r
311 // Combine all the groups (meshbuffers) into the mesh
\r
312 for ( u32 m = 0; m < Materials.size(); ++m )
\r
314 if ( Materials[m]->Meshbuffer->getIndexCount() > 0 )
\r
316 Materials[m]->Meshbuffer->recalculateBoundingBox();
\r
317 if (Materials[m]->RecalculateNormals)
\r
318 SceneManager->getMeshManipulator()->recalculateNormals(Materials[m]->Meshbuffer);
\r
319 mesh->addMeshBuffer( Materials[m]->Meshbuffer );
\r
323 // Create the Animated mesh if there's anything in the mesh
\r
324 SAnimatedMesh* animMesh = 0;
\r
325 if ( 0 != mesh->getMeshBufferCount() )
\r
327 mesh->recalculateBoundingBox();
\r
328 animMesh = new SAnimatedMesh();
\r
329 animMesh->Type = EAMT_OBJ;
\r
330 animMesh->addMesh(mesh);
\r
331 animMesh->recalculateBoundingBox();
\r
334 // Clean up the allocate obj file contents
\r
336 // more cleaning up
\r
344 const c8* COBJMeshFileLoader::readColor(const c8* bufPtr, video::SColor& color, const c8* const bufEnd)
\r
346 const u32 COLOR_BUFFER_LENGTH = 16;
\r
347 c8 colStr[COLOR_BUFFER_LENGTH];
\r
349 bufPtr = goAndCopyNextWord(colStr, bufPtr, COLOR_BUFFER_LENGTH, bufEnd);
\r
350 color.setRed((u32)core::round32(core::fast_atof(colStr)*255.f));
\r
351 bufPtr = goAndCopyNextWord(colStr, bufPtr, COLOR_BUFFER_LENGTH, bufEnd);
\r
352 color.setGreen((u32)core::round32(core::fast_atof(colStr)*255.f));
\r
353 bufPtr = goAndCopyNextWord(colStr, bufPtr, COLOR_BUFFER_LENGTH, bufEnd);
\r
354 color.setBlue((u32)core::round32(core::fast_atof(colStr)*255.f));
\r
359 //! Read 3d vector of floats
\r
360 const c8* COBJMeshFileLoader::readVec3(const c8* bufPtr, core::vector3df& vec, const c8* const bufEnd)
\r
362 const u32 WORD_BUFFER_LENGTH = 256;
\r
363 c8 wordBuffer[WORD_BUFFER_LENGTH];
\r
365 bufPtr = goAndCopyNextWord(wordBuffer, bufPtr, WORD_BUFFER_LENGTH, bufEnd);
\r
366 vec.X=-core::fast_atof(wordBuffer); // change handedness
\r
367 bufPtr = goAndCopyNextWord(wordBuffer, bufPtr, WORD_BUFFER_LENGTH, bufEnd);
\r
368 vec.Y=core::fast_atof(wordBuffer);
\r
369 bufPtr = goAndCopyNextWord(wordBuffer, bufPtr, WORD_BUFFER_LENGTH, bufEnd);
\r
370 vec.Z=core::fast_atof(wordBuffer);
\r
375 //! Read 2d vector of floats
\r
376 const c8* COBJMeshFileLoader::readUV(const c8* bufPtr, core::vector2df& vec, const c8* const bufEnd)
\r
378 const u32 WORD_BUFFER_LENGTH = 256;
\r
379 c8 wordBuffer[WORD_BUFFER_LENGTH];
\r
381 bufPtr = goAndCopyNextWord(wordBuffer, bufPtr, WORD_BUFFER_LENGTH, bufEnd);
\r
382 vec.X=core::fast_atof(wordBuffer);
\r
383 bufPtr = goAndCopyNextWord(wordBuffer, bufPtr, WORD_BUFFER_LENGTH, bufEnd);
\r
384 vec.Y=1-core::fast_atof(wordBuffer); // change handedness
\r
389 //! Read boolean value represented as 'on' or 'off'
\r
390 const c8* COBJMeshFileLoader::readBool(const c8* bufPtr, bool& tf, const c8* const bufEnd)
\r
392 const u32 BUFFER_LENGTH = 8;
\r
393 c8 tfStr[BUFFER_LENGTH];
\r
395 bufPtr = goAndCopyNextWord(tfStr, bufPtr, BUFFER_LENGTH, bufEnd);
\r
396 tf = strcmp(tfStr, "off") != 0;
\r
401 COBJMeshFileLoader::SObjMtl* COBJMeshFileLoader::findMtl(const core::stringc& mtlName, const core::stringc& grpName)
\r
403 COBJMeshFileLoader::SObjMtl* defMaterial = 0;
\r
404 // search existing Materials for best match
\r
405 // exact match does return immediately, only name match means a new group
\r
406 for (u32 i = 0; i < Materials.size(); ++i)
\r
408 if ( Materials[i]->Name == mtlName )
\r
410 if ( Materials[i]->Group == grpName )
\r
411 return Materials[i];
\r
413 defMaterial = Materials[i];
\r
416 // we found a partial match
\r
419 Materials.push_back(new SObjMtl(*defMaterial));
\r
420 Materials.getLast()->Group = grpName;
\r
421 return Materials.getLast();
\r
423 // we found a new group for a non-existant material
\r
424 else if (grpName.size())
\r
426 Materials.push_back(new SObjMtl(*Materials[0]));
\r
427 Materials.getLast()->Group = grpName;
\r
428 return Materials.getLast();
\r
434 //! skip space characters and stop on first non-space
\r
435 const c8* COBJMeshFileLoader::goFirstWord(const c8* buf, const c8* const bufEnd, bool acrossNewlines)
\r
437 // skip space characters
\r
438 if (acrossNewlines)
\r
439 while((buf != bufEnd) && core::isspace(*buf))
\r
442 while((buf != bufEnd) && core::isspace(*buf) && (*buf != '\n'))
\r
449 //! skip current word and stop at beginning of next one
\r
450 const c8* COBJMeshFileLoader::goNextWord(const c8* buf, const c8* const bufEnd, bool acrossNewlines)
\r
452 // skip current word
\r
453 while(( buf != bufEnd ) && !core::isspace(*buf))
\r
456 return goFirstWord(buf, bufEnd, acrossNewlines);
\r
460 //! Read until line break is reached and stop at the next non-space character
\r
461 const c8* COBJMeshFileLoader::goNextLine(const c8* buf, const c8* const bufEnd)
\r
463 // look for newline characters
\r
464 while(buf != bufEnd)
\r
466 // found it, so leave
\r
467 if (*buf=='\n' || *buf=='\r')
\r
471 return goFirstWord(buf, bufEnd);
\r
475 u32 COBJMeshFileLoader::copyWord(c8* outBuf, const c8* const inBuf, u32 outBufLength, const c8* const bufEnd)
\r
488 if (core::isspace(inBuf[i]) || &(inBuf[i]) == bufEnd)
\r
493 u32 length = core::min_(i, outBufLength-1);
\r
494 for (u32 j=0; j<length; ++j)
\r
495 outBuf[j] = inBuf[j];
\r
497 outBuf[length] = 0;
\r
502 core::stringc COBJMeshFileLoader::copyLine(const c8* inBuf, const c8* bufEnd)
\r
505 return core::stringc();
\r
507 const c8* ptr = inBuf;
\r
510 if (*ptr=='\n' || *ptr=='\r')
\r
514 // we must avoid the +1 in case the array is used up
\r
515 return core::stringc(inBuf, (u32)(ptr-inBuf+((ptr < bufEnd) ? 1 : 0)));
\r
519 const c8* COBJMeshFileLoader::goAndCopyNextWord(c8* outBuf, const c8* inBuf, u32 outBufLength, const c8* bufEnd)
\r
521 inBuf = goNextWord(inBuf, bufEnd, false);
\r
522 copyWord(outBuf, inBuf, outBufLength, bufEnd);
\r
527 bool COBJMeshFileLoader::retrieveVertexIndices(c8* vertexData, s32* idx, const c8* bufEnd, u32 vbsize, u32 vtsize, u32 vnsize)
\r
529 const u32 BUFFER_LENGTH = 16;
\r
530 c8 word[BUFFER_LENGTH];
\r
531 const c8* p = goFirstWord(vertexData, bufEnd);
\r
532 u32 idxType = 0; // 0 = posIdx, 1 = texcoordIdx, 2 = normalIdx
\r
535 while ( p != bufEnd )
\r
537 if ( i >= BUFFER_LENGTH )
\r
541 if ( ( core::isdigit(*p)) || (*p == '-') )
\r
543 // build up the number
\r
546 else if ( *p == '/' || *p == ' ' || *p == '\0' )
\r
548 // number is completed. Convert and store it
\r
550 // if no number was found index will become 0 and later on -1 by decrement
\r
551 idx[idxType] = core::strtol10(word);
\r
552 if (idx[idxType]<0)
\r
557 idx[idxType] += vbsize;
\r
560 idx[idxType] += vtsize;
\r
563 idx[idxType] += vnsize;
\r
574 // go to the next kind of index type
\r
577 if ( ++idxType > 2 )
\r
579 // error checking, shouldn't reach here unless file is wrong
\r
585 // set all missing values to disable (=-1)
\r
586 while (++idxType < 3)
\r
593 // go to the next char
\r
601 void COBJMeshFileLoader::cleanUp()
\r
603 for (u32 i=0; i < Materials.size(); ++i )
\r
605 Materials[i]->Meshbuffer->drop();
\r
606 delete Materials[i];
\r
613 } // end namespace scene
\r
614 } // end namespace irr
\r
616 #endif // _IRR_COMPILE_WITH_OBJ_LOADER_
\r