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
29 static const u32 WORD_BUFFER_LENGTH = 512;
\r
32 COBJMeshFileLoader::COBJMeshFileLoader(scene::ISceneManager* smgr, io::IFileSystem* fs)
\r
33 : SceneManager(smgr), FileSystem(fs)
\r
36 setDebugName("COBJMeshFileLoader");
\r
45 COBJMeshFileLoader::~COBJMeshFileLoader()
\r
52 //! returns true if the file maybe is able to be loaded by this class
\r
53 //! based on the file extension (e.g. ".bsp")
\r
54 bool COBJMeshFileLoader::isALoadableFileExtension(const io::path& filename) const
\r
56 return core::hasFileExtension ( filename, "obj" );
\r
60 //! creates/loads an animated mesh from the file.
\r
61 //! \return Pointer to the created mesh. Returns 0 if loading failed.
\r
62 //! If you no longer need the mesh, you should call IAnimatedMesh::drop().
\r
63 //! See IReferenceCounted::drop() for more information.
\r
64 IAnimatedMesh* COBJMeshFileLoader::createMesh(io::IReadFile* file)
\r
69 const long filesize = file->getSize();
\r
73 const u32 WORD_BUFFER_LENGTH = 512;
\r
75 core::array<core::vector3df, core::irrAllocatorFast<core::vector3df> > vertexBuffer(1000);
\r
76 core::array<core::vector3df, core::irrAllocatorFast<core::vector3df> > normalsBuffer(1000);
\r
77 core::array<core::vector2df, core::irrAllocatorFast<core::vector2df> > textureCoordBuffer(1000);
\r
79 SObjMtl * currMtl = new SObjMtl();
\r
80 Materials.push_back(currMtl);
\r
81 u32 smoothingGroup=0;
\r
83 const io::path fullName = file->getFileName();
\r
84 const io::path relPath = FileSystem->getFileDir(fullName)+"/";
\r
86 c8* buf = new c8[filesize];
\r
87 memset(buf, 0, filesize);
\r
88 file->read((void*)buf, filesize);
\r
89 const c8* const bufEnd = buf+filesize;
\r
91 // Process obj information
\r
92 const c8* bufPtr = buf;
\r
93 core::stringc grpName, mtlName;
\r
94 bool mtlChanged=false;
\r
95 bool useGroups = !SceneManager->getParameters()->getAttributeAsBool(OBJ_LOADER_IGNORE_GROUPS);
\r
96 bool useMaterials = !SceneManager->getParameters()->getAttributeAsBool(OBJ_LOADER_IGNORE_MATERIAL_FILES);
\r
97 irr::u32 lineNr = 1; // only counts non-empty lines, still useful in debugging to locate errors
\r
98 core::array<int> faceCorners;
\r
99 faceCorners.reallocate(32); // should be large enough
\r
100 const core::stringc TAG_OFF = "off";
\r
101 irr::u32 degeneratedFaces = 0;
\r
103 while(bufPtr != bufEnd)
\r
107 case 'm': // mtllib (material)
\r
111 c8 name[WORD_BUFFER_LENGTH];
\r
112 bufPtr = goAndCopyNextWord(name, bufPtr, WORD_BUFFER_LENGTH, bufEnd);
\r
113 #ifdef _IRR_DEBUG_OBJ_LOADER_
\r
114 os::Printer::log("Reading material file",name);
\r
120 case 'v': // v, vn, vt
\r
123 case ' ': // vertex
\r
125 core::vector3df vec;
\r
126 bufPtr = readVec3(bufPtr, vec, bufEnd);
\r
127 vertexBuffer.push_back(vec);
\r
131 case 'n': // normal
\r
133 core::vector3df vec;
\r
134 bufPtr = readVec3(bufPtr, vec, bufEnd);
\r
135 normalsBuffer.push_back(vec);
\r
139 case 't': // texcoord
\r
141 core::vector2df vec;
\r
142 bufPtr = readUV(bufPtr, vec, bufEnd);
\r
143 textureCoordBuffer.push_back(vec);
\r
149 case 'g': // group name
\r
151 c8 grp[WORD_BUFFER_LENGTH];
\r
152 bufPtr = goAndCopyNextWord(grp, bufPtr, WORD_BUFFER_LENGTH, bufEnd);
\r
153 #ifdef _IRR_DEBUG_OBJ_LOADER_
\r
154 os::Printer::log("Loaded group start",grp, ELL_DEBUG);
\r
161 grpName = "default";
\r
167 case 's': // smoothing can be a group or off (equiv. to 0)
\r
169 c8 smooth[WORD_BUFFER_LENGTH];
\r
170 bufPtr = goAndCopyNextWord(smooth, bufPtr, WORD_BUFFER_LENGTH, bufEnd);
\r
171 #ifdef _IRR_DEBUG_OBJ_LOADER_
\r
172 os::Printer::log("Loaded smoothing group start",smooth, ELL_DEBUG);
\r
174 if (TAG_OFF==smooth)
\r
177 smoothingGroup=core::strtoul10(smooth);
\r
179 (void)smoothingGroup; // disable unused variable warnings
\r
183 case 'u': // usemtl
\r
184 // get name of material
\r
186 c8 matName[WORD_BUFFER_LENGTH];
\r
187 bufPtr = goAndCopyNextWord(matName, bufPtr, WORD_BUFFER_LENGTH, bufEnd);
\r
188 #ifdef _IRR_DEBUG_OBJ_LOADER_
\r
189 os::Printer::log("Loaded material start",matName, ELL_DEBUG);
\r
198 c8 vertexWord[WORD_BUFFER_LENGTH]; // for retrieving vertex data
\r
199 video::S3DVertex v;
\r
200 // Assign vertex color from currently active material's diffuse color
\r
203 // retrieve the material
\r
204 SObjMtl *useMtl = findMtl(mtlName, grpName);
\r
205 // only change material if we found it
\r
211 v.Color = currMtl->Meshbuffer->Material.DiffuseColor;
\r
213 // get all vertices data in this face (current line of obj file)
\r
214 const core::stringc wordBuffer = copyLine(bufPtr, bufEnd);
\r
215 const c8* linePtr = wordBuffer.c_str();
\r
216 const c8* const endPtr = linePtr+wordBuffer.size();
\r
218 faceCorners.set_used(0); // fast clear
\r
220 // read in all vertices
\r
221 linePtr = goNextWord(linePtr, endPtr);
\r
222 while (0 != linePtr[0])
\r
224 // Array to communicate with retrieveVertexIndices()
\r
225 // sends the buffer sizes and gets the actual indices
\r
226 // if index not set returns -1
\r
228 Idx[0] = Idx[1] = Idx[2] = -1;
\r
230 // read in next vertex's data
\r
231 u32 wlength = copyWord(vertexWord, linePtr, WORD_BUFFER_LENGTH, endPtr);
\r
232 // this function will also convert obj's 1-based index to c++'s 0-based index
\r
233 retrieveVertexIndices(vertexWord, Idx, vertexWord+wlength+1, vertexBuffer.size(), textureCoordBuffer.size(), normalsBuffer.size());
\r
234 if ( -1 != Idx[0] && Idx[0] < (irr::s32)vertexBuffer.size() )
\r
235 v.Pos = vertexBuffer[Idx[0]];
\r
238 os::Printer::log("Invalid vertex index in this line:", wordBuffer.c_str(), ELL_ERROR);
\r
242 if ( -1 != Idx[1] && Idx[1] < (irr::s32)textureCoordBuffer.size() )
\r
243 v.TCoords = textureCoordBuffer[Idx[1]];
\r
245 v.TCoords.set(0.0f,0.0f);
\r
246 if ( -1 != Idx[2] && Idx[2] < (irr::s32)normalsBuffer.size() )
\r
247 v.Normal = normalsBuffer[Idx[2]];
\r
250 v.Normal.set(0.0f,0.0f,0.0f);
\r
251 currMtl->RecalculateNormals=true;
\r
255 core::map<video::S3DVertex, int>::Node* n = currMtl->VertMap.find(v);
\r
258 vertLocation = n->getValue();
\r
262 currMtl->Meshbuffer->Vertices.push_back(v);
\r
263 vertLocation = currMtl->Meshbuffer->Vertices.size() -1;
\r
264 currMtl->VertMap.insert(v, vertLocation);
\r
267 faceCorners.push_back(vertLocation);
\r
269 // go to next vertex
\r
270 linePtr = goNextWord(linePtr, endPtr);
\r
273 // triangulate the face
\r
274 const int c = faceCorners[0];
\r
275 for ( u32 i = 1; i < faceCorners.size() - 1; ++i )
\r
278 const int a = faceCorners[i + 1];
\r
279 const int b = faceCorners[i];
\r
280 if (a != b && a != c && b != c) // ignore degenerated faces. We can get them when we merge vertices above in the VertMap.
\r
282 currMtl->Meshbuffer->Indices.push_back(a);
\r
283 currMtl->Meshbuffer->Indices.push_back(b);
\r
284 currMtl->Meshbuffer->Indices.push_back(c);
\r
288 ++degeneratedFaces;
\r
294 case '#': // comment
\r
297 } // end switch(bufPtr[0])
\r
298 // eat up rest of line
\r
299 bufPtr = goNextLine(bufPtr, bufEnd);
\r
301 } // end while(bufPtr && (bufPtr-buf<filesize))
\r
303 if ( degeneratedFaces > 0 )
\r
305 irr::core::stringc log(degeneratedFaces);
\r
306 log += " degenerated faces removed in ";
\r
307 log += irr::core::stringc(fullName);
\r
308 os::Printer::log(log.c_str(), ELL_INFORMATION);
\r
311 SMesh* mesh = new SMesh();
\r
313 // Combine all the groups (meshbuffers) into the mesh
\r
314 for ( u32 m = 0; m < Materials.size(); ++m )
\r
316 if ( Materials[m]->Meshbuffer->getIndexCount() > 0 )
\r
318 Materials[m]->Meshbuffer->recalculateBoundingBox();
\r
319 if (Materials[m]->RecalculateNormals)
\r
320 SceneManager->getMeshManipulator()->recalculateNormals(Materials[m]->Meshbuffer);
\r
321 if (Materials[m]->Meshbuffer->Material.MaterialType == video::EMT_PARALLAX_MAP_SOLID)
\r
324 tmp.addMeshBuffer(Materials[m]->Meshbuffer);
\r
325 IMesh* tangentMesh = SceneManager->getMeshManipulator()->createMeshWithTangents(&tmp);
\r
326 mesh->addMeshBuffer(tangentMesh->getMeshBuffer(0));
\r
327 tangentMesh->drop();
\r
330 mesh->addMeshBuffer( Materials[m]->Meshbuffer );
\r
334 // Create the Animated mesh if there's anything in the mesh
\r
335 SAnimatedMesh* animMesh = 0;
\r
336 if ( 0 != mesh->getMeshBufferCount() )
\r
338 mesh->recalculateBoundingBox();
\r
339 animMesh = new SAnimatedMesh();
\r
340 animMesh->Type = EAMT_OBJ;
\r
341 animMesh->addMesh(mesh);
\r
342 animMesh->recalculateBoundingBox();
\r
345 // Clean up the allocate obj file contents
\r
347 // more cleaning up
\r
355 const c8* COBJMeshFileLoader::readColor(const c8* bufPtr, video::SColor& color, const c8* const bufEnd)
\r
357 const u32 COLOR_BUFFER_LENGTH = 16;
\r
358 c8 colStr[COLOR_BUFFER_LENGTH];
\r
360 bufPtr = goAndCopyNextWord(colStr, bufPtr, COLOR_BUFFER_LENGTH, bufEnd);
\r
361 color.setRed((u32)core::round32(core::fast_atof(colStr)*255.f));
\r
362 bufPtr = goAndCopyNextWord(colStr, bufPtr, COLOR_BUFFER_LENGTH, bufEnd);
\r
363 color.setGreen((u32)core::round32(core::fast_atof(colStr)*255.f));
\r
364 bufPtr = goAndCopyNextWord(colStr, bufPtr, COLOR_BUFFER_LENGTH, bufEnd);
\r
365 color.setBlue((u32)core::round32(core::fast_atof(colStr)*255.f));
\r
370 //! Read 3d vector of floats
\r
371 const c8* COBJMeshFileLoader::readVec3(const c8* bufPtr, core::vector3df& vec, const c8* const bufEnd)
\r
373 const u32 WORD_BUFFER_LENGTH = 256;
\r
374 c8 wordBuffer[WORD_BUFFER_LENGTH];
\r
376 bufPtr = goAndCopyNextWord(wordBuffer, bufPtr, WORD_BUFFER_LENGTH, bufEnd);
\r
377 vec.X=-core::fast_atof(wordBuffer); // change handedness
\r
378 bufPtr = goAndCopyNextWord(wordBuffer, bufPtr, WORD_BUFFER_LENGTH, bufEnd);
\r
379 vec.Y=core::fast_atof(wordBuffer);
\r
380 bufPtr = goAndCopyNextWord(wordBuffer, bufPtr, WORD_BUFFER_LENGTH, bufEnd);
\r
381 vec.Z=core::fast_atof(wordBuffer);
\r
386 //! Read 2d vector of floats
\r
387 const c8* COBJMeshFileLoader::readUV(const c8* bufPtr, core::vector2df& vec, const c8* const bufEnd)
\r
389 const u32 WORD_BUFFER_LENGTH = 256;
\r
390 c8 wordBuffer[WORD_BUFFER_LENGTH];
\r
392 bufPtr = goAndCopyNextWord(wordBuffer, bufPtr, WORD_BUFFER_LENGTH, bufEnd);
\r
393 vec.X=core::fast_atof(wordBuffer);
\r
394 bufPtr = goAndCopyNextWord(wordBuffer, bufPtr, WORD_BUFFER_LENGTH, bufEnd);
\r
395 vec.Y=1-core::fast_atof(wordBuffer); // change handedness
\r
400 //! Read boolean value represented as 'on' or 'off'
\r
401 const c8* COBJMeshFileLoader::readBool(const c8* bufPtr, bool& tf, const c8* const bufEnd)
\r
403 const u32 BUFFER_LENGTH = 8;
\r
404 c8 tfStr[BUFFER_LENGTH];
\r
406 bufPtr = goAndCopyNextWord(tfStr, bufPtr, BUFFER_LENGTH, bufEnd);
\r
407 tf = strcmp(tfStr, "off") != 0;
\r
412 COBJMeshFileLoader::SObjMtl* COBJMeshFileLoader::findMtl(const core::stringc& mtlName, const core::stringc& grpName)
\r
414 COBJMeshFileLoader::SObjMtl* defMaterial = 0;
\r
415 // search existing Materials for best match
\r
416 // exact match does return immediately, only name match means a new group
\r
417 for (u32 i = 0; i < Materials.size(); ++i)
\r
419 if ( Materials[i]->Name == mtlName )
\r
421 if ( Materials[i]->Group == grpName )
\r
422 return Materials[i];
\r
424 defMaterial = Materials[i];
\r
427 // we found a partial match
\r
430 Materials.push_back(new SObjMtl(*defMaterial));
\r
431 Materials.getLast()->Group = grpName;
\r
432 return Materials.getLast();
\r
434 // we found a new group for a non-existant material
\r
435 else if (grpName.size())
\r
437 Materials.push_back(new SObjMtl(*Materials[0]));
\r
438 Materials.getLast()->Group = grpName;
\r
439 return Materials.getLast();
\r
445 //! skip space characters and stop on first non-space
\r
446 const c8* COBJMeshFileLoader::goFirstWord(const c8* buf, const c8* const bufEnd, bool acrossNewlines)
\r
448 // skip space characters
\r
449 if (acrossNewlines)
\r
450 while((buf != bufEnd) && core::isspace(*buf))
\r
453 while((buf != bufEnd) && core::isspace(*buf) && (*buf != '\n'))
\r
460 //! skip current word and stop at beginning of next one
\r
461 const c8* COBJMeshFileLoader::goNextWord(const c8* buf, const c8* const bufEnd, bool acrossNewlines)
\r
463 // skip current word
\r
464 while(( buf != bufEnd ) && !core::isspace(*buf))
\r
467 return goFirstWord(buf, bufEnd, acrossNewlines);
\r
471 //! Read until line break is reached and stop at the next non-space character
\r
472 const c8* COBJMeshFileLoader::goNextLine(const c8* buf, const c8* const bufEnd)
\r
474 // look for newline characters
\r
475 while(buf != bufEnd)
\r
477 // found it, so leave
\r
478 if (*buf=='\n' || *buf=='\r')
\r
482 return goFirstWord(buf, bufEnd);
\r
486 u32 COBJMeshFileLoader::copyWord(c8* outBuf, const c8* const inBuf, u32 outBufLength, const c8* const bufEnd)
\r
499 if (core::isspace(inBuf[i]) || &(inBuf[i]) == bufEnd)
\r
504 u32 length = core::min_(i, outBufLength-1);
\r
505 for (u32 j=0; j<length; ++j)
\r
506 outBuf[j] = inBuf[j];
\r
508 outBuf[length] = 0;
\r
513 core::stringc COBJMeshFileLoader::copyLine(const c8* inBuf, const c8* bufEnd)
\r
516 return core::stringc();
\r
518 const c8* ptr = inBuf;
\r
521 if (*ptr=='\n' || *ptr=='\r')
\r
525 // we must avoid the +1 in case the array is used up
\r
526 return core::stringc(inBuf, (u32)(ptr-inBuf+((ptr < bufEnd) ? 1 : 0)));
\r
530 const c8* COBJMeshFileLoader::goAndCopyNextWord(c8* outBuf, const c8* inBuf, u32 outBufLength, const c8* bufEnd)
\r
532 inBuf = goNextWord(inBuf, bufEnd, false);
\r
533 copyWord(outBuf, inBuf, outBufLength, bufEnd);
\r
538 bool COBJMeshFileLoader::retrieveVertexIndices(c8* vertexData, s32* idx, const c8* bufEnd, u32 vbsize, u32 vtsize, u32 vnsize)
\r
541 const c8* p = goFirstWord(vertexData, bufEnd);
\r
542 u32 idxType = 0; // 0 = posIdx, 1 = texcoordIdx, 2 = normalIdx
\r
545 while ( p != bufEnd )
\r
547 if ( ( core::isdigit(*p)) || (*p == '-') )
\r
549 // build up the number
\r
552 else if ( *p == '/' || *p == ' ' || *p == '\0' )
\r
554 // number is completed. Convert and store it
\r
556 // if no number was found index will become 0 and later on -1 by decrement
\r
557 idx[idxType] = core::strtol10(word);
\r
558 if (idx[idxType]<0)
\r
563 idx[idxType] += vbsize;
\r
566 idx[idxType] += vtsize;
\r
569 idx[idxType] += vnsize;
\r
580 // go to the next kind of index type
\r
583 if ( ++idxType > 2 )
\r
585 // error checking, shouldn't reach here unless file is wrong
\r
591 // set all missing values to disable (=-1)
\r
592 while (++idxType < 3)
\r
599 // go to the next char
\r
607 void COBJMeshFileLoader::cleanUp()
\r
609 for (u32 i=0; i < Materials.size(); ++i )
\r
611 Materials[i]->Meshbuffer->drop();
\r
612 delete Materials[i];
\r
619 } // end namespace scene
\r
620 } // end namespace irr
\r
622 #endif // _IRR_COMPILE_WITH_OBJ_LOADER_
\r