// Copyright 2009-2021 Intel Corporation // SPDX-License-Identifier: Apache-2.0 #include "obj_loader.h" // #include "texture.h" namespace embree { /*! Three-index vertex, indexing start at 0, -1 means invalid vertex. */ struct Vertex { unsigned int v, vt, vn; Vertex() { }; Vertex(unsigned int v) : v(v), vt(v), vn(v) { }; Vertex(unsigned int v, unsigned int vt, unsigned int vn) : v(v), vt(vt), vn(vn) { }; }; struct Crease { float w; unsigned int a, b; Crease() : w(0), a(-1), b(-1) { }; Crease(float w, unsigned int a, unsigned int b) : w(w), a(a), b(b) { }; }; static inline bool operator <(const Vertex& a, const Vertex& b) { if (a.v != b.v) return a.v < b.v; if (a.vn != b.vn) return a.vn < b.vn; if (a.vt != b.vt) return a.vt < b.vt; return false; } /*! Fill space at the end of the token with 0s. */ static inline const char* trimEnd(const char* token) { if (*token == 0) return token; char* pe = (char *) token; while (*pe) pe++; pe--; while ((*pe == ' ' || *pe == '\t' || *pe == '\r') && pe >= token) *pe-- = 0; return token; } /*! Determine if character is a separator. */ static inline bool isSep(const char c) { return (c == ' ') || (c == '\t'); } /*! Parse separator. */ static inline const char* parseSep(const char*& token) { size_t sep = strspn(token, " \t"); if (!sep) THROW_RUNTIME_ERROR("separator expected"); return token += sep; } /*! Parse optional separator. */ static inline const char* parseSepOpt(const char*& token) { return token += strspn(token, " \t"); } /*! Read float from a string. */ static inline float getFloat(const char*& token) { token += strspn(token, " \t"); float n = (float) atof(token); token += strcspn(token, " \t\r"); return n; } /*! Read int from a string. */ static inline int getInt(const char*& token) { token += strspn(token, " \t"); int n = atoi(token); token += strcspn(token, " \t\r"); return n; } /*! Read Vec2f from a string. */ static inline Vec2f getVec2f(const char*& token) { float x = getFloat(token); float y = getFloat(token); return Vec2f(x, y); } /*! Read Vec3f from a string. */ static inline Vec3f getVec3f(const char*& token) { float x = getFloat(token); token += strspn(token, " \t"); if (*token == 0) return Vec3f(x); float y = getFloat(token); float z = getFloat(token); return Vec3f(x, y, z); } /*! Read Vec3fa from a string. */ static inline Vec3fa getVec3fa(const char*& token) { float x = getFloat(token); token += strspn(token, " \t"); if (*token == 0) return Vec3f(x); float y = getFloat(token); float z = getFloat(token); return Vec3fa(x, y, z); } class OBJLoader { public: /*! Constructor. */ OBJLoader(const FileName& fileName, const bool subdivMode, const bool combineIntoSingleObject); /*! output model */ Ref group; private: /*! file to load */ FileName path; /*! load only quads and ignore triangles */ bool subdivMode; /*! Geometry buffer. */ avector v; avector vn; std::vector vt; std::vector ec; std::vector > curGroup; std::vector > curGroupHair; /*! Material handling. */ std::string curMaterialName; Ref curMaterial; std::map > material; // std::map> textureMap; private: void loadMTL(const FileName& fileName); unsigned int fix_v(int index); unsigned int fix_vt(int index); unsigned int fix_vn(int index); void flushFaceGroup(); void flushTriGroup(); // void flushHairGroup(); Vertex getUInt3(const char*& token); uint32_t getVertex(std::map& vertexMap, Ref mesh, const Vertex& i); // std::shared_ptr loadTexture(const FileName& fname); }; OBJLoader::OBJLoader(const FileName& fileName, const bool subdivMode, const bool combineIntoSingleObject) : group(new SceneGraph::GroupNode), path(fileName.path()), subdivMode(subdivMode) { /* open file */ std::ifstream cin; cin.open(fileName.c_str()); if (!cin.is_open()) { THROW_RUNTIME_ERROR("cannot open " + fileName.str()); return; } /* generate default material */ Ref defaultMaterial = new OBJMaterial("default"); curMaterialName = "default"; curMaterial = defaultMaterial; while (cin.peek() != -1) { /* load next multiline */ std::string line; std::getline(cin, line); while (!line.empty() && line[line.size() - 1] == '\\') { line[line.size() - 1] = ' '; std::string next_line; std::getline(cin, next_line); if (next_line.empty()) break; line += next_line; } const char* token = trimEnd(line.c_str() + strspn(line.c_str(), " \t")); if (token[0] == 0) continue; /*! parse position */ if (token[0] == 'v' && isSep(token[1])) { v.push_back(getVec3f(token += 2)); continue; } /* parse normal */ if (token[0] == 'v' && token[1] == 'n' && isSep(token[2])) { vn.push_back(getVec3f(token += 3)); continue; } /* parse texcoord */ if (token[0] == 'v' && token[1] == 't' && isSep(token[2])) { vt.push_back(getVec2f(token += 3)); continue; } /*! parse face */ if (token[0] == 'f' && isSep(token[1])) { parseSep(token += 1); std::vector face; while (token[0]) { Vertex vtx = getUInt3(token); face.push_back(vtx); parseSepOpt(token); } curGroup.push_back(face); continue; } /*! parse corona hair */ if (!strncmp(token, "hair", 4) && isSep(token[4])) { throw std::runtime_error("corona hair is deimplemented"); } /*! parse edge crease */ if (token[0] == 'e' && token[1] == 'c' && isSep(token[2])) { parseSep(token += 2); float w = getFloat(token); parseSepOpt(token); unsigned int a = fix_v(getInt(token)); parseSepOpt(token); unsigned int b = fix_v(getInt(token)); parseSepOpt(token); ec.push_back(Crease(w, a, b)); continue; } /*! use material */ if (!strncmp(token, "usemtl", 6) && isSep(token[6])) { if (!combineIntoSingleObject) flushFaceGroup(); std::string name(parseSep(token += 6)); if (material.find(name) == material.end()) { curMaterial = defaultMaterial; curMaterialName = "default"; } else { curMaterial = material[name]; curMaterialName = name; } continue; } /* load material library */ if (!strncmp(token, "mtllib", 6) && isSep(token[6])) { loadMTL(path + std::string(parseSep(token += 6))); continue; } // ignore unknown stuff } flushFaceGroup(); cin.close(); } struct ExtObjMaterial { public: enum Type { NONE, MATTE, GLASS, METAL, METALLIC_PAINT }; Ref select() const { // std::shared_ptr nulltex; if (type == NONE) { // return new OBJMaterial(d,map_d,Ka,map_Ka,Kd,map_Kd,Ks,map_Ks,Kt,map_Kt,Ns,map_Ns,map_Displ); return new OBJMaterial(d, Ka, Kd, Ks, Kt, Ns); } else if (type == MATTE) { if (coat_eta != 1.0f) return new MetallicPaintMaterial(Kd, zero, 0.0f, eta.x); else // return new OBJMaterial(1.0f,nulltex,zero,nulltex,Kd,map_Kd,Ks,nulltex,zero,nulltex,1.0f/(1E-6f+roughness),nulltex,nulltex); return new OBJMaterial(1.0f, zero, Kd, Ks, zero, 1.0f / (1E-6f + roughness)); } else if (type == GLASS) { return new ThinDielectricMaterial(Vec3fa(1.0f), eta.x, 0.1f); } else if (type == METAL) { if (roughness == 0.0f) { if (eta == Vec3fa(1.0f) && k == Vec3fa(0.0f)) return new MirrorMaterial(Kd); else return new MetalMaterial(Kd, eta, k); } else return new MetalMaterial(Kd, eta, k, roughness); } else if (type == METALLIC_PAINT) { return new MetallicPaintMaterial(Kd, Ks, 0.0f, coat_eta); } return new MatteMaterial(Vec3fa(0.5f)); } public: Type type = NONE; int illum = 0; /*< illumination model */ float d = 1.0f; /*< dissolve factor, 1=opaque, 0=transparent */ float Ns = 1.0f; /*< specular exponent */ float Ni = 1.0f; /*< optical density for the surface (index of refraction) */ Vec3fa Ka = zero; /*< ambient reflectivity */ Vec3fa Kd = one; /*< diffuse reflectivity */ Vec3fa Ks = zero; /*< specular reflectivity */ // Vec3fa Kt = zero; /*< transmission filter */ Vec3fa Kt = one; /*< transmission filter */ // ^ Because many textures were deleted // std::shared_ptr map_d; /*< d texture */ // std::shared_ptr map_Ka; /*< Ka texture */ // std::shared_ptr map_Kd; /*< Kd texture */ // std::shared_ptr map_Ks; /*< Ks texture */ // std::shared_ptr map_Kt; /*< Kt texture */ // std::shared_ptr map_Ns; /*< Ns texture */ // std::shared_ptr map_Displ; /*< Displ texture */ float roughness = zero; // std::shared_ptr roughnessMap; float coat_eta = one; float coat_roughness = zero; // std::shared_ptr coat_roughnessMap; float bump = zero; Vec3f eta = Vec3f(1.4f); Vec3f k = Vec3f(3.0f); }; // std::shared_ptr OBJLoader::loadTexture(const FileName& fname) // { // if (textureMap.find(fname.str()) != textureMap.end()) // return textureMap[fname.str()]; // return std::shared_ptr(Texture::load(path+fname)); // } /* load material file */ void OBJLoader::loadMTL(const FileName& fileName) { std::ifstream cin; cin.open(fileName.c_str()); if (!cin.is_open()) { std::cerr << "cannot open " << fileName.str() << std::endl; return; } std::string name; ExtObjMaterial cur; while (cin.peek() != -1) { /* load next multiline */ std::string line; std::getline(cin, line); while (!line.empty() && line[line.size() - 1] == '\\') { line[line.size() - 1] = ' '; std::string next_line; std::getline(cin, next_line); if (next_line.empty()) break; line += next_line; } const char* token = trimEnd(line.c_str() + strspn(line.c_str(), " \t")); if (token[0] == 0) continue; // ignore empty lines if (token[0] == '#') continue; // ignore comments if (!strncmp(token, "newmtl", 6)) { if (name != "") { material[name] = cur.select(); material[name]->name = name; } parseSep(token += 6); name = token; cur = ExtObjMaterial(); continue; } if (name == "") THROW_RUNTIME_ERROR("invalid material file: newmtl expected first"); try { if (!strncmp(token, "illum", 5)) { parseSep(token += 5); continue; } if (!strncmp(token, "d", 1)) { parseSep(token += 1); cur.d = getFloat(token); continue; } if (!strncmp(token, "Ns", 2)) { parseSep(token += 2); cur.Ns = getFloat(token); continue; } if (!strncmp(token, "Ni", 2)) { parseSep(token += 2); cur.Ni = getFloat(token); continue; } // if (!strncmp(token, "map_d", 5) || !strncmp(token, "d_map", 5)) { // parseSep(token += 5); // cur.map_d = loadTexture(FileName(token)); // continue; // } // if (!strncmp(token, "map_Ka", 6) || !strncmp(token, "Ka_map", 6)) { // parseSep(token += 6); // cur.map_Ka = loadTexture(FileName(token)); // continue; // } // if (!strncmp(token, "map_Kd", 6) || !strncmp(token, "Kd_map", 6)) { // parseSep(token += 6); // cur.map_Kd = loadTexture(FileName(token)); // continue; // } // if (!strncmp(token, "map_Ks", 6) || !strncmp(token, "Ks_map", 6)) { // parseSep(token += 6); // cur.map_Ks = loadTexture(FileName(token)); // continue; // } // if (!strncmp(token, "map_Kt", 6) || !strncmp(token, "Kt_map", 6)) { // parseSep(token += 6); // cur.map_Kt = loadTexture(FileName(token)); // continue; // } // if (!strncmp(token, "map_Tf", 6) || !strncmp(token, "Tf_map", 6)) { // parseSep(token += 6); // cur.map_Kt = loadTexture(FileName(token)); // continue; // } // if (!strncmp(token, "map_Ns", 6) || !strncmp(token, "Ns_map", 6)) { // parseSep(token += 6); // cur.map_Ns = loadTexture(FileName(token)); // continue; // } // if (!strncmp(token, "map_Displ", 9) || !strncmp(token, "Displ_map", 9)) { // parseSep(token += 9); // cur.map_Displ = loadTexture(FileName(token)); // continue; // } if (!strncmp(token, "Ka", 2)) { parseSep(token += 2); cur.Ka = getVec3f(token); continue; } if (!strncmp(token, "Kd", 2)) { parseSep(token += 2); cur.Kd = getVec3f(token); continue; } if (!strncmp(token, "Ks", 2)) { parseSep(token += 2); cur.Ks = getVec3f(token); continue; } if (!strncmp(token, "Kt", 2)) { parseSep(token += 2); cur.Kt = getVec3f(token); continue; } if (!strncmp(token, "Tf", 2)) { parseSep(token += 2); cur.Kt = getVec3f(token); continue; } /* extended OBJ */ if (!strncmp(token, "type", 4)) { parseSep(token += 4); std::string type = token; if (type == "matte") cur.type = ExtObjMaterial::MATTE; else if (type == "glass") cur.type = ExtObjMaterial::GLASS; else if (type == "metal") cur.type = ExtObjMaterial::METAL; else if (type == "metallicPaint") cur.type = ExtObjMaterial::METALLIC_PAINT; else if (type == "principled") cur.type = ExtObjMaterial::NONE; else if (type == "luminous") cur.type = ExtObjMaterial::NONE; continue; } /* OSPRay principled material extensions */ if (!strncmp(token, "baseColor", 9)) { parseSep(token += 9); cur.Kd = getVec3f(token); continue; } if (!strncmp(token, "ior", 3)) { parseSep(token += 3); cur.Ni = getFloat(token); continue; } // if (!strncmp(token, "map_baseColor", 13)) { parseSep(token += 13); cur.map_Kd = loadTexture(FileName(token)); continue; } // if (!strncmp(token, "map_specular", 12)) { parseSep(token += 12); cur.map_Ks = loadTexture(FileName(token)); continue; } if (!strncmp(token, "map_normal.scale", 16)) { parseSep(token += 16); continue; } // if (!strncmp(token, "map_normal", 10)) { parseSep(token += 10); cur.map_Displ = loadTexture(FileName(token)); continue; } if (!strncmp(token, "transmissionColor", 17)) { parseSep(token += 17); cur.Kt = getVec3f(token); continue; } if (!strncmp(token, "transmission", 12)) { parseSep(token += 12); cur.Kt = Vec3f(getFloat(token)); continue; } // if (!strncmp(token, "roughnessMap", 12)) { parseSep(token += 12); cur.roughnessMap = loadTexture(FileName(token)); continue; } if (!strncmp(token, "roughness", 9)) { parseSep(token += 9); cur.roughness = getFloat(token); cur.Ns = 1.0f / (cur.roughness + 1E-6f); continue; } // if (!strncmp(token, "colorMap", 8)) { parseSep(token += 8); cur.map_Kd = loadTexture(FileName(token)); continue; } if (!strncmp(token, "color", 5)) { parseSep(token += 5); cur.Kd = getVec3f(token); continue; } if (!strncmp(token, "coat.color", 10)) { parseSep(token += 10); cur.Kd = getVec3f(token); continue; } if (!strncmp(token, "coat.eta", 8)) { parseSep(token += 8); cur.coat_eta = getFloat(token); continue; } // if (!strncmp(token, "coat.roughnessMap",17)) { parseSep(token += 17); cur.coat_roughnessMap = loadTexture(FileName(token)); continue; } if (!strncmp(token, "coat.roughness", 14)) { parseSep(token += 14); cur.coat_roughness = getFloat(token); continue; } // if (!strncmp(token, "bumpMap", 7)) { parseSep(token += 7); cur.map_Displ = loadTexture(FileName(token)); continue; } if (!strncmp(token, "bump", 4)) { parseSep(token += 4); cur.bump = getFloat(token); continue; } if (!strncmp(token, "eta", 3)) { parseSep(token += 3); cur.eta = getVec3f(token); continue; } if (!strncmp(token, "k", 1)) { parseSep(token += 1); cur.k = getVec3f(token); continue; } } catch (const std::runtime_error& e) { std::cerr << "Error: " << e.what() << std::endl; } } if (name != "") { material[name] = cur.select(); material[name]->name = name; } cin.close(); } /*! handles relative indices and starts indexing from 0 */ unsigned int OBJLoader::fix_v(int index) { return (index > 0 ? index - 1 : (index == 0 ? 0 : (int) v.size() + index)); } unsigned int OBJLoader::fix_vt(int index) { return (index > 0 ? index - 1 : (index == 0 ? 0 : (int) vt.size() + index)); } unsigned int OBJLoader::fix_vn(int index) { return (index > 0 ? index - 1 : (index == 0 ? 0 : (int) vn.size() + index)); } /*! Parse differently formatted triplets like: n0, n0/n1/n2, n0//n2, n0/n1. */ /*! All indices are converted to C-style (from 0). Missing entries are assigned -1. */ Vertex OBJLoader::getUInt3(const char*& token) { Vertex v(-1); v.v = fix_v(atoi(token)); token += strcspn(token, "/ \t\r"); if (token[0] != '/') return (v); token++; // it is i//n if (token[0] == '/') { token++; v.vn = fix_vn(atoi(token)); token += strcspn(token, " \t\r"); return (v); } // it is i/t/n or i/t v.vt = fix_vt(atoi(token)); token += strcspn(token, "/ \t\r"); if (token[0] != '/') return (v); token++; // it is i/t/n v.vn = fix_vn(atoi(token)); token += strcspn(token, " \t\r"); return (v); } uint32_t OBJLoader::getVertex(std::map& vertexMap, Ref mesh, const Vertex& i) { const std::map::iterator& entry = vertexMap.find(i); if (entry != vertexMap.end()) return (entry->second); if (i.v >= v.size()) std::cout << "WARNING: corrupted OBJ file" << std::endl; else mesh->positions[0].push_back(v[i.v]); if (i.vn != -1) { while (mesh->normals[0].size() < mesh->positions[0].size()) mesh->normals[0].push_back(zero); // some vertices might not had a normal if (i.vn >= vn.size()) std::cout << "WARNING: corrupted OBJ file" << std::endl; else mesh->normals[0][mesh->positions[0].size() - 1] = vn[i.vn]; } if (i.vt != -1) { while (mesh->texcoords.size() < mesh->positions[0].size()) mesh->texcoords.push_back(zero); // some vertices might not had a texture coordinate if (i.vt >= vt.size()) std::cout << "WARNING: corrupted OBJ file" << std::endl; else mesh->texcoords[mesh->positions[0].size() - 1] = vt[i.vt]; } return (vertexMap[i] = (unsigned int) (mesh->positions[0].size()) - 1); } void OBJLoader::flushFaceGroup() { flushTriGroup(); // flushHairGroup(); } /*! end current facegroup and append to mesh */ void OBJLoader::flushTriGroup() { if (curGroup.empty()) return; if (subdivMode) { throw std::runtime_error("Deimplemented"); // Ref mesh = new SceneGraph::SubdivMeshNode(curMaterial,BBox1f(0,1),1); // mesh->normals.resize(1); // group->add(mesh.cast()); // for (size_t i=0; ipositions[0].push_back(v[i]); // for (size_t i=0; inormals[0].push_back(vn[i]); // for (size_t i=0; itexcoords.push_back(vt[i]); // for (size_t i=0; iedge_creases.push_back(Vec2i(ec[i].a, ec[i].b)); // mesh->edge_crease_weights.push_back(ec[i].w); // } // for (size_t j=0; j& face = curGroup[j]; // mesh->verticesPerFace.push_back(int(face.size())); // for (size_t i=0; iposition_indices.push_back(face[i].v); // } // if (mesh->normals[0].size() == 0) // mesh->normals.clear(); // mesh->verify(); } else { Ref mesh = new SceneGraph::TriangleMeshNode(curMaterial, BBox1f(0, 1), 1); mesh->normals.resize(1); group->add(mesh.cast()); // merge three indices into one std::map vertexMap; for (size_t j = 0; j < curGroup.size(); j++) { /* iterate over all faces */ const std::vector& face = curGroup[j]; /* triangulate the face with a triangle fan */ Vertex i0 = face[0], i1 = Vertex(-1), i2 = face[1]; for (size_t k = 2; k < face.size(); k++) { i1 = i2; i2 = face[k]; uint32_t v0, v1, v2; v0 = getVertex(vertexMap, mesh, i0); v1 = getVertex(vertexMap, mesh, i1); v2 = getVertex(vertexMap, mesh, i2); assert(v0 < mesh->numVertices()); assert(v1 < mesh->numVertices()); assert(v2 < mesh->numVertices()); mesh->triangles.push_back(SceneGraph::TriangleMeshNode::Triangle(v0, v1, v2)); } } /* there may be vertices without normals or texture coordinates, thus we have to make these arrays the same size here */ if (mesh->normals[0].size()) while (mesh->normals[0].size() < mesh->numVertices()) mesh->normals[0]. push_back(zero); if (mesh->texcoords.size()) while (mesh->texcoords.size() < mesh->numVertices()) mesh->texcoords. push_back(zero); if (mesh->normals[0].size() == 0) mesh->normals.clear(); mesh->verify(); } curGroup.clear(); ec.clear(); } Ref loadOBJ(const FileName& fileName, const bool subdivMode, const bool combineIntoSingleObject) { OBJLoader loader(fileName, subdivMode, combineIntoSingleObject); return loader.group.cast(); } }