obj-model.cpp

Go to the documentation of this file.
00001 /*
00002  * obj-model.cpp
00003  *
00004  * Copyright (C) 2009  Thomas A. Vaughan
00005  * All rights reserved.
00006  *
00007  *
00008  * Redistribution and use in source and binary forms, with or without
00009  * modification, are permitted provided that the following conditions are met:
00010  *     * Redistributions of source code must retain the above copyright
00011  *       notice, this list of conditions and the following disclaimer.
00012  *     * Redistributions in binary form must reproduce the above copyright
00013  *       notice, this list of conditions and the following disclaimer in the
00014  *       documentation and/or other materials provided with the distribution.
00015  *     * Neither the name of the <organization> nor the
00016  *       names of its contributors may be used to endorse or promote products
00017  *       derived from this software without specific prior written permission.
00018  *
00019  * THIS SOFTWARE IS PROVIDED BY THOMAS A. VAUGHAN ''AS IS'' AND ANY
00020  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
00021  * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
00022  * DISCLAIMED. IN NO EVENT SHALL THOMAS A. VAUGHAN BE LIABLE FOR ANY
00023  * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
00024  * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
00025  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
00026  * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
00027  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
00028  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
00029  *
00030  *
00031  * Routines to parse and draw OBJ 3D models (see obj-model.h)
00032  */
00033 
00034 // includes --------------------------------------------------------------------
00035 #include "obj-model.h"          // always include our own header first
00036 
00037 #include "obj-material.h"
00038 
00039 #include "common/wave_ex.h"
00040 #include "perf/perf.h"
00041 #include "util/parsing.h"
00042 #include "util/file.h"
00043 #include "wave-glut/material.h"
00044 
00045 
00046 namespace obj {
00047 
00048 
00049 // interface destructors
00050 Model::~Model(void) throw() { }
00051 
00052 
00053 // constants and statics
00054 static const eParseBehavior s_parseFlags                = eParse_Strip;
00055 
00056 
00057 // texture u,v coordinates
00058 struct uv_t {
00059         uv_t(void) throw() { }
00060         uv_t(IN float iu, IN float iv) : u(iu), v(iv) { }
00061         void clear(void) throw() {
00062                         u = v = 0.0;
00063                 }
00064 
00065         // data fields
00066         float           u;
00067         float           v;
00068 };
00069 
00070 
00071 
00072 // this is the face as read from the file: list of indices to vertex arrays
00073 struct face_idx_t {
00074         face_idx_t(void) throw() { }
00075         face_idx_t(IN long iv, IN long ivt, IN long ivn) throw() :
00076             v(iv), vt(ivt), vn(ivn) { }
00077 
00078         // data fields
00079         long            v;      // vertex index
00080         long            vt;     // vertex texture index
00081         long            vn;     // vertex normal index
00082 };
00083 
00084 
00085 
00086 // this is the raw face vertex data.
00087 struct face_vtx_t {
00088         // data fields
00089         point3d_t       v;      // vertex
00090         point3d_t       n;      // normal
00091         uv_t            uv;     // texture u,v-coordinates
00092 };
00093 
00094 
00095 
00096 ////////////////////////////////////////////////////////////////////////////////
00097 //
00098 //      static helper methods
00099 //
00100 ////////////////////////////////////////////////////////////////////////////////
00101 
00102 char *
00103 findChar
00104 (
00105 IN char * string,
00106 IN char find
00107 )
00108 throw()
00109 {
00110         ASSERT(string, "null");
00111 
00112         char * p = string;
00113         for (; *p && *p != find; ++p) { }
00114         return p;
00115 }
00116 
00117 
00118 
00119 static point3d_t
00120 readPoint3d
00121 (
00122 IN const char * p
00123 )
00124 {
00125         point3d_t v(0, 0, 0);
00126         readFloatsFromString(p, 3, &v.x);
00127         // v.dump("just parsed");
00128         return v;
00129 }
00130 
00131 
00132 
00133 static uv_t
00134 readUV
00135 (
00136 IN const char * p
00137 )
00138 {
00139         uv_t uv(0, 0);
00140         readFloatsFromString(p, 3, &uv.u);
00141         return uv;
00142 }
00143 
00144 
00145 
00146 ////////////////////////////////////////////////////////////////////////////////
00147 //
00148 //      OBJModel -- class that implements the obj::Model interface for
00149 //              Open Asset Import library aiScene objects.
00150 //
00151 ////////////////////////////////////////////////////////////////////////////////
00152 
00153 class OBJModel : public obj::Model {
00154 public:
00155         // constructor, destructor ---------------------------------------------
00156         OBJModel(void) throw();
00157         ~OBJModel(void) throw() { }
00158 
00159         // public class methods ------------------------------------------------
00160         void initialize(IN nstream::Stream * stream,
00161                                 IN float scale);
00162 
00163         // glut::Renderable class interface methods ----------------------------
00164         rect3d_t getBoundingBox(void) const throw() { return m_bounds; }
00165         void render(IN const glut::render_context_t& rc,
00166                                 IN glut::RenderQueue * rq);
00167 
00168         // obj::Model class interface methods ----------------------------------
00169 
00170 private:
00171         // private typedefs ----------------------------------------------------
00172         typedef std::vector<point3d_t> vec_point_t;
00173         typedef std::vector<uv_t> vec_uv_t;
00174         typedef std::vector<face_vtx_t> face_t;
00175 
00176         struct mesh_t {
00177                 std::string     material;
00178                 face_t          vertices;
00179         };
00180 
00181         typedef std::vector<smart_ptr<mesh_t> > vec_mesh_t;
00182 
00183         // private helper methods ----------------------------------------------
00184         void readFace(IN mesh_t * mesh,
00185                                 IN const char * p);
00186         void readMaterialMaps(IN nstream::Stream * stream,
00187                                 IN const char * p);
00188 
00189         // private member data -------------------------------------------------
00190         rect3d_t                        m_bounds;
00191         std::string                     m_timerName;
00192         vec_point_t                     m_vertices;
00193         vec_point_t                     m_vertexNormals;
00194         vec_uv_t                        m_vertexTexCoords;
00195         vec_mesh_t                      m_meshes;
00196         map_mtl_t                       m_materials;
00197         float                           m_scale;
00198         long                            m_faceCount;    // faces read from file
00199         long                            m_triangleCount;// triangles found
00200 };
00201 
00202 
00203 
00204 OBJModel::OBJModel
00205 (
00206 void
00207 )
00208 throw()
00209 {
00210         m_faceCount = 0;
00211         m_triangleCount = 0;
00212         m_scale = 1.0;
00213 }
00214 
00215 
00216 
00217 void
00218 OBJModel::initialize
00219 (
00220 IN nstream::Stream * stream,
00221 IN float scale
00222 )
00223 {
00224         perf::Timer timer("OBJ format parse");
00225         ASSERT(stream, "null");
00226         ASSERT(scale > 0, "Bad scale");
00227 
00228         // save scale
00229         m_scale = scale;
00230 
00231         // get the timer name
00232         m_timerName = "render:";
00233         smart_ptr<nstream::File> file = stream->getFile();
00234         if (file) {
00235                 m_timerName += GetFilename(file->getName());
00236         }
00237 
00238         // parse line by line
00239         eParseBehavior flags = s_parseFlags;
00240         std::string line, keyword, token;
00241         smart_ptr<mesh_t> currMesh;             // current mesh
00242         while (!stream->eof()) {
00243                 line = getNextLineFromStream(*stream, flags);
00244 
00245                 // now parse this line
00246                 const char * p = line.c_str();
00247 
00248                 // get the first token (keyword)
00249                 p = getNextTokenFromString(p, keyword, flags);
00250 
00251                 // skip empty lines
00252                 if ("" == keyword)
00253                         continue;
00254 
00255                 // what do we do?
00256                 // REMEMBER: it is more efficient to put most common first!
00257                 if ("v" == keyword) {
00258                         point3d_t v = readPoint3d(p);   // read vertex
00259                         if (!m_vertices.size()) {
00260                                 // first vertex!
00261                                 m_bounds.setToPoint(v);
00262                         } else {
00263                                 m_bounds.includePoint(v);
00264                         }
00265                         m_vertices.push_back(v);
00266                 } else if ("vn" == keyword) {
00267                         m_vertexNormals.push_back(readPoint3d(p));
00268                 } else if ("vt" == keyword) {
00269                         m_vertexTexCoords.push_back(readUV(p));
00270                 } else if ("f" == keyword) {
00271                         ASSERT_THROW(currMesh,
00272                             "faces specified without material?");
00273                         this->readFace(currMesh, p);
00274                 } else if ("usemtl" == keyword) {
00275                         currMesh = new mesh_t;
00276                         ASSERT(currMesh, "out of memory");
00277                         getNextTokenFromString(p, currMesh->material,
00278                             s_parseFlags);
00279                         ASSERT_THROW(
00280                             m_materials.end() != m_materials.find(currMesh->material),
00281                             "Unknown material: " << p);
00282                         DPRINTF("Reading new mesh with material: '%s'", 
00283                             currMesh->material.c_str());
00284                         m_meshes.push_back(currMesh);
00285                 } else if ("mtllib" == keyword) {
00286                         this->readMaterialMaps(stream, p);
00287                 } else {
00288                         DPRINTF("Skipping unrecognized OBJ keyword: %s",
00289                             line.c_str());
00290                 }
00291         }
00292 
00293         // report
00294         DPRINTF("Read %d vertices", (int) m_vertices.size());
00295         DPRINTF("Read %d vertex normals", (int) m_vertexNormals.size());
00296         DPRINTF("Read %d vertex texture coordinates", (int) m_vertexTexCoords.size());
00297         DPRINTF("Read %d meshes", (int) m_meshes.size());
00298         DPRINTF("Read %d materials", (int) m_materials.size());
00299         DPRINTF("Converted %ld faces into %ld triangles", m_faceCount,
00300             m_triangleCount);
00301         m_bounds.dump("Bounding box");
00302 
00303         // free memory!  all of this has been copied to vertex structs
00304         m_vertices.clear();
00305         m_vertexNormals.clear();
00306         m_vertexTexCoords.clear();
00307 }
00308 
00309 
00310 
00311 void
00312 OBJModel::render
00313 (
00314 IN const glut::render_context_t& rc,
00315 IN glut::RenderQueue * rq
00316 )
00317 {
00318         perf::Timer timer(m_timerName.c_str());
00319         ASSERT(rq, "null");
00320 
00321         // scale
00322         glMatrixMode(GL_MODELVIEW);
00323         glPushMatrix();
00324         glScalef(m_scale, m_scale, m_scale);
00325 
00326         // first, we'll get the vertex and vertex normal arrays into OpenGL
00327         glEnableClientState(GL_VERTEX_ARRAY);
00328         glEnableClientState(GL_NORMAL_ARRAY);
00329 
00330         // render each mesh
00331         for (vec_mesh_t::iterator i = m_meshes.begin(); i != m_meshes.end();
00332              ++i) {
00333                 mesh_t * mesh = *i;
00334                 ASSERT(mesh, "null");
00335 
00336                 // look up the material
00337                 map_mtl_t::iterator mtl = m_materials.find(mesh->material);
00338                 if (m_materials.end() == mtl) {
00339                         DPRINTF("WARNING: cannot find material: %s",
00340                             mesh->material.c_str());
00341                         continue;       // skip mesh altogether
00342                 }
00343                 smart_ptr<glut::Material>& material = mtl->second;
00344                 ASSERT(material, "null");
00345 
00346                 // set up material (and make sure it is cleaned up after)
00347                 glut::MaterialContext mc(material);
00348 
00349                 // we know the mesh is a triangle mesh, with vertices and
00350                 //      normals already provided to OpenGL above.
00351                 int nVertices = (int) mesh->vertices.size();
00352 
00353                 const int coordinatesPerVertex = 3;
00354                 const int type = GL_FLOAT;
00355                 const int stride = sizeof(face_vtx_t);
00356 
00357                 // get a reference to the first face vertex struct
00358                 face_vtx_t& fv0 = mesh->vertices[0];
00359 
00360                 // set vertex and normal arrays
00361                 glVertexPointer(coordinatesPerVertex, type, stride, &fv0.v.x);
00362                 glNormalPointer(type, stride, &fv0.n.x);
00363 
00364                 // now blast through all triangles
00365                 const int mode = GL_TRIANGLES;
00366                 const int startIndex = 0;
00367                 glDrawArrays(mode, startIndex, nVertices);
00368         }
00369 
00370         // all done!
00371         glDisableClientState(GL_VERTEX_ARRAY);
00372         glDisableClientState(GL_NORMAL_ARRAY);
00373         glPopMatrix();
00374 }
00375 
00376 
00377 
00378 ////////////////////////////////////////////////////////////////////////////////
00379 //
00380 //      OBJModel -- private helper methods
00381 //
00382 ////////////////////////////////////////////////////////////////////////////////
00383 
00384 void
00385 OBJModel::readFace
00386 (
00387 IN mesh_t * mesh,
00388 IN const char * p
00389 )
00390 {
00391         ASSERT(mesh, "null");
00392         ASSERT(p, "null");
00393 
00394         // initialize face
00395         smart_ptr<face_t> face = new face_t;
00396         ASSERT(face, "out of memory");
00397 
00398         // keep reading triplets
00399         const int s_maxSize = 31;       // should be big enough for 3 ints
00400         const int s_bufsize = s_maxSize + 1;
00401         char buffer[s_bufsize];
00402 
00403         // store parsed faces here
00404         const int s_maxFaceVertices = 12;
00405         face_idx_t face_indices[s_maxFaceVertices];
00406         int nVertices = 0;
00407         while (*p) {
00408                 int nChars;
00409                 p = getNextTokenFromString(p, buffer, s_bufsize, eParse_None,
00410                       nChars);
00411                 ASSERT_THROW(nChars < s_bufsize,
00412                     "entry in face vertex list is too large!");
00413 
00414                 // buffer now contains a string of the form X/Y/Z
00415                 // (Y may or may not be empty)
00416                 char * firstSlash = findChar(buffer, '/');
00417                 char * secondSlash = findChar(firstSlash + 1, '/');
00418 
00419                 ASSERT_THROW(*firstSlash && *secondSlash,
00420                     "Badly formatted face string: " << buffer);
00421 
00422                 // replace with nulls
00423                 *firstSlash = 0;
00424                 *secondSlash = 0;
00425                 char * secondInt = firstSlash + 1;
00426                 char * thirdInt = secondSlash + 1;
00427 
00428                 // okay, get values
00429                 ASSERT_THROW(nVertices < s_maxFaceVertices,
00430                     "OBJ file contains face with " << nVertices
00431                     << " or more vertices!");
00432 
00433                 // get a reference to the appropriate face
00434                 face_idx_t& f = face_indices[nVertices];
00435                 nVertices++;
00436 
00437                 // populate indices based on what we just parsed
00438                 f.v = atol(buffer);
00439                 f.vt = atol(secondInt);
00440                 f.vn = atol(thirdInt);
00441 
00442                 // obj format is 1-based, we want 0-based
00443                 f.v--;
00444                 f.vt--;
00445                 f.vn--;
00446 
00447                 // validate
00448                 ASSERT_THROW(f.v >= 0 && f.v < (long) m_vertices.size(),
00449                     "Invalid face vertex index: " << f.v);
00450                 ASSERT_THROW(f.vt >= -1 && f.vt < (long) m_vertexTexCoords.size(),
00451                     "Invalid face vertex texture coordinate index: " << f.vt);
00452                 ASSERT_THROW(f.vn >= -1 && f.vn < (long) m_vertexNormals.size(),
00453                     "Invalid face vertex normal index: " << f.vn);
00454         }
00455 //      DPRINTF("Face polygon contained %d vertices", nVertices);
00456         m_faceCount++;
00457 
00458         // convert this polygon into triangles
00459         int nTriangles = nVertices - 2;
00460 //      DPRINTF("  I'll convert this into %d triangles", nTriangles);
00461         for (int i = 0; i < nTriangles; ++i) {
00462 
00463                 // use a simple fan algorithm, pivoting on first vertex
00464                 face_idx_t v[3];
00465                 v[0] = face_indices[0];
00466                 v[1] = face_indices[i + 1];
00467                 v[2] = face_indices[i + 2];
00468 
00469                 // construct denormalized (non-indexed) face vertex struct
00470                 for (int j = 0; j < 3; ++j) {
00471                         face_vtx_t fv;
00472                         fv.v = m_vertices[v[j].v];
00473                         long idx = v[j].vn;
00474                         if (idx > -1) {
00475                                 fv.n = m_vertexNormals[idx];
00476                         } else {
00477                                 fv.n.clear();
00478                         }
00479                         idx = v[j].vt;
00480                         if (idx > -1) {
00481                                 fv.uv = m_vertexTexCoords[idx];
00482                         } else {
00483                                 fv.uv.clear();
00484                         }
00485 
00486                         // save this denormalized face vertex
00487                         mesh->vertices.push_back(fv);
00488                 }
00489         }
00490         m_triangleCount += nTriangles;
00491 }
00492 
00493 
00494 
00495 void
00496 OBJModel::readMaterialMaps
00497 (
00498 IN nstream::Stream * stream,
00499 IN const char * p
00500 )
00501 {
00502         ASSERT(stream, "null");
00503         ASSERT(p, "null");
00504 
00505         // get the stream File
00506         smart_ptr<nstream::File> file = stream->getFile();
00507         ASSERT_THROW(file, "stream does not have valid file?");
00508         const char * filename = file->getName();
00509         //DPRINTF("Stream is from file: %s", filename);
00510         smart_ptr<nstream::Manager> mgr = file->getManager();
00511         ASSERT_THROW(mgr, "file does not have valid stream Manager? " <<
00512             filename);
00513 
00514         // the mtllib line can contain multiple files--iterate
00515         std::string relPath;
00516         while (*p) {
00517                 p = getNextTokenFromString(p, relPath, s_parseFlags);
00518 
00519                 std::string path = getPathRelativeTo(filename, relPath.c_str());
00520                 //DPRINTF("Looking for relative path: %s", relPath.c_str());
00521                 //DPRINTF("  --> absolute path: %s", path.c_str());
00522 
00523                 smart_ptr<nstream::File> file = mgr->getEntry(path.c_str());
00524                 if (!file) {
00525                         DPRINTF("WARNING--could not load material file: %s",
00526                             path.c_str());
00527                         DPRINTF("This is often due to bad .obj files with "
00528                             "absolute instead of relative paths to .mtl files");
00529                         DPRINTF("(absolute paths are nonportable--your OBJ "
00530                             "files will break if copied to another path, even "
00531                             "if the MTL files go with them)");
00532                         DPRINTF("I'll try to load the mtl file as a peer to the"
00533                             " obj file...");
00534 
00535                         path = getPathRelativeTo(filename,
00536                             GetFilename(relPath.c_str()));
00537                         DPRINTF("Trying path: %s", path.c_str());
00538                         file = mgr->getEntry(path.c_str());
00539 
00540                         if (!file) {
00541                                 DPRINTF("That didn't work either!");
00542                                 DPRINTF("As a last-ditch attempt I'll try the "
00543                                     "absolute path.");
00544                                 DPRINTF("This is completely nonportable "
00545                                     "(definitely won't work if people copy "
00546                                     "files around) but it's the last thing I "
00547                                     "can try...");
00548                                 DPRINTF("Trying path: %s", relPath.c_str());
00549                                 file = mgr->getEntry(relPath.c_str());
00550 
00551                                 if (!file) {
00552                                         DPRINTF("ERROR--failed to load material"
00553                                             "file: %s", relPath.c_str());
00554                                         DPRINTF("Usually this is an error in "
00555                                             "the application writing the .OBJ "
00556                                             "file to begin with.");
00557                                         continue;       // skip material
00558                                 }
00559                         }
00560                 }
00561 
00562                 smart_ptr<nstream::Stream> stream = file->openStream();
00563                 ASSERT_THROW(stream, "could not open stream: " << path);
00564 
00565                 // okay, parse all the materials in that stream
00566                 map_mtl_t map;
00567                 try {
00568                         map = parseOBJMaterials(stream);
00569                 } catch (std::exception& e) {
00570                         WAVE_EX(wex);
00571                         wex << "Failed to parse material file: " << path;
00572                         wex << "\n" << e.what();
00573                 }
00574                 DPRINTF("Found %d materials in file", (int) map.size());
00575                 for (map_mtl_t::iterator i = map.begin(); i != map.end(); ++i) {
00576                         const char * mtlName = i->first.c_str();
00577                         smart_ptr<glut::Material> mtl = i->second;
00578                         ASSERT(mtl, "null");
00579 
00580                         // the policy is that the first map wins
00581                         // so don't allow duplicate inserts
00582                         map_mtl_t::iterator j = m_materials.find(mtlName);
00583                         if (m_materials.end() != j) {
00584                                 DPRINTF("WARNING--material already exists: %s",
00585                                     mtlName);
00586                                 DPRINTF("  Ignoring second specification");
00587                                 continue;
00588                         }
00589 
00590                         // okay, add it!
00591                         m_materials[mtlName] = mtl;
00592                 }
00593         }
00594 }
00595 
00596 
00597 
00598 ////////////////////////////////////////////////////////////////////////////////
00599 //
00600 //      public API
00601 //
00602 ////////////////////////////////////////////////////////////////////////////////
00603 
00604 smart_ptr<Model>
00605 Model::create
00606 (
00607 IN nstream::Stream * stream,
00608 IN float scale
00609 )
00610 {
00611         ASSERT(stream, "null");
00612         ASSERT(scale > 0, "Bad scale: %f", scale);
00613 
00614         smart_ptr<OBJModel> local = new OBJModel;
00615         ASSERT(local, "out of memory");
00616 
00617         try {
00618                 local->initialize(stream, scale);
00619         } catch (std::exception& e) {
00620                 WAVE_EX(wex);
00621                 wex << "Failed to parse OBJ file: ";
00622                 stream->writeDiagnostics(wex);
00623                 wex << "\n" << e.what();
00624         }
00625 
00626         return local;
00627 }
00628 
00629 
00630 
00631 };      // obj namespace
00632