From 77de8df2bc17e7dfb76858d047bef7184184e4a3 Mon Sep 17 00:00:00 2001 From: Squareys Date: Wed, 7 Jun 2017 17:11:26 +0200 Subject: [PATCH] WIP Signed-off-by: Squareys --- src/MagnumPlugins/ObjImporter/ObjImporter.cpp | 841 ++++++++++++------ src/MagnumPlugins/ObjImporter/ObjImporter.h | 7 +- src/MagnumPlugins/ObjImporter/Test/Test.cpp | 80 +- .../ObjImporter/Test/moreMeshes.obj | 4 +- .../ObjImporter/Test/multiMaterial.obj | 18 +- .../ObjImporter/Test/normals.obj | 8 +- .../Test/textureCoordinatesNormals.obj | 9 +- .../ObjImporter/Test/unnamedFirstMesh.obj | 2 + 8 files changed, 664 insertions(+), 305 deletions(-) diff --git a/src/MagnumPlugins/ObjImporter/ObjImporter.cpp b/src/MagnumPlugins/ObjImporter/ObjImporter.cpp index 97b2d0e63..ff7dede1c 100644 --- a/src/MagnumPlugins/ObjImporter/ObjImporter.cpp +++ b/src/MagnumPlugins/ObjImporter/ObjImporter.cpp @@ -29,6 +29,7 @@ #include #include #include +#include #include #include #include @@ -40,79 +41,166 @@ #include "Magnum/Math/Vector3.h" #include "Magnum/Math/Color.h" #include "Magnum/Trade/MeshData3D.h" +#include "Magnum/Trade/MeshObjectData3D.h" #include "Magnum/Trade/ImageData.h" #include "Magnum/Trade/PhongMaterialData.h" #include "MagnumPlugins/TgaImporter/TgaImporter.h" -#if defined(CORRADE_TARGET_NACL_NEWLIB) || defined(CORRADE_TARGET_ANDROID) -#include -#endif - using namespace Corrade::Containers; namespace Magnum { namespace Trade { -// Not using PhongMaterialData, since we may want to set color and texture to -// later decide which flags we set. We do not know whether we have a texture -// or color beforehand. +/* Not using PhongMaterialData, since we may want to set color and texture to + * later decide which flags we set. We do not know whether we have a texture + * or color beforehand. */ struct ObjMaterial { std::string name; Color3 ambient; Color3 diffuse; Color3 specular; Float specularity; - UnsignedInt ambientTexture = -1; - UnsignedInt diffuseTexture = -1; - UnsignedInt specularTexture = -1; + Int ambientTexture = -1; + Int diffuseTexture = -1; + Int specularTexture = -1; +}; + +struct ObjObject; +struct ObjGroup; +struct ObjMesh; + +struct ObjMeshData { + ObjGroup& group; + int materialId; /* from 'usemtl' keyword */ + + ObjMesh* points = nullptr; + ObjMesh* lines = nullptr; + ObjMesh* faces = nullptr; + + ObjMeshData()=delete; + ObjMeshData(ObjGroup& g, int matId): group(g), materialId(matId) {} }; -// Semantic object for a set of indices belonging to a mesh. +struct ObjGroup { + ObjObject& object; + std::string name; /* from 'g' keyword */ + + std::vector> meshes; + std::unordered_map meshPerMaterial; + + ObjGroup()=delete; + ObjGroup(ObjObject& o): object(o) {} + + /* Create or get mesh for given material id */ + ObjMeshData& meshDataForMaterial(int materialId) { + if(meshPerMaterial.find(materialId) != meshPerMaterial.end()) { + return *meshes[meshPerMaterial[materialId]]; + } else { + meshPerMaterial[materialId] = meshes.size(); + meshes.emplace_back(new ObjMeshData{*this, materialId}); + ObjMeshData& mesh = *meshes.back(); + return mesh; + } + } +}; + +struct ObjObject { + std::string name; /* from 'o' keyword */ + std::vector> groups; + + ObjObject(): name{""} {} + ObjObject(ArrayView name): name{name.data(), name.size()} {} +}; + +/* An intermediate object representing a mesh and it's properties as well as + * where to find the data associated to it */ struct ObjMesh { - std::vector> indices; + ObjMeshData& data; /* parent object containing data shared meshes for different primitives */ + MeshPrimitive primitive; + + /* Sections of the file belonging to this mesh */ + std::vector> sections; + + int minPrimitives = 0; /* For smarter vector memory allocation later */ + + ObjMesh(ObjMeshData& d, MeshPrimitive p): data(d), primitive(p) {} + + std::string name() { + std::string name = data.group.object.name; + + if(!data.group.name.empty()) { + name += ":" + data.group.name; + } + + if(data.materialId != -1) { + name += "$" + std::to_string(data.materialId); + } + + const int numPrimitiveTypes = + ((data.points) ? 1 : 0) + + ((data.lines) ? 1 : 0) + + ((data.faces) ? 1 : 0); + if(numPrimitiveTypes > 1) { + std::ostringstream out; + Debug(&out) << primitive; + name += "%" + out.str(); + } + + return name; + } }; -// Kinda misleading: *Not* similar to the OpenGexImporter Importer State. -// This just holds all the members of the importer to 1. avoid huge includes -// in the header and 2. not have to switch file when adding new ones (I needed -// to be done quickly ;) ) + +/* The state of the imported generated by openData() */ struct ImporterState { - std::vector positions; - std::vector textureCoordinates; - std::vector normals; std::vector materials; - std::vector meshMaterials; - std::unordered_map materialIndices; - std::vector> meshes; + std::unordered_map materialIds; + std::vector textures; - std::unordered_map textureIndices; + std::unordered_map textureIds; + + std::vector> objects; + + std::vector> meshes; + std::unordered_map meshIds; + + std::vector meshlessObjects; + std::unordered_map meshlessObjectIds; + + std::vector positions; + std::vector texCoords; + std::vector normals; }; namespace { -// entire contents of data in a std::string -std::string arrayToString(const Containers::ArrayView data) { - /* Propagate errors */ - if(!data) return ""; - - if(data.empty() ) { - return ""; +int strToInt(const ArrayView str) { + char* err; + const int i = int(strtol(str.data(), &err, 10)); + if(err == nullptr) { + Error() << "Trade::ObjImporter::mesh3D(): error while converting numeric data"; + return 0; } - std::string out; + return i; +} - for(const char* i = data; i != data.end(); ++i) { - const char c = *i; - out += c; +float strToFloat(const ArrayView str) { + char* err; + const float f = strtof(str.data(), &err); + if(err == nullptr) { + Error() << "Trade::ObjImporter::mesh3D(): error while converting numeric data"; + return 0; } - return out; + return f; } // find first index of c in pos, cancel search at newline or whitespace if flag set. -int findNext(const ArrayView& pos, char c, bool termByNewline=false, bool termByWhitespace=false) { - for(int i = 0; i < pos.size(); ++i) { +int findNext(const ArrayView pos, char c, bool termByNewline=false, bool termByWhitespace=false) { + const int size = pos.size(); + for(int i = 0; i < size; ++i) { if(pos[i] == c) { return i; } @@ -125,56 +213,116 @@ int findNext(const ArrayView& pos, char c, bool termByNewline=false, bool } // find next non-whitespace char and return suffix at that point. -// param strict if "true", '\n' and '\r' are not considered whitespace -ArrayView skipWhitespaces(const ArrayView& pos, bool strict=false) { - for(int i = 0; i < pos.size(); ++i) { +// param newlineIsWhitespace if "true", '\n' and '\r' are skipped also +ArrayView skipWhitespaces(const ArrayView pos, bool newlineIsWhitespace=true) { + const int size = pos.size(); + for(int i = 0; i < size; ++i) { const char c = pos[i]; - if(c != ' ' && c != '\t' && (strict || (c != '\n' && c != '\r'))) { + if(c != ' ' && c != '\t' && c != '\0' && (!newlineIsWhitespace || (c != '\n' && c != '\r'))) { return pos.suffix(i); } } - return ArrayView{}; + return {}; } -// returns suffix from after next '\n' -ArrayView ignoreLine(const ArrayView& pos) { +// returns suffix after next '\n' +ArrayView ignoreLine(const ArrayView pos) { return pos.suffix(findNext(pos, '\n') + 1); } // same as ignoreLine, but also returns the content "ignored". // Different description: Get string until next '\n' and return ArrayView of data // after it. -std::tuple> nextLine(const ArrayView& pos) { +std::tuple, ArrayView> nextLine(const ArrayView& pos) { int i = Math::min(findNext(pos, '\n'), findNext(pos, '\r')); - if(i == -1) { + if(i == -1) { i = pos.size(); - std::string str = arrayToString(pos.prefix(i)); - return std::make_tuple(str, pos.suffix(i)); + return std::make_tuple(pos.prefix(i), pos.suffix(i)); + } + return std::make_tuple(pos.prefix(i), pos.suffix(i+1)); +} + +bool atEndOfLine(const ArrayView pos) { + int i = 0; + while(pos[i] == ' ') ++i; + + return (pos[i] == '\n' || pos[i] == '\r'); +} + +// get string of content until next ' ' and also return ArrayView for data after the word. +std::tuple, ArrayView> nextWord(const ArrayView pos) { + int i = 0; + const int size = pos.size(); + for(; i < size; ++i) { + if(pos[i] == ' ' || pos[i] == '\r' || pos[i] == '\n' || pos[i] == '\0') { + break; + } } - std::string str = arrayToString(pos.prefix(i)); - return std::make_tuple(str, pos.suffix(i+1)); + return std::make_tuple(pos.prefix(i), pos.suffix(i)); +} + +std::tuple, ArrayView> parseLine(const ArrayView pos) { + std::array indices{0, 0}; + ArrayView endpos{}; + + int i = findNext(pos, '/', true, true); + if(i == -1) { + /* v1 v2 rather than v1/t1 v2/t2 or v1/ v2/ */ + ArrayView word; + std::tie(word, endpos) = nextWord(pos); + indices[0] = strToInt(word); + return std::make_tuple(indices, endpos); + } + + indices[0] = strToInt(pos.prefix(i)); + endpos = pos.suffix(i+1); + + i = findNext(endpos, ' ', true, true); /* Texture coordinates are not terminated by '/', but ' ' or newline */ + if(i == -1) + i = findNext(endpos, '\r', true, true); + if(i == -1) + i = findNext(endpos, '\n', true, true); + + if(i != -1) { /* There may not be a texCoord! Eg. "1/" */ + auto prefix = endpos.prefix(i); + if(!prefix.empty()) { + indices[1] = strToInt(prefix); + endpos = endpos.suffix(i); + } + } + + return std::make_tuple(indices, endpos); } // Parse a "v/n/t" string to indices // Warning: I'm not handing cases like "v/n", where the normal is not terminated by '/', but ' '! // Same for "v", where even the normal is omitted. -std::tuple, ArrayView> parseVertex(const ArrayView& pos) { - std::array indices{-1, -1, -1}; +std::tuple, ArrayView> parseVertex(const ArrayView pos) { + std::array indices{0, 0, 0}; + ArrayView endpos{}; + + int i = findNext(pos, '/', true, true); + if(i == -1) { + /* v1 v2 rather than v1/t1 v2/t2 or v1/ v2/ */ + ArrayView word; + std::tie(word, endpos) = nextWord(pos); + indices[0] = strToInt(word); + return std::make_tuple(indices, endpos); + } - int i = findNext(pos, '/'); - indices[0] = stoi(arrayToString(pos.prefix(i))); - auto endpos = pos.suffix(i+1); + indices[0] = strToInt(pos.prefix(i)); + endpos = pos.suffix(i+1); i = findNext(endpos, '/', true, true); - if(i != -1) { // there may not be a normal! Eg. "1//2" + if(i != -1) { /* there may not be a normal! Eg. "1//2", in which case the indices of the / are 1 apart */ auto prefix = endpos.prefix(i); if(!prefix.empty()) { - indices[1] = stoi(arrayToString(prefix)); - endpos = endpos.suffix(i+1); + indices[1] = strToInt(prefix); } + endpos = endpos.suffix(i+1); } - i = findNext(endpos, ' ', true, true); // texture coordinates are not terminated by '/', but ' ' or newline + i = findNext(endpos, ' ', true, true); /* texture coordinates are not terminated by '/', but ' ' or newline */ if(i == -1) i = findNext(endpos, '\r', true, true); if(i == -1) @@ -183,24 +331,35 @@ std::tuple, ArrayView> parseVertex(const ArrayView> nextWord(const ArrayView& pos) { - int i = 0; - for(; i < pos.size(); ++i) { - if(pos[i] == ' ' || pos[i] == '\r' || pos[i] == '\n') { - break; - } +template +ArrayView getVector(ArrayView pos, Math::Vector& v) { + ArrayView word; + + for(int i = 0; i < int(D); ++i) { + pos = skipWhitespaces(pos); + std::tie(word, pos) = nextWord(pos); + v[i] = strToFloat(word); + } + + return pos; +} + +template std::vector reindex(const std::vector& indices, std::vector& data) { + /* Check that indices are in range */ + for(UnsignedInt i: indices) if(i >= data.size()) { + Error() << "Trade::ObjImporter::mesh3D(): index out of range"; + throw 0; } - std::string str = arrayToString(pos.prefix(i)); - return std::make_tuple(str, pos.suffix(i+1)); + + return MeshTools::duplicate(indices, data); } } @@ -230,81 +389,185 @@ void ObjImporter::doOpenData(Containers::ArrayView data) { } void ObjImporter::parse() { - ArrayView pos = _in; - ObjMesh* mesh = new ObjMesh; + ArrayView line = _in; /* points to beginning of current line */ + ArrayView pos = _in; /* points to current character in line */ + + ObjObject* object = nullptr; + ObjGroup* group = nullptr; + ObjMeshData* meshData = nullptr; + + ArrayView section{nullptr}; + int minSectionPrimitives = 0; + char sectionPrimitive = '?'; + + /* Set index 0 of data to default value */ + _state->positions.emplace_back(); + _state->normals.emplace_back(); // TODO Default normal? + _state->texCoords.emplace_back(); + + /* Create 'object' if not created by 'o' keyword */ + auto ensureObject = [&]{ + if(object == nullptr) { + // TODO: C++ 17 + _state->objects.emplace_back(new ObjObject); + object = _state->objects.back().get(); + } + }; + + /* Create 'group' if not created by 'g' keyword */ + auto ensureGroup = [&]{ + if(group == nullptr) { + ensureObject(); + + // TODO: C++ 17 + object->groups.emplace_back(new ObjGroup{*object}); + group = object->groups.back().get(); + } + }; + + /* Create 'meshData' if not created by 'usemtl' keyword */ + auto ensureMeshData = [&](){ + if(meshData == nullptr) { + ensureGroup(); + meshData = &group->meshDataForMaterial(-1); + } + }; + + /* Close a section and add it to the current meshData */ + auto finishSection = [&]{ + if(section.data() == nullptr) { + /* No open section */ + return; + } + + ensureMeshData(); + + ObjMesh* mesh; + if(sectionPrimitive == 'p') { + if(!meshData->points) { + // TODO: C++ 17 + _state->meshes.emplace_back(new ObjMesh{*meshData, MeshPrimitive::Points}); + meshData->points = _state->meshes.back().get(); + } + mesh = meshData->points; + } else if(sectionPrimitive == 'l') { + if(!meshData->lines) { + // TODO: C++ 17 + _state->meshes.emplace_back(new ObjMesh{*meshData, MeshPrimitive::Lines}); + meshData->lines = _state->meshes.back().get(); + } + mesh = meshData->lines; + } else { /* sectionPrimitive == 'f' */ + if(!meshData->faces) { + // TODO: C++ 17 + _state->meshes.emplace_back(new ObjMesh{*meshData, MeshPrimitive::Triangles}); + meshData->faces = _state->meshes.back().get(); + } + mesh = meshData->faces; + } + if(line.data() == nullptr) { + /* Usually for the last line */ + mesh->sections.push_back(section); + } else { + mesh->sections.emplace_back(section.data(), size_t(line.data()-section.data())); + } + mesh->minPrimitives += minSectionPrimitives; - int currentMaterialIndex = -1; + section = {nullptr}; + minSectionPrimitives = 0; + }; + + auto finishObject = [&]{ + if(object && object->groups.empty()) { + /* Create dummy mesh for this object so that it gets loaded as ObjectData */ + _state->meshlessObjects.push_back(object->name); + } + }; while(!pos.empty()) { /* Comment line */ if(pos[0] == '#') { - pos = ignoreLine(pos); - pos = skipWhitespaces(pos); + finishSection(); + line = pos = skipWhitespaces(ignoreLine(pos)); continue; } /* Parse the keyword */ std::string keyword; - std::tie(keyword, pos) = nextWord(pos); + ArrayView word; + std::tie(word, pos) = nextWord(pos); + keyword = std::string(word.data(), word.size()); pos = skipWhitespaces(pos); + bool sectionEnd = true; /* Vertex position */ if(keyword == "v") { - std::string word; - - // All of this code should be abstracted away in a function. - // Currently copy pasted to vt, vn, Ka, Ks, Kd... - std::tie(word, pos) = nextWord(pos); - const float x = std::stof(word); + // TODO C++17: Could be one slick line with emplace_back ref return. + Vector3 v; + pos = getVector<3>(pos, v); + _state->positions.push_back(v); + /* Texture coordinate */ + } else if(keyword == "vt") { + // TODO C++17: Could be one slick line with emplace_back ref return. + Vector2 tc; + pos = getVector<2>(pos, tc); + _state->texCoords.push_back(tc); - std::tie(word, pos) = nextWord(pos); - const float y = std::stof(word); + /* Normal */ + } else if(keyword == "vn") { + // TODO C++17: Could be one slick line with emplace_back ref return. + Vector3 n; + pos = getVector<3>(pos, n); + _state->normals.push_back(n); - std::tie(word, pos) = nextWord(pos); - const float z = std::stof(word); + /* Indices */ + } else if(keyword == "f" || keyword == "l" || keyword == "p") { + sectionEnd = false; - _state->positions.push_back(Vector3{x, y, z}); - /* Texture coordinate */ - } else if(keyword == "vt") { - std::string word; + if(sectionPrimitive != keyword[0]) { + /* Create new section, mixed primitives! */ + finishSection(); + } - std::tie(word, pos) = nextWord(pos); - const float x = std::stof(word); + if(section.data() == nullptr) { + /* Create new section */ + section = line; + sectionPrimitive = keyword[0]; + } - std::tie(word, pos) = nextWord(pos); - const float y = std::stof(word); + ++minSectionPrimitives; - _state->textureCoordinates.push_back(Vector2{x, y}); + /* Object name */ + } else if(keyword == "o") { + finishSection(); + finishObject(); - /* Normal */ - } else if(keyword == "vn") { - std::string word; + ArrayView name; + std::tie(name, pos) = nextWord(pos); - std::tie(word, pos) = nextWord(pos); - const float x = std::stof(word); + _state->objects.emplace_back(new ObjObject{name}); + object = _state->objects.back().get(); + group = nullptr; + meshData = nullptr; - std::tie(word, pos) = nextWord(pos); - const float y = std::stof(word); + /* Object group */ + } else if(keyword == "g") { + //TODO: Handle geometry shared by multiple groups, e.g. g group1 group2 + ensureObject(); + object->groups.emplace_back(new ObjGroup{*object}); + group = object->groups.back().get(); - std::tie(word, pos) = nextWord(pos); - const float z = std::stof(word); + meshData = nullptr; - _state->normals.push_back(Vector3{x, y, z}); + ArrayView name; + std::tie(name, pos) = nextWord(pos); + group->name = std::string{name.data(), name.size()}; - /* Indices */ - } else if(keyword == "f") { - // Not handling case that there are more than three vertices! - for(int i = 0; i < 3; ++i) { - std::array indices; - std::tie(indices, pos) = parseVertex(pos); - mesh->indices.push_back(indices); - pos = skipWhitespaces(pos, true); - } /* Load a material library */ } else if(keyword == "mtllib") { - std::string word; + ArrayView word; pos = skipWhitespaces(pos); std::tie(word, pos) = nextWord(pos); @@ -312,20 +575,16 @@ void ObjImporter::parse() { /* Set current material and add a new mesh for it */ } else if(keyword == "usemtl") { - std::string word; - pos = skipWhitespaces(pos); + ArrayView word; + pos = skipWhitespaces(pos); std::tie(word, pos) = nextWord(pos); - int materialIndex = _state->materialIndices[word]; - if(materialIndex != currentMaterialIndex) { - currentMaterialIndex = materialIndex; - /* switching the material here, need to create a new mesh */ - _state->meshMaterials.push_back(materialIndex); - - if(!mesh->indices.empty()) { - _state->meshes.push_back(std::unique_ptr(mesh)); - } /* empty meshes are skipped */ - mesh = new ObjMesh; + const int materialIndex = _state->materialIds[std::string{word.data(), word.size()}]; + if(meshData == nullptr || materialIndex != meshData->materialId) { + /* Switching the material here, need to create a new mesh */ + //TODO C++17 + ensureGroup(); + meshData = &group->meshDataForMaterial(materialIndex); } // else: usemtl did not result in material switch, no need to create new mesh /* Ignore unsupported keywords, error out on unknown keywords */ @@ -333,17 +592,25 @@ void ObjImporter::parse() { Warning() << "Trade::ObjImporter::parse(): unknown keyword:" << keyword; } + if(sectionEnd) { + finishSection(); + } + /* Ignore the rest of the line */ - pos = skipWhitespaces(ignoreLine(pos)); + line = pos = skipWhitespaces(ignoreLine(pos)); } - if(!mesh->indices.empty()) { - _state->meshes.push_back(std::unique_ptr(mesh)); + finishSection(); + finishObject(); + + int i = _state->meshes.size(); + for(auto name : _state->meshlessObjects) { + _state->meshlessObjectIds[name] = i++; } } -void ObjImporter::parseMaterialLibrary(std::string libname) { - std::string filename = _fileRoot + libname; +void ObjImporter::parseMaterialLibrary(const ArrayView libname) { + std::string filename = _fileRoot + std::string(libname.data(), libname.size()); /* Open file */ if(!Utility::Directory::fileExists(filename)) { @@ -352,8 +619,8 @@ void ObjImporter::parseMaterialLibrary(std::string libname) { } Containers::Array contents = Utility::Directory::read(filename); + ArrayView pos = contents; /* points to current character in line */ - ArrayView pos = contents; ObjMaterial* mat = nullptr; while(!pos.empty()) { @@ -366,24 +633,25 @@ void ObjImporter::parseMaterialLibrary(std::string libname) { } /* Parse the keyword */ - std::string keyword; - std::tie(keyword, pos) = nextWord(pos); + ArrayView word; + std::tie(word, pos) = nextWord(pos); + std::string keyword{word.data(), word.size()}; if(keyword.empty()) { + pos = skipWhitespaces(pos); continue; } pos = skipWhitespaces(pos); if(keyword == "newmtl") { - if(mat != nullptr) { - _state->materials.push_back(*mat); - _state->materialIndices.insert( - std::make_pair(mat->name, _state->materials.size()-1)); - delete mat; - } - mat = new ObjMaterial; - std::tie(mat->name, pos) = nextWord(pos); + std::tie(word, pos) = nextWord(pos); + + _state->materials.emplace_back(); + mat = &_state->materials.back(); + mat->name = std::string{word.data(), word.size()}; + + _state->materialIds[mat->name] = _state->materials.size()-1; continue; } else if (mat == nullptr) { Error() << "Expected newmtl keyword, got" << keyword; @@ -391,104 +659,66 @@ void ObjImporter::parseMaterialLibrary(std::string libname) { /* Ambient color */ if(keyword == "Ka") { - std::string word; - - // again, this code is duplicated alot. - std::tie(word, pos) = nextWord(pos); - const float r = std::stof(word); - - std::tie(word, pos) = nextWord(pos); - const float g = std::stof(word); + ArrayView word; - std::tie(word, pos) = nextWord(pos); - const float b = std::stof(word); + for(int i : {0, 1, 2}) { + std::tie(word, pos) = nextWord(pos); + mat->ambient[i] = strToFloat(word); + } - mat->ambient = Color3(r, g, b); /* Diffuse color */ } else if(keyword == "Kd") { - std::string word; - - std::tie(word, pos) = nextWord(pos); - const float r = std::stof(word); - - std::tie(word, pos) = nextWord(pos); - const float g = std::stof(word); + ArrayView word; - std::tie(word, pos) = nextWord(pos); - const float b = std::stof(word); - - mat->diffuse = Color3(r, g, b); + for(int i : {0, 1, 2}) { + std::tie(word, pos) = nextWord(pos); + mat->diffuse[i] = strToFloat(word); + } /* Specular color */ } else if(keyword == "Ks") { - std::string word; + ArrayView word; - std::tie(word, pos) = nextWord(pos); - const float r = std::stof(word); - - std::tie(word, pos) = nextWord(pos); - const float g = std::stof(word); - - std::tie(word, pos) = nextWord(pos); - const float b = std::stof(word); - - mat->specular = Color3(r, g, b); + for(int i : {0, 1, 2}) { + std::tie(word, pos) = nextWord(pos); + mat->specular[i] = strToFloat(word); + } /* Specularity */ } else if(keyword == "Ns") { - std::string word; + ArrayView word; std::tie(word, pos) = nextWord(pos); - const float f = std::stof(word); + const float f = strToFloat(word); mat->specularity = f; /* Ambient texture */ - } else if(keyword == "map_Ka") { - std::string texture; - std::tie(texture, pos) = nextLine(pos); + } else if(keyword.substr(0, 4) == "map_") { + ArrayView line; + std::tie(line, pos) = nextLine(pos); + std::string texture{line.data(), line.size()}; - // This is also very similar code for every "map_*" - if(_state->textureIndices.find(texture) == _state->textureIndices.end()) { + int textureId = -1; + if(_state->textureIds.find(texture) == _state->textureIds.end()) { /* new texture, create it */ int index = _state->textures.size(); _state->textures.push_back(texture); - _state->textureIndices[texture] = index; - mat->ambientTexture = index; + _state->textureIds[texture] = index; + textureId = index; } else { - mat->ambientTexture = _state->textureIndices[texture]; + textureId = _state->textureIds[texture]; } - /* Diffuse texture */ - } else if(keyword == "map_Kd") { - std::string texture; - std::tie(texture, pos) = nextLine(pos); - - if(_state->textureIndices.find(texture) == _state->textureIndices.end()) { - /* new texture, create it */ - int index = _state->textures.size(); - _state->textures.push_back(texture); - - _state->textureIndices[texture] = index; - mat->diffuseTexture = index; - } else { - mat->diffuseTexture = _state->textureIndices[texture]; - } - - /* Specular texture */ - } else if(keyword == "map_Ks") { - std::string texture; - std::tie(texture, pos) = nextLine(pos); - - if(_state->textureIndices.find(texture) == _state->textureIndices.end()) { - /* new texture, create it */ - int index = _state->textures.size(); - _state->textures.push_back(texture); - - _state->textureIndices[texture] = index; - mat->specularTexture = index; + auto suffix = keyword.substr(4, 2); + if(suffix == "Kd") { + mat->diffuseTexture = textureId; + } else if(suffix == "Ka") { + mat->ambientTexture = textureId; + } else if(suffix == "Ks") { + mat->specularTexture = textureId; } else { - mat->specularTexture = _state->textureIndices[texture]; + Warning() << "Trade::ObjImporter::parseMaterialLibrary(): unsupported texture type:" << suffix; } /* Ignore unsupported keywords, error out on unknown keywords */ @@ -499,62 +729,171 @@ void ObjImporter::parseMaterialLibrary(std::string libname) { /* Ignore the rest of the line */ pos = skipWhitespaces(ignoreLine(pos)); } - - // add last currently active material to materials vector. Usually added in "newmtl" keyword handling - if(mat != nullptr) { - _state->materials.push_back(*mat); - _state->materialIndices.insert( - std::make_pair(mat->name, _state->materials.size()-1)); - delete mat; - } } UnsignedInt ObjImporter::doMesh3DCount() const { return _state->meshes.size(); } -UnsignedInt ObjImporter::doMaterialCount() const { return _state->meshMaterials.size(); } - +UnsignedInt ObjImporter::doMaterialCount() const { return _state->materials.size(); } UnsignedInt ObjImporter::doImage2DCount() const { return _state->textures.size(); } +UnsignedInt ObjImporter::doObject3DCount() const { return _state->meshes.size() + _state->meshlessObjects.size(); } + +Int ObjImporter::doObject3DForName(const std::string& name) { + auto result = _state->meshlessObjectIds.find(name); + if(result != _state->meshlessObjectIds.end()) { + return result->second; + } + + result = _state->meshIds.find(name); + if(result != _state->meshIds.end()) { + return result->second; + } + + return -1; +} + +std::string ObjImporter::doObject3DName(UnsignedInt id) { + const size_t numMeshes = _state->meshes.size(); + if(id >= numMeshes) { + return _state->meshlessObjects[id - numMeshes]; + } + return _state->meshes[id]->name(); /* Intentional, objects are just meshes + material */ +} + +std::unique_ptr ObjImporter::doObject3D(UnsignedInt id) { + const size_t numMeshes = _state->meshes.size(); + if(id > numMeshes) { + return std::unique_ptr{new ObjectData3D{{}, {}, &_state->meshlessObjects[id - numMeshes]}}; + } + const ObjMesh& mesh = *_state->meshes[id]; + return std::unique_ptr{ + new MeshObjectData3D{{}, {}, id, mesh.data.materialId, _state->meshes[id].get()}}; +} + Int ObjImporter::doMesh3DForName(const std::string& name) { - return 0; + return _state->meshIds[name]; } std::string ObjImporter::doMesh3DName(UnsignedInt id) { - return ""; + return _state->meshes[id]->name(); } std::optional ObjImporter::doMesh3D(UnsignedInt id) { - ObjMesh& mesh = *_state->meshes[id]; - - std::vector positions, normals; - std::vector textureCoords; - - // resolve indices... probably use combineIndexArrays instead? - positions.reserve(mesh.indices.size()); - normals.reserve(mesh.indices.size()); - textureCoords.reserve(mesh.indices.size()); + const ObjMesh& mesh = *_state->meshes[id]; + + const int primitiveSize = (mesh.primitive == MeshPrimitive::Triangles) + ? 3 : ((mesh.primitive == MeshPrimitive::Lines) ? 2 : 1); + + std::vector positionIndices; + std::vector normalIndices; + std::vector textureCoordinateIndices; + + positionIndices.reserve(mesh.minPrimitives*primitiveSize); + if(primitiveSize >= 2) { + /* Only need to allocate texCoords for lines and faces */ + textureCoordinateIndices.reserve(mesh.minPrimitives*primitiveSize); + if(primitiveSize == 3) { + /* Only need to allocate normals for faces */ + normalIndices.reserve(mesh.minPrimitives*primitiveSize); + } + } - for(std::array& indexArray : mesh.indices) { - positions.push_back(_state->positions[indexArray[0]-1]); + // TODO set to true on first non (-1) encounter + bool hasNormals = false; + bool hasTexCoords = false; + + for(auto section : mesh.sections) { + auto pos = skipWhitespaces(section); + + switch(mesh.primitive) { + case MeshPrimitive::Triangles: + while(!pos.empty()) { + CORRADE_ASSERT(pos[0] == 'f' && pos[1] == ' ', "Unexpected primitive keyword for Triangles", {}); + pos = pos.suffix(2); + + for(int i = 0; i < 3; ++i) { + std::array vertex; + std::tie(vertex, pos) = parseVertex(pos); + positionIndices.push_back(vertex[0]); + textureCoordinateIndices.push_back(vertex[1]); + normalIndices.push_back(vertex[2]); + + hasTexCoords = hasTexCoords || (vertex[1] != 0); + hasNormals = hasNormals || (vertex[2] != 0); + + pos = skipWhitespaces(pos, false); + } + pos = skipWhitespaces(ignoreLine(pos)); + } + break; + case MeshPrimitive::Lines: + while(!pos.empty()) { + CORRADE_ASSERT(pos[0] == 'l' && pos[1] == ' ', "Unexpected primitive keyword for Lines", {}); + pos = pos.suffix(2); + + while(!atEndOfLine(pos)) { + std::array line; + std::tie(line, pos) = parseLine(pos); + positionIndices.push_back(line[0]); + textureCoordinateIndices.push_back(line[1]); + + hasTexCoords = hasTexCoords || (line[1] != 0); + + pos = skipWhitespaces(pos, false); + } + pos = skipWhitespaces(ignoreLine(pos)); + } + break; + case MeshPrimitive::Points: + while(!pos.empty()) { + CORRADE_ASSERT(pos[0] == 'p' && pos[1] == ' ', "Unexpected primitive keyword for Points", {}); + pos = pos.suffix(2); + + ArrayView word; + while(!atEndOfLine(pos)) { + std::tie(word, pos) = nextWord(pos); + positionIndices.push_back(strToInt(word)); + pos = skipWhitespaces(pos, false); + } + pos = skipWhitespaces(ignoreLine(pos)); + } + break; + default: + CORRADE_ASSERT_UNREACHABLE(); + } + } - if(indexArray[1] != -1) // even though this looks like it's handling the case, it's not, this is utterly useless. - // may result in differently sized arrays, which is not valid. - normals.push_back(_state->normals[indexArray[1]-1]); - if(indexArray[2] != -1) - textureCoords.push_back(_state->textureCoordinates[indexArray[2]-1]); + /* Merge index arrays, if there aren't just the positions */ + std::vector indices; + + std::vector> positionLayers; + std::vector> normalLayers; + std::vector> texCoordLayers; + + std::vector>> arrays; + arrays.reserve(3); + arrays.emplace_back(positionIndices); + if(hasNormals) arrays.emplace_back(normalIndices); + if(hasTexCoords) arrays.emplace_back(textureCoordinateIndices); + indices = MeshTools::combineIndexArrays(arrays); + + /* Reindex data arrays */ + try { + positionLayers.push_back(reindex(positionIndices, _state->positions)); + if(hasNormals) normalLayers.push_back(reindex(normalIndices, _state->normals)); + if(hasTexCoords) texCoordLayers.push_back(reindex(textureCoordinateIndices, _state->texCoords)); + } catch(...) { + /* Error message already printed */ + return std::nullopt; } - return MeshData3D( - MeshPrimitive::Triangles, - {}, - {positions}, - {normals}, - {textureCoords}); + return MeshData3D(mesh.primitive, std::move(indices), std::move(positionLayers), + std::move(normalLayers), std::move(texCoordLayers), {}, &mesh); } -std::unique_ptr ObjImporter::doMaterial(UnsignedInt id) { - ObjMaterial& objMat = _state->materials[_state->meshMaterials[id]]; +std::unique_ptr ObjImporter::doMaterial(const UnsignedInt id) { + ObjMaterial& objMat = _state->materials[id]; PhongMaterialData::Flags flags; if(objMat.ambientTexture != -1) { diff --git a/src/MagnumPlugins/ObjImporter/ObjImporter.h b/src/MagnumPlugins/ObjImporter/ObjImporter.h index a3cca3ba1..b13d37ced 100644 --- a/src/MagnumPlugins/ObjImporter/ObjImporter.h +++ b/src/MagnumPlugins/ObjImporter/ObjImporter.h @@ -95,6 +95,11 @@ class MAGNUM_OBJIMPORTER_EXPORT ObjImporter: public AbstractImporter { MAGNUM_OBJIMPORTER_LOCAL void doOpenFile(const std::string& filename) override; MAGNUM_OBJIMPORTER_LOCAL void doClose() override; + MAGNUM_OBJIMPORTER_LOCAL UnsignedInt doObject3DCount() const override; + MAGNUM_OBJIMPORTER_LOCAL Int doObject3DForName(const std::string& name) override; + MAGNUM_OBJIMPORTER_LOCAL std::string doObject3DName(UnsignedInt id) override; + MAGNUM_OBJIMPORTER_LOCAL std::unique_ptr doObject3D(UnsignedInt id) override; + MAGNUM_OBJIMPORTER_LOCAL UnsignedInt doMesh3DCount() const override; MAGNUM_OBJIMPORTER_LOCAL Int doMesh3DForName(const std::string& name) override; MAGNUM_OBJIMPORTER_LOCAL std::string doMesh3DName(UnsignedInt id) override; @@ -107,7 +112,7 @@ class MAGNUM_OBJIMPORTER_EXPORT ObjImporter: public AbstractImporter { MAGNUM_OBJIMPORTER_LOCAL std::optional doImage2D(UnsignedInt id); MAGNUM_OBJIMPORTER_LOCAL void parse(); - MAGNUM_OBJIMPORTER_LOCAL void parseMaterialLibrary(std::string libname); + MAGNUM_OBJIMPORTER_LOCAL void parseMaterialLibrary(Containers::ArrayView libname); Containers::Array _in; std::unique_ptr _state; diff --git a/src/MagnumPlugins/ObjImporter/Test/Test.cpp b/src/MagnumPlugins/ObjImporter/Test/Test.cpp index d82b2afca..ad341af8e 100644 --- a/src/MagnumPlugins/ObjImporter/Test/Test.cpp +++ b/src/MagnumPlugins/ObjImporter/Test/Test.cpp @@ -50,7 +50,6 @@ struct ObjImporterTest: TestSuite::Tester { void textureCoordinatesNormals(); void emptyFile(); - void unnamedMesh(); void namedMesh(); void moreMeshes(); void unnamedFirstMesh(); @@ -104,7 +103,6 @@ ObjImporterTest::ObjImporterTest() { &ObjImporterTest::textureCoordinatesNormals, &ObjImporterTest::emptyFile, - &ObjImporterTest::unnamedMesh, &ObjImporterTest::namedMesh, &ObjImporterTest::moreMeshes, &ObjImporterTest::unnamedFirstMesh, @@ -156,11 +154,11 @@ void ObjImporterTest::pointMesh() { CORRADE_COMPARE(data->positionArrayCount(), 1); CORRADE_COMPARE(data->positions(0), (std::vector{ {0.5f, 2.0f, 3.0f}, - {0.0f, 1.5f, 1.0f}, - {2.0f, 3.0f, 5.0f} + {2.0f, 3.0f, 5.0f}, + {0.0f, 1.5f, 1.0f} })); CORRADE_COMPARE(data->indices(), (std::vector{ - 0, 2, 1, 0 + 0, 1, 2, 0 })); } @@ -206,12 +204,32 @@ void ObjImporterTest::triangleMesh() { void ObjImporterTest::mixedPrimitives() { ObjImporter importer; CORRADE_VERIFY(importer.openFile(Utility::Directory::join(OBJIMPORTER_TEST_DIR, "mixedPrimitives.obj"))); - CORRADE_COMPARE(importer.mesh3DCount(), 1); + CORRADE_COMPARE(importer.mesh3DCount(), 2); - std::ostringstream out; - Error redirectError{&out}; - CORRADE_VERIFY(!importer.mesh3D(0)); - CORRADE_COMPARE(out.str(), "Trade::ObjImporter::mesh3D(): mixed primitive MeshPrimitive::Points and MeshPrimitive::Lines\n"); + /* point mesh */ + auto pointData = importer.mesh3D(0); + CORRADE_VERIFY(pointData); + CORRADE_COMPARE(pointData->primitive(), MeshPrimitive::Points); + CORRADE_COMPARE(pointData->positions(0), (std::vector{ + {0.5f, 2.0f, 3.0f}, + {2.0f, 3.0f, 5.0f}, + {0.0f, 1.5f, 1.0f} + })); + CORRADE_COMPARE(pointData->indices(), (std::vector{ + 0, 1, 2 + })); + + auto lineData = importer.mesh3D(1); + CORRADE_VERIFY(lineData); + CORRADE_COMPARE(lineData->primitive(), MeshPrimitive::Lines); + CORRADE_COMPARE(lineData->positions(0), (std::vector{ + {0.5f, 2.0f, 3.0f}, + {0.0f, 1.5f, 1.0f}, + {2.0f, 3.0f, 5.0f} + })); + CORRADE_COMPARE(lineData->indices(), (std::vector{ + 0, 1, 1, 2 + })); } void ObjImporterTest::positionsOnly() { @@ -261,7 +279,7 @@ void ObjImporterTest::normals() { const Containers::Optional data = importer.mesh3D(0); CORRADE_VERIFY(data); - CORRADE_COMPARE(data->primitive(), MeshPrimitive::Lines); + CORRADE_COMPARE(data->primitive(), MeshPrimitive::Triangles); CORRADE_COMPARE(data->positionArrayCount(), 1); CORRADE_VERIFY(!data->hasTextureCoords2D()); CORRADE_COMPARE(data->normalArrayCount(), 1); @@ -278,7 +296,7 @@ void ObjImporterTest::normals() { {0.5f, 1.0f, 0.5f} })); CORRADE_COMPARE(data->indices(), (std::vector{ - 0, 1, 2, 3, 1, 0 + 0, 1, 0, 2, 3, 0, 1, 0, 0 })); } @@ -289,7 +307,7 @@ void ObjImporterTest::textureCoordinatesNormals() { const Containers::Optional data = importer.mesh3D(0); CORRADE_VERIFY(data); - CORRADE_COMPARE(data->primitive(), MeshPrimitive::Lines); + CORRADE_COMPARE(data->primitive(), MeshPrimitive::Triangles); CORRADE_COMPARE(data->positionArrayCount(), 1); CORRADE_COMPARE(data->textureCoords2DArrayCount(), 1); CORRADE_COMPARE(data->normalArrayCount(), 1); @@ -315,30 +333,26 @@ void ObjImporterTest::textureCoordinatesNormals() { {0.5f, 1.0f, 0.5f} })); CORRADE_COMPARE(data->indices(), (std::vector{ - 0, 1, 2, 3, 1, 0, 4, 2 + 0, 1, 2, 3, 1, 0, 4, 2, 2 })); } void ObjImporterTest::emptyFile() { ObjImporter importer; CORRADE_VERIFY(importer.openFile(Utility::Directory::join(OBJIMPORTER_TEST_DIR, "emptyFile.obj"))); - CORRADE_COMPARE(importer.mesh3DCount(), 1); -} -void ObjImporterTest::unnamedMesh() { - ObjImporter importer; - CORRADE_VERIFY(importer.openFile(Utility::Directory::join(OBJIMPORTER_TEST_DIR, "emptyFile.obj"))); - CORRADE_COMPARE(importer.mesh3DCount(), 1); - CORRADE_COMPARE(importer.mesh3DName(0), ""); - CORRADE_COMPARE(importer.mesh3DForName(""), -1); + CORRADE_COMPARE(importer.mesh3DCount(), 0); + CORRADE_COMPARE(importer.object3DCount(), 0); + CORRADE_COMPARE(importer.image3DCount(), 0); } void ObjImporterTest::namedMesh() { ObjImporter importer; CORRADE_VERIFY(importer.openFile(Utility::Directory::join(OBJIMPORTER_TEST_DIR, "namedMesh.obj"))); - CORRADE_COMPARE(importer.mesh3DCount(), 1); - CORRADE_COMPARE(importer.mesh3DName(0), "MyMesh"); - CORRADE_COMPARE(importer.mesh3DForName("MyMesh"), 0); + CORRADE_COMPARE(importer.mesh3DCount(), 0); + CORRADE_COMPARE(importer.object3DCount(), 1); + CORRADE_COMPARE(importer.object3DName(0), "MyMesh"); + CORRADE_COMPARE(importer.object3DForName("MyMesh"), 0); } void ObjImporterTest::moreMeshes() { @@ -357,7 +371,7 @@ void ObjImporterTest::moreMeshes() { {0.0f, 1.5f, 1.0f} })); CORRADE_COMPARE(data->indices(), (std::vector{ - 0, 1 + 0, 1, 0 })); CORRADE_COMPARE(importer.mesh3DName(1), "LineMesh"); @@ -393,13 +407,17 @@ void ObjImporterTest::moreMeshes() { void ObjImporterTest::unnamedFirstMesh() { ObjImporter importer; CORRADE_VERIFY(importer.openFile(Utility::Directory::join(OBJIMPORTER_TEST_DIR, "unnamedFirstMesh.obj"))); - CORRADE_COMPARE(importer.mesh3DCount(), 2); + CORRADE_COMPARE(importer.mesh3DCount(), 1); + CORRADE_COMPARE(importer.object3DCount(), 2); /* Second mesh is empty, hence 2 objects, but 1 mesh */ + + CORRADE_COMPARE(importer.object3DName(0), ""); + CORRADE_COMPARE(importer.object3DForName(""), 0); // TODO: why is this -1? CORRADE_COMPARE(importer.mesh3DName(0), ""); - CORRADE_COMPARE(importer.mesh3DForName(""), -1); + CORRADE_COMPARE(importer.mesh3DForName(""), 0); - CORRADE_COMPARE(importer.mesh3DName(1), "SecondMesh"); - CORRADE_COMPARE(importer.mesh3DForName("SecondMesh"), 1); + CORRADE_COMPARE(importer.object3DName(1), "SecondMesh"); + CORRADE_COMPARE(importer.object3DForName("SecondMesh"), 1); } void ObjImporterTest::wrongFloat() { @@ -410,7 +428,6 @@ void ObjImporterTest::wrongFloat() { std::ostringstream out; Error redirectError{&out}; - CORRADE_VERIFY(!importer.mesh3D(id)); CORRADE_COMPARE(out.str(), "Trade::ObjImporter::mesh3D(): error while converting numeric data\n"); } @@ -745,7 +762,6 @@ void ObjImporterTest::unsupportedKeyword() { CORRADE_COMPARE(data->positions(0), (std::vector{ {0.0f, 1.0f, 2.0f} })); - CORRADE_COMPARE(data->indices(), std::vector{0}); } void ObjImporterTest::unknownKeyword() { diff --git a/src/MagnumPlugins/ObjImporter/Test/moreMeshes.obj b/src/MagnumPlugins/ObjImporter/Test/moreMeshes.obj index e4820d5b9..0a779c0c0 100644 --- a/src/MagnumPlugins/ObjImporter/Test/moreMeshes.obj +++ b/src/MagnumPlugins/ObjImporter/Test/moreMeshes.obj @@ -4,8 +4,8 @@ v 0.5 2 3 v 0 1.5 1 vn 0.5 2 3 vn 0 1.5 1 -p 1//1 -p 2//2 +p 1 2 +p 1 # Lines o LineMesh diff --git a/src/MagnumPlugins/ObjImporter/Test/multiMaterial.obj b/src/MagnumPlugins/ObjImporter/Test/multiMaterial.obj index 85b14bcbc..4da632842 100644 --- a/src/MagnumPlugins/ObjImporter/Test/multiMaterial.obj +++ b/src/MagnumPlugins/ObjImporter/Test/multiMaterial.obj @@ -1,24 +1,22 @@ mtllib multiMaterial.mtl - - # Billboard -# positions +# positions v -2.94249 18.5768 3.27633 v 1.72414 18.9233 -3.20162 v 2.74428 -0.499733 -3.50576 v -1.92235 -0.846268 2.9722 -# normals +# normals vn 0.810085 0.0333816 0.585361 vn 0.810085 0.0333816 0.585361 vn 0.810085 0.0333816 0.585361 vn 0.810085 0.0333816 0.585361 -# texture coords +# texture coords vt 0 1 vt 0 0 vt 1 0 @@ -26,28 +24,28 @@ vt 1 1 usemtl mat_0 -# faces +# faces f 1/1/1 2/2/2 3/3/3 f 1/1/1 3/3/3 4/4/4 # Billboard -# positions +# positions v 2.43556 18.8755 2.23745 v -3.6539 18.6246 -2.16274 v -2.63377 -0.798466 -2.46688 v 3.4557 -0.547535 1.93331 -# normals +# normals vn 0.583961 0.0433639 -0.810622 vn 0.583961 0.0433639 -0.810622 vn 0.583961 0.0433639 -0.810622 vn 0.583961 0.0433639 -0.810622 -# texture coords +# texture coords vt 0 1 vt 0 0 vt 1 0 @@ -55,6 +53,6 @@ vt 1 1 usemtl mat_1 -# faces +# faces f 5/5/5 6/6/6 7/7/7 f 5/5/5 7/7/7 8/8/8 diff --git a/src/MagnumPlugins/ObjImporter/Test/normals.obj b/src/MagnumPlugins/ObjImporter/Test/normals.obj index 15711d5ca..deb4a897f 100644 --- a/src/MagnumPlugins/ObjImporter/Test/normals.obj +++ b/src/MagnumPlugins/ObjImporter/Test/normals.obj @@ -6,7 +6,7 @@ v 0 1.5 1 vn 1 0.5 3.5 vn 0.5 1 0.5 -# Lines -l 1//1 2//1 -l 1//2 2//2 -l 2//1 1//1 +# Triangles +f 1//1 2//1 1//1 +f 1//2 2//2 1//1 +f 2//1 1//1 1//1 diff --git a/src/MagnumPlugins/ObjImporter/Test/textureCoordinatesNormals.obj b/src/MagnumPlugins/ObjImporter/Test/textureCoordinatesNormals.obj index c1750ecb2..3ab1cab91 100644 --- a/src/MagnumPlugins/ObjImporter/Test/textureCoordinatesNormals.obj +++ b/src/MagnumPlugins/ObjImporter/Test/textureCoordinatesNormals.obj @@ -10,8 +10,7 @@ vt 0.5 1 vn 1 0.5 3.5 vn 0.5 1 0.5 -# Lines -l 1/1/1 2/1/2 -l 1/2/2 2/2/1 -l 2/1/2 1/1/1 -l 2/2/2 1/2/2 +# Faces +f 1/1/1 2/1/2 1/2/2 +f 2/2/1 2/1/2 1/1/1 +f 2/2/2 1/2/2 1/2/2 diff --git a/src/MagnumPlugins/ObjImporter/Test/unnamedFirstMesh.obj b/src/MagnumPlugins/ObjImporter/Test/unnamedFirstMesh.obj index 71752c6d9..7131e199b 100644 --- a/src/MagnumPlugins/ObjImporter/Test/unnamedFirstMesh.obj +++ b/src/MagnumPlugins/ObjImporter/Test/unnamedFirstMesh.obj @@ -1,2 +1,4 @@ v 1 2 3 +p 1 o SecondMesh +# empty