// Copyright (C) Thorsten Thormaehlen, Marburg, 2014, All rights reserved // Contact: www.thormae.de // This software is written for educational (non-commercial) purpose. // There is no warranty or other guarantee of fitness for this software, // it is provided solely "as is". // This tool converts a Wavefront OBJ file into an array of float values, which is // then stored as a binary file or a plain text file. // Such an array-based representation of a 3D mesh is useful because the data // can be directly placed (without parsing) in a Vertex Buffer Object (VBO). // Vertex buffers are, for example, used for direct rendering by OpenGL // via the "glDrawArrays" function or by DirectX via the "DrawPrimitive" function. // The first number in the plain text file (or first 4 bytes interpreted // as "unsigned int" in the binary file) informs about the total number of // floats to follow in the file. This is useful for allocating the correct amount of // memory while reading. // The output format of the float array can be customized in the // parameter section of this file. #include #include #include #include #include #include #include using namespace std; enum VertexDataTypes { POS_3f, // position x,y,z (3 floats) NORMAL_3f, // normal (3 floats) TEXCOORD_2f, // texture coordinates (2 floats) DIFFUSE_3f, // diffuse color (3 floats) SPECULAR_3f, // specular color (3 floats) AMBIENT_3f, // ambient color (3 floats) SPEC_EXP_1f, // specular exponent (1 float) ALPHA_1f, // alpha value (1 float) MAT_ID_1f, // internal material id (1 float) FILL1_1f, // 1.0 (1 float) FILL0_1f // 0.0 (1 float) }; ////////////////////////////////////////////////////////////////////// // Start parameter section (choose your customized output format here) ////////////////////////////////////////////////////////////////////// int includedPerVertexData[] = {POS_3f, NORMAL_3f, TEXCOORD_2f, DIFFUSE_3f, SPECULAR_3f, AMBIENT_3f, SPEC_EXP_1f, ALPHA_1f, MAT_ID_1f}; // Here you can select which per-vertex data should be included as well as their order, e.g., // if the per-vertex data in the output VBO should look like this: // // struct Vertex { // float position[3]; // float texCoord[2]; // float normal[3]; // }; // // you could select: // int includedPerVertexData[] = {POS_3f, TEXCOORD_2f, NORMAL_3f}; // // The FILL1 and FILL0 elements can be used to fill up with 0 or 1 entries, e.g., // if the per-vertex data in the output VBO should look like this: // // struct Vertex { // float rgba[4]; // float position[4]; // }; // // you could select: // int includedPerVertexData[] = {DIFFUSE_3f, ALPHA_1f, POS_3f, FILL1_1f}; // // One last example, which is a very typical setting: // // struct Vertex { // float position[3]; // float color[4]; // float texCoord[2]; // float normal[3]; // }; // // you could select: // int includedPerVertexData[] = {POS_3f, DIFFUSE_3f, ALPHA_1f, TEXCOORD_2f, NORMAL_3f}; // bool writeBinary = false; // false = write to plain text file, true = write to binary file // A binary file will be faster to read but is less portable // becaue the target machine might have a different byte order (endianness). std::string separator("\n"); // if the float array is written in plain text format, you can choose a // separator here, such as " " or "," or ";" or "\t" or "\n" bool normalize = true; float boundingSize = 1.0f; // false = position data is used as it is read from the obj file // true = position data is moved and scaled such that // the center of the mesh is at the origin and // the compete mesh is within the bounds // [-boundingSize/2; boundingSize/2] in x, y, and z-direction ////////////////////////////////////////////////////////////////////// // End parameter section ////////////////////////////////////////////////////////////////////// #define OBJ2VBO_ERROR(msg) { cout << "ERROR: " << msg << " (in Line: "<< __LINE__ << ")" << endl; } #define OBJ2VBO_WARN(msg) { cout << "WARNING: " << msg << " (in Line: "<< __LINE__ << ")" << endl; } #define OBJ2VBO_DEBUG(msg) {/*cout << "DEBUG: " << msg << " (in Line: "<< __LINE__ << ")" << endl;*/} #define OBJ2VBO_INFO(msg) { cout << "INFO: " << msg << " (in Line: "<< __LINE__ << ")" << endl;} class Material { public : float diffuseColor[3]; // diffuse color in range [0.0, 1.0] float specularColor[3]; // specular color in range [0.0, 1.0] float ambientColor[3]; // ambient color in range [0.0, 1.0] float specularExponent; // specular exponent in range [0.0, 1000.0] float alpha; // alpa value: 0.0 = fully transparent, 1.0 = fully opaque float illuMod; // obj illumination model // constructor Material() { diffuseColor[0] = 0.5f; diffuseColor[1] = 0.5f; diffuseColor[2] = 0.5f; specularColor[0] = 1.0f; specularColor[1] = 1.0f; specularColor[2] = 1.0f; ambientColor[0] = 0.1f; ambientColor[1] = 0.1f; ambientColor[2] = 0.1f; specularExponent = 10.0f; alpha = 1.0f; illuMod = 2.0f; } }; // some local helper functions bool fileExist(const std::string &filename) { OBJ2VBO_DEBUG ("Probing file: " << filename); ifstream myfile(filename.c_str()); if (!myfile.is_open()) { return false; } myfile.close(); return true; } std::string findFile(const std::string &filename, const std::string &baseFile) { std::string delimiters ("\\/"); // backslash or slash string::size_type posFile = filename.find_last_of(delimiters); string::size_type posBase = baseFile.find_last_of(delimiters); std::string basePath (""); if(posBase != string::npos) { basePath = baseFile.substr(0, posBase+1); } if(posFile != string::npos) { string::size_type posCol = filename.find_last_of(':'); if(posCol != string::npos) { // if the file name seems to be an absolute path, use the filename string directly if(fileExist(filename)) return filename; } } // assuming a relative path if(fileExist(basePath + filename)) return basePath + filename; // trying to find the mat file in the same directory as the obj file std::string inDirName = filename; if(posFile != string::npos) { inDirName = filename.substr(posFile+1, filename.size()-posFile); } if(fileExist(basePath + inDirName)) return basePath + inDirName; return filename; } void normalizePositions(std::vector &pos, float targetBound) { if(pos.size() < 3) return; float min[3], max[3], center[3]; min[0] = min[1] = min[2] = 1e16f; max[0] = max[1] = max[2] = -1e16f; // get min, max for(unsigned i=0; i < pos.size(); i+=3) { for(unsigned j=0; j < 3; j++) { float val = pos[i+j]; if(val < min[j]) min[j] = val; if(val > max[j]) max[j] = val; } } // get center for(unsigned j=0; j < 3; j++) { center[j] = (max[j] + min[j]) / 2.0f; } // get largest axis float largestDiff = 0.0f; for(unsigned j=0; j < 3; j++) { float diff = fabs(max[j] - min[j]); if(diff > largestDiff) largestDiff = diff; } // normalize for(unsigned i=0; i < pos.size(); i+=3) { for(unsigned j=0; j < 3; j++) { pos[i+j] -= center[j]; if(largestDiff > 1e-16) { pos[i+j] *= targetBound / largestDiff; } } } OBJ2VBO_INFO("Mesh's center moved from (" << center[0] << "," << center[1] << "," << center[2] << ") to (0,0,0)"); if(largestDiff > 1e-16) { OBJ2VBO_INFO("Mesh's maximum bounding length scaled from " << largestDiff << " to " << targetBound); } } bool loadMaterialLib(const std::string &filename, std::vector &materials, std::vector &matNames) { if(matNames.size() != materials.size()) { matNames.resize(materials.size()); } OBJ2VBO_INFO("Loading material lib: " << filename); std::string line; unsigned lineNumber = 0; int currentMaterial = -1; ifstream myfile(filename.c_str()); if (!myfile.is_open()) { OBJ2VBO_ERROR ("Can not open file: " << filename); return false; } while ( myfile.good() ) { std::getline (myfile,line); ++lineNumber; OBJ2VBO_DEBUG (line); std::istringstream ss(line); ss.unsetf(std::ios_base::skipws); // skip whitespace at beginning of line ss >> std::ws; if(!ss.eof()) { char firstChar; ss >> firstChar; switch(firstChar) { case '#' : { OBJ2VBO_DEBUG ("Found comment:"); } break; case 'n' : { // newmtl std::string newmtl; std::string materialName; ss >> newmtl >> std::ws >> materialName; if(newmtl.compare("ewmtl") == 0) { OBJ2VBO_DEBUG ("newmtl"); Material mat; matNames.push_back(materialName); materials.push_back(mat); currentMaterial = materials.size() - 1; } } break; case 'm' : { // map std::string map; std::string mapFileName; ss >> map >> std::ws >> mapFileName; OBJ2VBO_DEBUG ("Map is not used: " << mapFileName); } break; case 'i' : { // illumination model std::string illum; int illumNo; ss >> illum >> std::ws >> illumNo; if (!ss.fail()) { if(currentMaterial >=0 && currentMaterial < int(materials.size())) { OBJ2VBO_DEBUG ("illumination model"); materials[currentMaterial].illuMod = float(illumNo); } } } // end 'i' break; case 'N' : { // Ns (specular coefficient) char nextChar, ws; ss >> nextChar; if(nextChar == 's') { float specular; ss >> ws >> std::ws >> specular; if (!ss.fail() && isspace(ws)) { if(currentMaterial >=0 && currentMaterial < int(materials.size())) { OBJ2VBO_DEBUG ("specular coefficient"); materials[currentMaterial].specularExponent = specular; } } } }// end 'N' break; case 'T' : { // Tr (transparency) char nextChar, ws; ss >> nextChar; if(nextChar == 'r') { float tr; ss >> ws >> std::ws >> tr; if (!ss.fail() && isspace(ws)) { if(currentMaterial >=0 && currentMaterial < int(materials.size())) { OBJ2VBO_DEBUG ("transparency"); materials[currentMaterial].alpha = 1-tr; } } } }// end 'T' break; case 'd' : { // d (dissolved = transparency) float alpha; char ws; ss >> ws >> std::ws >> alpha; if (ss && isspace(ws)) { if(currentMaterial >=0 && currentMaterial < int(materials.size())) { OBJ2VBO_DEBUG ("dissolved"); materials[currentMaterial].alpha = alpha; } } }// end 'd' break; case 'K': // Ka, Kd, or Ks { char nextChar, ws_r, ws_g, ws_b; ss >> nextChar; switch(nextChar) { case 'a': { // ambient color OBJ2VBO_DEBUG ("Ambient color"); float rgb[3]; ss >> ws_r >> std::ws >> rgb[0] >> ws_g >> std::ws >> rgb[1] >> ws_b >> std::ws >> rgb[2]; if (!ss.fail() && isspace(ws_r) && isspace(ws_g) && isspace(ws_b)) { if(currentMaterial >=0 && currentMaterial < int(materials.size())) { materials[currentMaterial].ambientColor[0] = rgb[0]; materials[currentMaterial].ambientColor[1] = rgb[1]; materials[currentMaterial].ambientColor[2] = rgb[2]; } }else{ OBJ2VBO_ERROR ("Parsing error"); } } break; case 'd' : { // diffuse color OBJ2VBO_DEBUG ("Diffuse color"); float rgb[3]; ss >> ws_r >> std::ws >> rgb[0] >> ws_g >> std::ws >> rgb[1] >> ws_b >> std::ws >> rgb[2]; if (!ss.fail() && isspace(ws_r) && isspace(ws_g) && isspace(ws_b)) { if(currentMaterial >=0 && currentMaterial < int(materials.size())) { materials[currentMaterial].diffuseColor[0] = rgb[0]; materials[currentMaterial].diffuseColor[1] = rgb[1]; materials[currentMaterial].diffuseColor[2] = rgb[2]; } }else{ OBJ2VBO_ERROR ("Parsing error"); } } break; case 's' : { // diffuse color OBJ2VBO_DEBUG ("Specular color"); float rgb[3]; ss >> ws_r >> std::ws >> rgb[0] >> ws_g >> std::ws >> rgb[1] >> ws_b >> std::ws >> rgb[2]; if (!ss.fail() && isspace(ws_r) && isspace(ws_g) && isspace(ws_b)) { if(currentMaterial >=0 && currentMaterial < int(materials.size())) { materials[currentMaterial].specularColor[0] = rgb[0]; materials[currentMaterial].specularColor[1] = rgb[1]; materials[currentMaterial].specularColor[2] = rgb[2]; } }else{ OBJ2VBO_ERROR ("Parsing error"); } } break; default: OBJ2VBO_DEBUG ("Not supported"); break; } } // end 'K' break; } } } myfile.close(); return true; } int main(int argc, char *argv[]) { if(argc < 2) { cout << "Usage: ObjtoVbo input.obj [output.vbo]" << endl; return 1; } std::string filename(argv[1]); std::string output("default.vbo"); if(argc >= 3) { output = std::string(argv[2]); } OBJ2VBO_INFO("Loading OBJ file: " << filename); std::vector positionData; std::vector texCoordData; std::vector normalData; // The loader only supports triangles. Quads are // converted into two triangles. std::vector triangles; std::vector materials; std::vector materialIDs; std::vector matNames; Material defaultMat; materials.push_back(defaultMat); matNames.push_back("ObjToVboDefaultMat"); int currentMaterial = 0; std::string line; unsigned lineNumber = 0; ifstream myfile(filename.c_str()); if (!myfile.is_open()) { OBJ2VBO_ERROR ("Can not open file: " << filename); return 1; } while ( myfile.good() ) { std::getline (myfile,line); ++lineNumber; OBJ2VBO_DEBUG (line); std::istringstream ss(line); ss.unsetf(std::ios_base::skipws); // skip whitespace at beginning of line ss >> std::ws; if(!ss.eof()) { char firstChar; ss >> firstChar; switch(firstChar) { case '#' : { OBJ2VBO_DEBUG ("Found comment"); } break; case 'v' : { // vertex char nextChar, ws_tx, ws_xy, ws_yz; ss >> nextChar; if(isspace(nextChar)) { // Polygonal Vertex OBJ2VBO_DEBUG ("Polygonal Vertex"); float xyz[3]; ss >> std::ws >> xyz[0] >> ws_xy >> std::ws >> xyz[1] >> ws_yz >> std::ws >> xyz[2]; bool test = isspace(ws_xy); if (!ss.fail() && isspace(ws_xy) && isspace(ws_yz)) { positionData.push_back(xyz[0]); positionData.push_back(xyz[1]); positionData.push_back(xyz[2]); }else{ OBJ2VBO_ERROR ("Parsing error"); } }else{ switch(nextChar) { case 't' : { // Texture Vertex OBJ2VBO_DEBUG ("Texture Vertex"); float xyz[3]; ss >> ws_tx >> std::ws >> xyz[0] >> ws_xy >> std::ws >> xyz[1]; if (!ss.fail() && isspace(ws_tx) && isspace(ws_xy)) { texCoordData.push_back(xyz[0]); texCoordData.push_back(xyz[1]); texCoordData.push_back(0.0); }else{ OBJ2VBO_ERROR ("Parsing error"); } } break; case 'n' : { //Normal OBJ2VBO_DEBUG ("Normal"); float xyz[3]; ss >> ws_tx >> std::ws >> xyz[0] >> ws_xy >> std::ws >> xyz[1] >> ws_yz >> std::ws >> xyz[2]; if (ss && isspace(ws_tx) && isspace(ws_xy) && isspace(ws_yz)) { normalData.push_back(xyz[0]); normalData.push_back(xyz[1]); normalData.push_back(xyz[2]); }else{ OBJ2VBO_ERROR ("Parsing error"); } } break; default: OBJ2VBO_DEBUG ("Not supported"); break; } } } // end 'v' break; case 'g' : { // group OBJ2VBO_DEBUG ("Group"); } break; case 'f' : { // face OBJ2VBO_DEBUG ("Face"); char slash, ws; int v[4][3]; for(unsigned k=0; k < 4; k++) { v[k][0] = -1; v[k][1] = -1; v[k][2] = -1; ss >> ws >> std::ws >> v[k][0]; if(!ss.fail() && isspace(ws)) { bool firstSlashFound = false; if(ss.peek() == '/') { ss >> slash; if(!ss.fail()) firstSlashFound = true; } if(firstSlashFound) { if(ss.peek() == '/') { // format is f v//vn ss >> slash; ss >> v[k][2]; } else { // format is f v/vt/vn or v/vt ss >> v[k][1]; if(!ss.fail()) { if(ss.peek() == '/') { //format is f v/vt/vn ss >> slash >> v[k][2]; } } } } } } // end for if(v[0][0] != -1 && v[1][0] != -1 && v[2][0] != -1) { if(v[3][0] == -1) { // triangle for(unsigned k=0; k < 3; k++) { for(unsigned j=0; j < 3; j++) { triangles.push_back(v[k][j]); } } materialIDs.push_back(currentMaterial); } else { // quad // The loader only supports triangles. Quads are // converted into two triangles. for(unsigned k=0; k < 3; k++) { for(unsigned j=0; j < 3; j++) { triangles.push_back(v[k][j]); } } materialIDs.push_back(currentMaterial); for(unsigned k=0; k < 3; k++) { for(unsigned j=0; j < 3; j++) { triangles.push_back(v[(k+2)%4][j]); } } materialIDs.push_back(currentMaterial); } } } // end 'f' break; case 'm' : { // material library file ("mtllib") OBJ2VBO_DEBUG ("mtllib"); std::string mtllib; std::string mtlfile; ss >> mtllib >> std::ws >> mtlfile; if(mtllib.compare("tllib") == 0) { std::string newFileName = findFile(mtlfile, filename); loadMaterialLib(newFileName, materials, matNames); } } break; case 'u' : { // usemtl std::string usemtl; std::string mtlName; ss >> usemtl >> std::ws >> mtlName; if(usemtl.compare("semtl") == 0) { OBJ2VBO_DEBUG ("usemtl"); bool foundMat = false; if(matNames.size() != materials.size()) { OBJ2VBO_ERROR ("matNames has wrong size"); } else { for(int m = matNames.size()-1; m >=0 && !foundMat; m--) { if(mtlName.compare(matNames[m]) == 0) { currentMaterial = m; foundMat = true; } } } if(!foundMat) { OBJ2VBO_WARN ("could not find material \"" << mtlName << "\" using default material instead" ); currentMaterial = 0; } } } break; } } } myfile.close(); if(triangles.size() % 9 != 0) { OBJ2VBO_ERROR ("Parsing error"); return 1; } unsigned noTriangles = triangles.size() / 9; if(noTriangles != materialIDs.size()) { OBJ2VBO_ERROR ("materialIDs has wrong size"); }; OBJ2VBO_INFO("Number of triangles = " << noTriangles); OBJ2VBO_INFO("Number of vertices = " << positionData.size() / 3); OBJ2VBO_INFO("Number of normals = " << normalData.size() / 3); OBJ2VBO_INFO("Number of texture coordinates = " << texCoordData.size() / 3); OBJ2VBO_INFO("Number of materials = " << materials.size()-1); if(normalize) { normalizePositions(positionData, boundingSize); } std::vector vbo; int noElement = sizeof(includedPerVertexData)/sizeof(int); for(unsigned t=0; t < noTriangles; t++) { for(unsigned k=0; k < 3; k++) { int mId = materialIDs[t]; Material &m = materials[mId]; for(int i=0; i < noElement; i++) { switch(includedPerVertexData[i]) { case POS_3f : // position x,y,z (3 floats) { int v = triangles[t*9 + k*3] - 1; // -1 because they start with 1 not 0 in the file if(v >=0 && (v*3) < (int)positionData.size()) { vbo.push_back(positionData[v*3 + 0]); vbo.push_back(positionData[v*3 + 1]); vbo.push_back(positionData[v*3 + 2]); }else{ if(v+1 != -1) { OBJ2VBO_ERROR ("Parsing error"); } vbo.push_back(0.0f); vbo.push_back(0.0f); vbo.push_back(0.0f); } } break; case NORMAL_3f : // normal (3 floats) { int n = triangles[t*9 + k*3 + 2] - 1; // -1 because they start with 1 not 0 in the file if(n >=0 && (n*3) < (int)normalData.size()) { vbo.push_back(normalData[n*3 + 0]); vbo.push_back(normalData[n*3 + 1]); vbo.push_back(normalData[n*3 + 2]); }else{ if(n+1 != -1) { OBJ2VBO_ERROR ("Parsing error"); } vbo.push_back(0.0f); vbo.push_back(0.0f); vbo.push_back(0.0f); } } break; case TEXCOORD_2f : // texture coordinates (2 floats) { int vt = triangles[t*9 + k*3 + 1] - 1; // -1 because they start with 1 not 0 in the file; if(vt >=0 && (vt*3) < (int)texCoordData.size()) { vbo.push_back(texCoordData[vt*3 + 0]); vbo.push_back(texCoordData[vt*3 + 1]); }else{ if(vt+1 != -1) { OBJ2VBO_ERROR ("Parsing error"); } vbo.push_back(0.0f); vbo.push_back(0.0f); } } break; case DIFFUSE_3f : // diffuse color (3 floats) vbo.push_back(m.diffuseColor[0]); vbo.push_back(m.diffuseColor[1]); vbo.push_back(m.diffuseColor[2]); break; case SPECULAR_3f : // specular color (3 floats) vbo.push_back(m.specularColor[0]); vbo.push_back(m.specularColor[1]); vbo.push_back(m.specularColor[2]); break; case AMBIENT_3f : // ambient color (3 floats) vbo.push_back(m.ambientColor[0]); vbo.push_back(m.ambientColor[1]); vbo.push_back(m.ambientColor[2]); break; case SPEC_EXP_1f : // specular exponent (1 float) vbo.push_back(m.specularExponent); break; case ALPHA_1f : // alpha value (1 float) vbo.push_back(m.alpha); break; case MAT_ID_1f : // internal material id (1 float) vbo.push_back(float(mId)); break; case FILL1_1f : // 1.0 (1 float) vbo.push_back(1.0f); break; case FILL0_1f : // 0.0 (1 float) vbo.push_back(0.0f); break; default: { OBJ2VBO_ERROR ("Unknown per-vertex data type"); } break; } } } } if(writeBinary) { ofstream outfile(output.c_str(), ofstream::out | ofstream::binary); if (!outfile.is_open()) { OBJ2VBO_ERROR ("could not open output file \"" << output << "\""); return 1; } unsigned int numFloats = vbo.size(); int byteCount = numFloats * sizeof(float); outfile.write((char*)(&numFloats), sizeof(unsigned int)); outfile.write( (char*)(&vbo[0]), byteCount); OBJ2VBO_INFO (numFloats << " float values written to \"" << output << "\" in binary format (4 + " << byteCount << " bytes)." ); outfile.close(); } else { ofstream outfile(output.c_str()); if (!outfile.is_open()) { OBJ2VBO_ERROR ("could not open output file \"" << output << "\""); return 1; } unsigned int numFloats = vbo.size(); outfile << numFloats; for(unsigned i=0; i < vbo.size(); i++) { outfile << separator; outfile << vbo[i]; } OBJ2VBO_INFO (numFloats << " float values written to \"" << output << "\" as plain text." ); outfile.close(); } return 0; }