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 #include "COBJMeshFileLoader.h"
\r
7 #include "IMeshManipulator.h"
\r
8 #include "IVideoDriver.h"
\r
10 #include "SMeshBuffer.h"
\r
11 #include "SAnimatedMesh.h"
\r
12 #include "IReadFile.h"
\r
13 #include "IAttributes.h"
\r
14 #include "fast_atof.h"
\r
15 #include "coreutil.h"
\r
24 #define _IRR_DEBUG_OBJ_LOADER_
\r
28 COBJMeshFileLoader::COBJMeshFileLoader(scene::ISceneManager* smgr, io::IFileSystem* fs)
\r
29 : SceneManager(smgr), FileSystem(fs)
\r
32 setDebugName("COBJMeshFileLoader");
\r
41 COBJMeshFileLoader::~COBJMeshFileLoader()
\r
48 //! returns true if the file maybe is able to be loaded by this class
\r
49 //! based on the file extension (e.g. ".bsp")
\r
50 bool COBJMeshFileLoader::isALoadableFileExtension(const io::path& filename) const
\r
52 return core::hasFileExtension ( filename, "obj" );
\r
56 //! creates/loads an animated mesh from the file.
\r
57 //! \return Pointer to the created mesh. Returns 0 if loading failed.
\r
58 //! If you no longer need the mesh, you should call IAnimatedMesh::drop().
\r
59 //! See IReferenceCounted::drop() for more information.
\r
60 IAnimatedMesh* COBJMeshFileLoader::createMesh(io::IReadFile* file)
\r
65 const long filesize = file->getSize();
\r
69 const u32 WORD_BUFFER_LENGTH = 512;
\r
71 core::array<core::vector3df> vertexBuffer(1000);
\r
72 core::array<core::vector3df> normalsBuffer(1000);
\r
73 core::array<core::vector2df> textureCoordBuffer(1000);
\r
75 SObjMtl * currMtl = new SObjMtl();
\r
76 Materials.push_back(currMtl);
\r
77 u32 smoothingGroup=0;
\r
79 const io::path fullName = file->getFileName();
\r
80 const io::path relPath = FileSystem->getFileDir(fullName)+"/";
\r
82 c8* buf = new c8[filesize];
\r
83 memset(buf, 0, filesize);
\r
84 file->read((void*)buf, filesize);
\r
85 const c8* const bufEnd = buf+filesize;
\r
87 // Process obj information
\r
88 const c8* bufPtr = buf;
\r
89 core::stringc grpName, mtlName;
\r
90 bool mtlChanged=false;
\r
91 bool useGroups = !SceneManager->getParameters()->getAttributeAsBool(OBJ_LOADER_IGNORE_GROUPS);
\r
92 bool useMaterials = !SceneManager->getParameters()->getAttributeAsBool(OBJ_LOADER_IGNORE_MATERIAL_FILES);
\r
93 irr::u32 lineNr = 1; // only counts non-empty lines, still useful in debugging to locate errors
\r
94 core::array<int> faceCorners;
\r
95 faceCorners.reallocate(32); // should be large enough
\r
96 const core::stringc TAG_OFF = "off";
\r
97 irr::u32 degeneratedFaces = 0;
\r
99 while(bufPtr != bufEnd)
\r
103 case 'm': // mtllib (material)
\r
107 c8 name[WORD_BUFFER_LENGTH];
\r
108 bufPtr = goAndCopyNextWord(name, bufPtr, WORD_BUFFER_LENGTH, bufEnd);
\r
109 #ifdef _IRR_DEBUG_OBJ_LOADER_
\r
110 os::Printer::log("Reading material file",name);
\r
116 case 'v': // v, vn, vt
\r
119 case ' ': // vertex
\r
121 core::vector3df vec;
\r
122 bufPtr = readVec3(bufPtr, vec, bufEnd);
\r
123 vertexBuffer.push_back(vec);
\r
127 case 'n': // normal
\r
129 core::vector3df vec;
\r
130 bufPtr = readVec3(bufPtr, vec, bufEnd);
\r
131 normalsBuffer.push_back(vec);
\r
135 case 't': // texcoord
\r
137 core::vector2df vec;
\r
138 bufPtr = readUV(bufPtr, vec, bufEnd);
\r
139 textureCoordBuffer.push_back(vec);
\r
145 case 'g': // group name
\r
147 c8 grp[WORD_BUFFER_LENGTH];
\r
148 bufPtr = goAndCopyNextWord(grp, bufPtr, WORD_BUFFER_LENGTH, bufEnd);
\r
149 #ifdef _IRR_DEBUG_OBJ_LOADER_
\r
150 os::Printer::log("Loaded group start",grp, ELL_DEBUG);
\r
157 grpName = "default";
\r
163 case 's': // smoothing can be a group or off (equiv. to 0)
\r
165 c8 smooth[WORD_BUFFER_LENGTH];
\r
166 bufPtr = goAndCopyNextWord(smooth, bufPtr, WORD_BUFFER_LENGTH, bufEnd);
\r
167 #ifdef _IRR_DEBUG_OBJ_LOADER_
\r
168 os::Printer::log("Loaded smoothing group start",smooth, ELL_DEBUG);
\r
170 if (TAG_OFF==smooth)
\r
173 smoothingGroup=core::strtoul10(smooth);
\r
175 (void)smoothingGroup; // disable unused variable warnings
\r
179 case 'u': // usemtl
\r
180 // get name of material
\r
182 c8 matName[WORD_BUFFER_LENGTH];
\r
183 bufPtr = goAndCopyNextWord(matName, bufPtr, WORD_BUFFER_LENGTH, bufEnd);
\r
184 #ifdef _IRR_DEBUG_OBJ_LOADER_
\r
185 os::Printer::log("Loaded material start",matName, ELL_DEBUG);
\r
194 c8 vertexWord[WORD_BUFFER_LENGTH]; // for retrieving vertex data
\r
195 video::S3DVertex v;
\r
196 // Assign vertex color from currently active material's diffuse color
\r
199 // retrieve the material
\r
200 SObjMtl *useMtl = findMtl(mtlName, grpName);
\r
201 // only change material if we found it
\r
207 v.Color = currMtl->Meshbuffer->Material.DiffuseColor;
\r
209 // get all vertices data in this face (current line of obj file)
\r
210 const core::stringc wordBuffer = copyLine(bufPtr, bufEnd);
\r
211 const c8* linePtr = wordBuffer.c_str();
\r
212 const c8* const endPtr = linePtr+wordBuffer.size();
\r
214 faceCorners.set_used(0); // fast clear
\r
216 // read in all vertices
\r
217 linePtr = goNextWord(linePtr, endPtr);
\r
218 while (0 != linePtr[0])
\r
220 // Array to communicate with retrieveVertexIndices()
\r
221 // sends the buffer sizes and gets the actual indices
\r
222 // if index not set returns -1
\r
224 Idx[0] = Idx[1] = Idx[2] = -1;
\r
226 // read in next vertex's data
\r
227 u32 wlength = copyWord(vertexWord, linePtr, WORD_BUFFER_LENGTH, endPtr);
\r
228 // this function will also convert obj's 1-based index to c++'s 0-based index
\r
229 retrieveVertexIndices(vertexWord, Idx, vertexWord+wlength+1, vertexBuffer.size(), textureCoordBuffer.size(), normalsBuffer.size());
\r
230 if ( -1 != Idx[0] && Idx[0] < (irr::s32)vertexBuffer.size() )
\r
231 v.Pos = vertexBuffer[Idx[0]];
\r
234 os::Printer::log("Invalid vertex index in this line:", wordBuffer.c_str(), ELL_ERROR);
\r
238 if ( -1 != Idx[1] && Idx[1] < (irr::s32)textureCoordBuffer.size() )
\r
239 v.TCoords = textureCoordBuffer[Idx[1]];
\r
241 v.TCoords.set(0.0f,0.0f);
\r
242 if ( -1 != Idx[2] && Idx[2] < (irr::s32)normalsBuffer.size() )
\r
243 v.Normal = normalsBuffer[Idx[2]];
\r
246 v.Normal.set(0.0f,0.0f,0.0f);
\r
247 currMtl->RecalculateNormals=true;
\r
251 auto n = currMtl->VertMap.find(v);
\r
252 if (n != currMtl->VertMap.end())
\r
254 vertLocation = n->second;
\r
258 currMtl->Meshbuffer->Vertices.push_back(v);
\r
259 vertLocation = currMtl->Meshbuffer->Vertices.size() -1;
\r
260 currMtl->VertMap.emplace(v, vertLocation);
\r
263 faceCorners.push_back(vertLocation);
\r
265 // go to next vertex
\r
266 linePtr = goNextWord(linePtr, endPtr);
\r
269 // triangulate the face
\r
270 const int c = faceCorners[0];
\r
271 for ( u32 i = 1; i < faceCorners.size() - 1; ++i )
\r
274 const int a = faceCorners[i + 1];
\r
275 const int b = faceCorners[i];
\r
276 if (a != b && a != c && b != c) // ignore degenerated faces. We can get them when we merge vertices above in the VertMap.
\r
278 currMtl->Meshbuffer->Indices.push_back(a);
\r
279 currMtl->Meshbuffer->Indices.push_back(b);
\r
280 currMtl->Meshbuffer->Indices.push_back(c);
\r
284 ++degeneratedFaces;
\r
290 case '#': // comment
\r
293 } // end switch(bufPtr[0])
\r
294 // eat up rest of line
\r
295 bufPtr = goNextLine(bufPtr, bufEnd);
\r
297 } // end while(bufPtr && (bufPtr-buf<filesize))
\r
299 if ( degeneratedFaces > 0 )
\r
301 irr::core::stringc log(degeneratedFaces);
\r
302 log += " degenerated faces removed in ";
\r
303 log += irr::core::stringc(fullName);
\r
304 os::Printer::log(log.c_str(), ELL_INFORMATION);
\r
307 SMesh* mesh = new SMesh();
\r
309 // Combine all the groups (meshbuffers) into the mesh
\r
310 for ( u32 m = 0; m < Materials.size(); ++m )
\r
312 if ( Materials[m]->Meshbuffer->getIndexCount() > 0 )
\r
314 Materials[m]->Meshbuffer->recalculateBoundingBox();
\r
315 if (Materials[m]->RecalculateNormals)
\r
316 SceneManager->getMeshManipulator()->recalculateNormals(Materials[m]->Meshbuffer);
\r
317 mesh->addMeshBuffer( Materials[m]->Meshbuffer );
\r
321 // Create the Animated mesh if there's anything in the mesh
\r
322 SAnimatedMesh* animMesh = 0;
\r
323 if ( 0 != mesh->getMeshBufferCount() )
\r
325 mesh->recalculateBoundingBox();
\r
326 animMesh = new SAnimatedMesh();
\r
327 animMesh->Type = EAMT_OBJ;
\r
328 animMesh->addMesh(mesh);
\r
329 animMesh->recalculateBoundingBox();
\r
332 // Clean up the allocate obj file contents
\r
334 // more cleaning up
\r
342 const c8* COBJMeshFileLoader::readColor(const c8* bufPtr, video::SColor& color, const c8* const bufEnd)
\r
344 const u32 COLOR_BUFFER_LENGTH = 16;
\r
345 c8 colStr[COLOR_BUFFER_LENGTH];
\r
347 bufPtr = goAndCopyNextWord(colStr, bufPtr, COLOR_BUFFER_LENGTH, bufEnd);
\r
348 color.setRed((u32)core::round32(core::fast_atof(colStr)*255.f));
\r
349 bufPtr = goAndCopyNextWord(colStr, bufPtr, COLOR_BUFFER_LENGTH, bufEnd);
\r
350 color.setGreen((u32)core::round32(core::fast_atof(colStr)*255.f));
\r
351 bufPtr = goAndCopyNextWord(colStr, bufPtr, COLOR_BUFFER_LENGTH, bufEnd);
\r
352 color.setBlue((u32)core::round32(core::fast_atof(colStr)*255.f));
\r
357 //! Read 3d vector of floats
\r
358 const c8* COBJMeshFileLoader::readVec3(const c8* bufPtr, core::vector3df& vec, const c8* const bufEnd)
\r
360 const u32 WORD_BUFFER_LENGTH = 256;
\r
361 c8 wordBuffer[WORD_BUFFER_LENGTH];
\r
363 bufPtr = goAndCopyNextWord(wordBuffer, bufPtr, WORD_BUFFER_LENGTH, bufEnd);
\r
364 vec.X=-core::fast_atof(wordBuffer); // change handedness
\r
365 bufPtr = goAndCopyNextWord(wordBuffer, bufPtr, WORD_BUFFER_LENGTH, bufEnd);
\r
366 vec.Y=core::fast_atof(wordBuffer);
\r
367 bufPtr = goAndCopyNextWord(wordBuffer, bufPtr, WORD_BUFFER_LENGTH, bufEnd);
\r
368 vec.Z=core::fast_atof(wordBuffer);
\r
373 //! Read 2d vector of floats
\r
374 const c8* COBJMeshFileLoader::readUV(const c8* bufPtr, core::vector2df& vec, const c8* const bufEnd)
\r
376 const u32 WORD_BUFFER_LENGTH = 256;
\r
377 c8 wordBuffer[WORD_BUFFER_LENGTH];
\r
379 bufPtr = goAndCopyNextWord(wordBuffer, bufPtr, WORD_BUFFER_LENGTH, bufEnd);
\r
380 vec.X=core::fast_atof(wordBuffer);
\r
381 bufPtr = goAndCopyNextWord(wordBuffer, bufPtr, WORD_BUFFER_LENGTH, bufEnd);
\r
382 vec.Y=1-core::fast_atof(wordBuffer); // change handedness
\r
387 //! Read boolean value represented as 'on' or 'off'
\r
388 const c8* COBJMeshFileLoader::readBool(const c8* bufPtr, bool& tf, const c8* const bufEnd)
\r
390 const u32 BUFFER_LENGTH = 8;
\r
391 c8 tfStr[BUFFER_LENGTH];
\r
393 bufPtr = goAndCopyNextWord(tfStr, bufPtr, BUFFER_LENGTH, bufEnd);
\r
394 tf = strcmp(tfStr, "off") != 0;
\r
399 COBJMeshFileLoader::SObjMtl* COBJMeshFileLoader::findMtl(const core::stringc& mtlName, const core::stringc& grpName)
\r
401 COBJMeshFileLoader::SObjMtl* defMaterial = 0;
\r
402 // search existing Materials for best match
\r
403 // exact match does return immediately, only name match means a new group
\r
404 for (u32 i = 0; i < Materials.size(); ++i)
\r
406 if ( Materials[i]->Name == mtlName )
\r
408 if ( Materials[i]->Group == grpName )
\r
409 return Materials[i];
\r
411 defMaterial = Materials[i];
\r
414 // we found a partial match
\r
417 Materials.push_back(new SObjMtl(*defMaterial));
\r
418 Materials.getLast()->Group = grpName;
\r
419 return Materials.getLast();
\r
421 // we found a new group for a non-existant material
\r
422 else if (grpName.size())
\r
424 Materials.push_back(new SObjMtl(*Materials[0]));
\r
425 Materials.getLast()->Group = grpName;
\r
426 return Materials.getLast();
\r
432 //! skip space characters and stop on first non-space
\r
433 const c8* COBJMeshFileLoader::goFirstWord(const c8* buf, const c8* const bufEnd, bool acrossNewlines)
\r
435 // skip space characters
\r
436 if (acrossNewlines)
\r
437 while((buf != bufEnd) && core::isspace(*buf))
\r
440 while((buf != bufEnd) && core::isspace(*buf) && (*buf != '\n'))
\r
447 //! skip current word and stop at beginning of next one
\r
448 const c8* COBJMeshFileLoader::goNextWord(const c8* buf, const c8* const bufEnd, bool acrossNewlines)
\r
450 // skip current word
\r
451 while(( buf != bufEnd ) && !core::isspace(*buf))
\r
454 return goFirstWord(buf, bufEnd, acrossNewlines);
\r
458 //! Read until line break is reached and stop at the next non-space character
\r
459 const c8* COBJMeshFileLoader::goNextLine(const c8* buf, const c8* const bufEnd)
\r
461 // look for newline characters
\r
462 while(buf != bufEnd)
\r
464 // found it, so leave
\r
465 if (*buf=='\n' || *buf=='\r')
\r
469 return goFirstWord(buf, bufEnd);
\r
473 u32 COBJMeshFileLoader::copyWord(c8* outBuf, const c8* const inBuf, u32 outBufLength, const c8* const bufEnd)
\r
486 if (core::isspace(inBuf[i]) || &(inBuf[i]) == bufEnd)
\r
491 u32 length = core::min_(i, outBufLength-1);
\r
492 for (u32 j=0; j<length; ++j)
\r
493 outBuf[j] = inBuf[j];
\r
495 outBuf[length] = 0;
\r
500 core::stringc COBJMeshFileLoader::copyLine(const c8* inBuf, const c8* bufEnd)
\r
503 return core::stringc();
\r
505 const c8* ptr = inBuf;
\r
508 if (*ptr=='\n' || *ptr=='\r')
\r
512 // we must avoid the +1 in case the array is used up
\r
513 return core::stringc(inBuf, (u32)(ptr-inBuf+((ptr < bufEnd) ? 1 : 0)));
\r
517 const c8* COBJMeshFileLoader::goAndCopyNextWord(c8* outBuf, const c8* inBuf, u32 outBufLength, const c8* bufEnd)
\r
519 inBuf = goNextWord(inBuf, bufEnd, false);
\r
520 copyWord(outBuf, inBuf, outBufLength, bufEnd);
\r
525 bool COBJMeshFileLoader::retrieveVertexIndices(c8* vertexData, s32* idx, const c8* bufEnd, u32 vbsize, u32 vtsize, u32 vnsize)
\r
527 const u32 BUFFER_LENGTH = 16;
\r
528 c8 word[BUFFER_LENGTH];
\r
529 const c8* p = goFirstWord(vertexData, bufEnd);
\r
530 u32 idxType = 0; // 0 = posIdx, 1 = texcoordIdx, 2 = normalIdx
\r
533 while ( p != bufEnd )
\r
535 if ( i >= BUFFER_LENGTH )
\r
539 if ( ( core::isdigit(*p)) || (*p == '-') )
\r
541 // build up the number
\r
544 else if ( *p == '/' || *p == ' ' || *p == '\0' )
\r
546 // number is completed. Convert and store it
\r
548 // if no number was found index will become 0 and later on -1 by decrement
\r
549 idx[idxType] = core::strtol10(word);
\r
550 if (idx[idxType]<0)
\r
555 idx[idxType] += vbsize;
\r
558 idx[idxType] += vtsize;
\r
561 idx[idxType] += vnsize;
\r
572 // go to the next kind of index type
\r
575 if ( ++idxType > 2 )
\r
577 // error checking, shouldn't reach here unless file is wrong
\r
583 // set all missing values to disable (=-1)
\r
584 while (++idxType < 3)
\r
591 // go to the next char
\r
599 void COBJMeshFileLoader::cleanUp()
\r
601 for (u32 i=0; i < Materials.size(); ++i )
\r
603 Materials[i]->Meshbuffer->drop();
\r
604 delete Materials[i];
\r
611 } // end namespace scene
\r
612 } // end namespace irr
\r