diff --git a/src/MagnumPlugins/ObjImporter/CMakeLists.txt b/src/MagnumPlugins/ObjImporter/CMakeLists.txt index a9a2756185..70f9a10782 100644 --- a/src/MagnumPlugins/ObjImporter/CMakeLists.txt +++ b/src/MagnumPlugins/ObjImporter/CMakeLists.txt @@ -60,6 +60,9 @@ if(BUILD_STATIC_PIC) set_target_properties(ObjImporter PROPERTIES POSITION_INDEPENDENT_CODE ON) endif() target_link_libraries(ObjImporter Magnum MagnumMeshTools) +if(CORRADE_TARGET_WINDOWS) + target_link_libraries(ObjImporter AnyImageImporter) +endif() install(FILES ${ObjImporter_HEADERS} DESTINATION ${MAGNUM_PLUGINS_INCLUDE_INSTALL_DIR}/ObjImporter) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/configure.h DESTINATION ${MAGNUM_PLUGINS_INCLUDE_INSTALL_DIR}/ObjImporter) @@ -73,11 +76,26 @@ if(BUILD_PLUGINS_STATIC) endif() if(BUILD_TESTS) - add_library(MagnumObjImporterTestLib STATIC - $ - ${PROJECT_SOURCE_DIR}/src/dummy.cpp) # XCode workaround, see file comment for details + # On Win32 we need to avoid dllimporting AnyImageImporter symbols, because + # it would search for the symbols in some DLL even when they were linked + # statically. However it apparently doesn't matter that they were + # dllexported when building the static library. EH. And because the + # -DObjImporterObjects_EXPORTS is no longer set in this case, we need + # to avoid dllimporting ObjImporter symbols as well. + if(CORRADE_TARGET_WINDOWS) + add_library(MagnumObjImporterTestLib STATIC + ${ObjImporter_SRCS} + ${ObjImporter_HEADERS}) + target_compile_definitions(MagnumObjImporterTestLib PRIVATE + "MAGNUM_ANYIMAGEIMPORTER_BUILD_STATIC" + "MAGNUM_OBJIMPORTER_BUILD_STATIC") + else() + add_library(MagnumObjImporterTestLib STATIC + $ + ${PROJECT_SOURCE_DIR}/src/dummy.cpp) # XCode workaround, see file comment for details + endif() set_target_properties(MagnumObjImporterTestLib PROPERTIES FOLDER "MagnumPlugins/ObjImporter") - target_link_libraries(MagnumObjImporterTestLib Magnum MagnumMeshTools) + target_link_libraries(MagnumObjImporterTestLib Magnum MagnumMeshTools MagnumAnyImageImporterTestLib) add_subdirectory(Test) endif() diff --git a/src/MagnumPlugins/ObjImporter/ObjImporter.cpp b/src/MagnumPlugins/ObjImporter/ObjImporter.cpp index 9470ca177a..bacebe7822 100644 --- a/src/MagnumPlugins/ObjImporter/ObjImporter.cpp +++ b/src/MagnumPlugins/ObjImporter/ObjImporter.cpp @@ -28,62 +28,344 @@ #include #include #include +#include #include #include #include +#include #include "Magnum/Mesh.h" #include "Magnum/MeshTools/CombineIndexedArrays.h" #include "Magnum/MeshTools/Duplicate.h" +#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/AnyImageImporter/AnyImageImporter.h" + +using namespace Corrade::Containers; namespace Magnum { namespace Trade { -struct ObjImporter::File { - std::unordered_map meshesForName; - std::vector meshNames; - std::vector> meshes; - std::unique_ptr in; + +/* 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; + 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; + + explicit ObjMeshData(ObjGroup& g, Int matId): group(g), materialId(matId) {} +}; + +struct ObjGroup { + ObjObject& object; + std::string name; /* From 'g' keyword */ + + std::vector> meshes; + std::unordered_map meshPerMaterial; + + explicit 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 { + 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 */ + + explicit 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; + } +}; + + +/* The state of the imported generated by openData() */ +struct ImporterState { + Containers::Array in; + std::string fileRoot; + + std::vector materials; + std::unordered_map materialIds; + + std::vector textures; + 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 { -void ignoreLine(std::istream& in) { - in.ignore(std::numeric_limits::max(), '\n'); +/* Returns the given string parsed as Int */ +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; + } + + return i; +} + +/* Returns the given string parsed as Float */ +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 f; +} + +/* + * Returns the index of the next occurrence of `c` or -1, if it could not be found. + * + * @param termByNewline if `true`, the search will terminate at '\n' and return -1. + * @param termByWhitespace if `true`, the search will terminate at ' ' and return -1. + */ +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; + } + + if((pos[i] == '\n' && termByNewline) || (pos[i] == ' ' && termByWhitespace)) { + return -1; + } + } + return -1; +} + +/* + * Return suffix beginning at next non-whitespace character + * @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' && c != '\0' && (!newlineIsWhitespace || (c != '\n' && c != '\r'))) { + return pos.suffix(i); + } + } + return {}; +} + +/* Returns suffix after the next '\n' or the given ArrayView if no newline could be found */ +ArrayView ignoreLine(const ArrayView pos) { + return pos.suffix(findNext(pos, '\n') + 1); +} + +/* Returns a prefix until the next '\n' and the suffix after the newline. */ +std::pair, ArrayView> nextLine(const ArrayView& pos) { + Int i = Math::min(findNext(pos, '\n'), findNext(pos, '\r')); + if(i == -1) { + i = pos.size(); + return {pos.prefix(i), pos.suffix(i)}; + } + return {pos.prefix(i), pos.suffix(i+1)}; +} + +/* Returns true if the next non-whitespace character is a newline character */ +bool atEndOfLine(const ArrayView pos) { + Int i = 0; + while(pos[i] == ' ') ++i; + + return (pos[i] == '\n' || pos[i] == '\r'); } -template Math::Vector extractFloatData(const std::string& str, Float* extra = nullptr) { - std::vector data = Utility::String::splitWithoutEmptyParts(str, ' '); - if(data.size() < size || data.size() > size + (extra ? 1 : 0)) { - Error() << "Trade::ObjImporter::mesh3D(): invalid float array size"; - throw 0; +/* Returns the prefix until the next ' ' character and the suffix starting with it */ +std::pair, 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; + } } + return {pos.prefix(i), pos.suffix(i)}; +} - Math::Vector output; +/* Parse indices for a line, e.g. "1/2" */ +std::pair, 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 {indices, endpos}; + } - for(std::size_t i = 0; i != size; ++i) - output[i] = std::stof(data[i]); + indices[0] = strToInt(pos.prefix(i)); + endpos = pos.suffix(i+1); - if(data.size() == size+1) { - /* This should be obvious from the first if, but add this just to make - Clang Analyzer happy */ - CORRADE_INTERNAL_ASSERT(extra); + 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); - *extra = std::stof(data.back()); + 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 output; + return {indices, endpos}; } -template void reindex(const std::vector& indices, std::vector& data) { +/* Parse a "v/n/t" string to indices for a face */ +std::pair, 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 {indices, endpos}; + } + + 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", in which case the indices of the / are 1 apart */ + auto prefix = endpos.prefix(i); + if(!prefix.empty()) { + indices[1] = strToInt(prefix); + } + endpos = endpos.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[2] = strToInt(prefix); + endpos = endpos.suffix(i); + } + } + + return {indices, endpos}; +} + +template Math::Vector parseVector(ArrayView& pos) { + ArrayView word; + Math::Vector v; + + for(Int i = 0; i < Int(dimensions); ++i) { + std::tie(word, pos) = nextWord(skipWhitespaces(pos)); + v[i] = strToFloat(word); + } + + return v; +} + +template std::vector reindex(const std::vector& indices, const 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; + return {}; } - data = MeshTools::duplicate(indices, data); + return MeshTools::duplicate(indices, data); } } @@ -96,351 +378,564 @@ ObjImporter::~ObjImporter() = default; auto ObjImporter::doFeatures() const -> Features { return Feature::OpenData; } -void ObjImporter::doClose() { _file.reset(); } +void ObjImporter::doClose() { _state->in = nullptr; } -bool ObjImporter::doIsOpened() const { return !!_file; } +bool ObjImporter::doIsOpened() const { return _state->in; } void ObjImporter::doOpenFile(const std::string& filename) { - std::unique_ptr in{new std::ifstream{filename, std::ios::binary}}; - if(!in->good()) { - Error() << "Trade::ObjImporter::openFile(): cannot open file" << filename; - return; - } - - _file.reset(new File); - _file->in = std::move(in); - parseMeshNames(); + _state->fileRoot = Utility::Directory::path(filename); + AbstractImporter::doOpenFile(filename); } void ObjImporter::doOpenData(Containers::ArrayView data) { - _file.reset(new File); - _file->in.reset(new std::istringstream{{data.begin(), data.size()}}); - - parseMeshNames(); + _state->in = Containers::Array{data.size()}; + std::copy(data.begin(), data.end(), _state->in.begin()); + _state.reset(new ImporterState); + parse(); } -void ObjImporter::parseMeshNames() { - /* First mesh starts at the beginning, its indices start from 1. The end - offset will be updated to proper value later. */ - UnsignedInt positionIndexOffset = 1; - UnsignedInt normalIndexOffset = 1; - UnsignedInt textureCoordinateIndexOffset = 1; - _file->meshes.emplace_back(0, 0, positionIndexOffset, normalIndexOffset, textureCoordinateIndexOffset); +void ObjImporter::parse() { + ArrayView line = _state->in; /* Points to beginning of current line */ + ArrayView pos = _state->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(); - /* The first mesh doesn't have name by default but we might find it later, - so we need to track whether there are any data before first name */ - bool thisIsFirstMeshAndItHasNoData = true; - _file->meshNames.emplace_back(); + 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 if(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; + } else { + CORRADE_ASSERT_UNREACHABLE(); + } + 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; - while(_file->in->good()) { - /* The previous object might end at the beginning of this line */ - const std::streampos end = _file->in->tellg(); + 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(_file->in->peek() == '#') { - ignoreLine(*_file->in); + if(pos[0] == '#') { + finishSection(); + line = pos = skipWhitespaces(ignoreLine(pos)); continue; } /* Parse the keyword */ std::string keyword; - *_file->in >> keyword; + ArrayView word; + std::tie(word, pos) = nextWord(pos); + keyword = std::string(word.data(), word.size()); - /* Mesh name */ - if(keyword == "o") { - std::string name; - std::getline(*_file->in, name); - name = Utility::String::trim(name); + pos = skipWhitespaces(pos); + bool sectionEnd = true; - /* This is the name of first mesh */ - if(thisIsFirstMeshAndItHasNoData) { - thisIsFirstMeshAndItHasNoData = false; + /* Vertex position */ + if(keyword == "v") { + _state->positions.push_back(parseVector<3>(pos)); - /* Update its name and add it to name map */ - if(!name.empty()) - _file->meshesForName.emplace(name, _file->meshes.size() - 1); - _file->meshNames.back() = std::move(name); + /* Texture coordinate */ + } else if(keyword == "vt") { + _state->texCoords.push_back(parseVector<2>(pos)); - /* Update its begin offset to be more precise */ - std::get<0>(_file->meshes.back()) = _file->in->tellg(); + /* Normal */ + } else if(keyword == "vn") { + _state->normals.push_back(parseVector<3>(pos)); - /* Otherwise this is a name of new mesh */ - } else { - /* Set end of the previous one */ - std::get<1>(_file->meshes.back()) = end; - - /* Save name and offset of the new one. The end offset will be - updated later. */ - if(!name.empty()) - _file->meshesForName.emplace(name, _file->meshes.size()); - _file->meshNames.emplace_back(std::move(name)); - _file->meshes.emplace_back(_file->in->tellg(), 0, positionIndexOffset, textureCoordinateIndexOffset, normalIndexOffset); + /* Indices */ + } else if(keyword == "f" || keyword == "l" || keyword == "p") { + sectionEnd = false; + + if(sectionPrimitive != keyword[0]) { + /* Create new section, mixed primitives! */ + finishSection(); } - continue; + if(section.data() == nullptr) { + /* Create new section */ + section = line; + sectionPrimitive = keyword[0]; + } - /* If there are any data/indices before the first name, it means that - the first object is unnamed. We need to check for them. */ + ++minSectionPrimitives; - /* Vertex data, update index offset for the following meshes */ - } else if(keyword == "v") { - ++positionIndexOffset; - thisIsFirstMeshAndItHasNoData = false; - } else if(keyword == "vt") { - ++textureCoordinateIndexOffset; - thisIsFirstMeshAndItHasNoData = false; - } else if(keyword == "vn") { - ++normalIndexOffset; - thisIsFirstMeshAndItHasNoData = false; - - /* Index data, just mark that we found something for first unnamed - object */ - } else if(thisIsFirstMeshAndItHasNoData) for(const std::string& data: {"p", "l", "f"}) { - if(keyword == data) { - thisIsFirstMeshAndItHasNoData = false; - break; - } + /* Object name */ + } else if(keyword == "o") { + finishSection(); + finishObject(); + + ArrayView name; + std::tie(name, pos) = nextWord(pos); + + _state->objects.emplace_back(new ObjObject{name}); + object = _state->objects.back().get(); + group = nullptr; + meshData = nullptr; + + /* 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(); + + meshData = nullptr; + + ArrayView name; + std::tie(name, pos) = nextWord(pos); + group->name = std::string{name.data(), name.size()}; + + /* Load a material library */ + } else if(keyword == "mtllib") { + ArrayView word; + pos = skipWhitespaces(pos); + std::tie(word, pos) = nextWord(pos); + + parseMaterialLibrary(word); + + /* Set current material and add a new mesh for it */ + } else if(keyword == "usemtl") { + ArrayView word; + pos = skipWhitespaces(pos); + std::tie(word, pos) = nextWord(pos); + + 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 */ + } else { + Warning() << "Trade::ObjImporter::parse(): unknown keyword:" << keyword; + } + + if(sectionEnd) { + finishSection(); } /* Ignore the rest of the line */ - ignoreLine(*_file->in); + line = pos = skipWhitespaces(ignoreLine(pos)); } - /* Set end of the last object */ - _file->in->clear(); - _file->in->seekg(0, std::ios::end); - std::get<1>(_file->meshes.back()) = _file->in->tellg(); + finishSection(); + finishObject(); + + Int i = _state->meshes.size(); + for(auto name : _state->meshlessObjects) { + _state->meshlessObjectIds[name] = i++; + } } -UnsignedInt ObjImporter::doMesh3DCount() const { return _file->meshes.size(); } +void ObjImporter::parseMaterialLibrary(const ArrayView libname) { + std::string filename = _state->fileRoot + std::string(libname.data(), libname.size()); -Int ObjImporter::doMesh3DForName(const std::string& name) { - const auto it = _file->meshesForName.find(name); - return it == _file->meshesForName.end() ? -1 : it->second; -} + /* Open file */ + if(!Utility::Directory::fileExists(filename)) { + Error() << "Trade::AbstractImporter::parseMaterialLibrary(): cannot open file" << filename; + return; + } -std::string ObjImporter::doMesh3DName(UnsignedInt id) { - return _file->meshNames[id]; -} + Containers::Array contents = Utility::Directory::read(filename); + ArrayView pos = contents; /* Points to current character in line */ -Containers::Optional ObjImporter::doMesh3D(UnsignedInt id) { - /* Seek the file, set mesh parsing parameters */ - std::streampos begin, end; - UnsignedInt positionIndexOffset, textureCoordinateIndexOffset, normalIndexOffset; - std::tie(begin, end, positionIndexOffset, textureCoordinateIndexOffset, normalIndexOffset) = _file->meshes[id]; - _file->in->seekg(begin); + ObjMaterial* mat = nullptr; - Containers::Optional primitive; - std::vector positions; - std::vector> textureCoordinates; - std::vector> normals; - std::vector positionIndices; - std::vector textureCoordinateIndices; - std::vector normalIndices; + while(!pos.empty()) { - try { while(_file->in->good() && _file->in->tellg() < end) { - /* Ignore comments */ - if(_file->in->peek() == '#') { - ignoreLine(*_file->in); + /* Comment line */ + if(pos[0] == '#') { + pos = ignoreLine(pos); + pos = skipWhitespaces(pos); continue; } - /* Get the line */ - std::string line; - std::getline(*_file->in, line); - line = Utility::String::trim(line); - - /* Ignore empty lines */ - if(line.empty()) continue; - - /* Split the line into keyword and contents */ - const std::size_t keywordEnd = line.find(' '); - const std::string keyword = line.substr(0, keywordEnd); - const std::string contents = keywordEnd != std::string::npos ? - Utility::String::ltrim(line.substr(keywordEnd+1)) : ""; - - /* Vertex position */ - if(keyword == "v") { - Float extra{1.0f}; - const Vector3 data = extractFloatData<3>(contents, &extra); - if(!Math::TypeTraits::equals(extra, 1.0f)) { - Error() << "Trade::ObjImporter::mesh3D(): homogeneous coordinates are not supported"; - return Containers::NullOpt; - } - - positions.push_back(data); + /* Parse the keyword */ + ArrayView word; + std::tie(word, pos) = nextWord(pos); + std::string keyword{word.data(), word.size()}; - /* Texture coordinate */ - } else if(keyword == "vt") { - Float extra{0.0f}; - const auto data = extractFloatData<2>(contents, &extra); - if(!Math::TypeTraits::equals(extra, 0.0f)) { - Error() << "Trade::ObjImporter::mesh3D(): 3D texture coordinates are not supported"; - return Containers::NullOpt; - } + if(keyword.empty()) { + pos = skipWhitespaces(pos); + continue; + } - if(textureCoordinates.empty()) textureCoordinates.emplace_back(); - textureCoordinates.front().emplace_back(data); + pos = skipWhitespaces(pos); - /* Normal */ - } else if(keyword == "vn") { - if(normals.empty()) normals.emplace_back(); - normals.front().emplace_back(extractFloatData<3>(contents)); + if(keyword == "newmtl") { + std::tie(word, pos) = nextWord(pos); - /* Indices */ - } else if(keyword == "p" || keyword == "l" || keyword == "f") { - const std::vector indexTuples = Utility::String::splitWithoutEmptyParts(contents, ' '); - - /* Points */ - if(keyword == "p") { - /* Check that we don't mix the primitives in one mesh */ - if(primitive && primitive != MeshPrimitive::Points) { - Error() << "Trade::ObjImporter::mesh3D(): mixed primitive" << *primitive << "and" << MeshPrimitive::Points; - return Containers::NullOpt; - } + _state->materials.emplace_back(); + mat = &_state->materials.back(); + mat->name = std::string{word.data(), word.size()}; - /* Check vertex count per primitive */ - if(indexTuples.size() != 1) { - Error() << "Trade::ObjImporter::mesh3D(): wrong index count for point"; - return Containers::NullOpt; - } + _state->materialIds[mat->name] = _state->materials.size()-1; + continue; + } else if (mat == nullptr) { + Error() << "Expected newmtl keyword, got" << keyword; + } - primitive = MeshPrimitive::Points; + /* Ambient color */ + if(keyword == "Ka") { + ArrayView word; - /* Lines */ - } else if(keyword == "l") { - /* Check that we don't mix the primitives in one mesh */ - if(primitive && primitive != MeshPrimitive::Lines) { - Error() << "Trade::ObjImporter::mesh3D(): mixed primitive" << *primitive << "and" << MeshPrimitive::Lines; - return Containers::NullOpt; - } + for(Int i : {0, 1, 2}) { + std::tie(word, pos) = nextWord(pos); + mat->ambient[i] = strToFloat(word); + } - /* Check vertex count per primitive */ - if(indexTuples.size() != 2) { - Error() << "Trade::ObjImporter::mesh3D(): wrong index count for line"; - return Containers::NullOpt; - } + /* Diffuse color */ + } else if(keyword == "Kd") { + ArrayView word; - primitive = MeshPrimitive::Lines; + for(Int i : {0, 1, 2}) { + std::tie(word, pos) = nextWord(pos); + mat->diffuse[i] = strToFloat(word); + } - /* Faces */ - } else if(keyword == "f") { - /* Check that we don't mix the primitives in one mesh */ - if(primitive && primitive != MeshPrimitive::Triangles) { - Error() << "Trade::ObjImporter::mesh3D(): mixed primitive" << *primitive << "and" << MeshPrimitive::Triangles; - return Containers::NullOpt; - } + /* Specular color */ + } else if(keyword == "Ks") { + ArrayView word; - /* Check vertex count per primitive */ - if(indexTuples.size() < 3) { - Error() << "Trade::ObjImporter::mesh3D(): wrong index count for triangle"; - return Containers::NullOpt; - } else if(indexTuples.size() != 3) { - Error() << "Trade::ObjImporter::mesh3D(): polygons are not supported"; - return Containers::NullOpt; - } + for(Int i : {0, 1, 2}) { + std::tie(word, pos) = nextWord(pos); + mat->specular[i] = strToFloat(word); + } - primitive = MeshPrimitive::Triangles; + /* Specularity */ + } else if(keyword == "Ns") { + ArrayView word; - } else CORRADE_ASSERT_UNREACHABLE(); /* LCOV_EXCL_LINE */ + std::tie(word, pos) = nextWord(pos); + const Float f = strToFloat(word); - for(const std::string& indexTuple: indexTuples) { - std::vector indices = Utility::String::split(indexTuple, '/'); - if(indices.size() > 3) { - Error() << "Trade::ObjImporter::mesh3D(): invalid index data"; - return Containers::NullOpt; - } + mat->specularity = f; + /* Ambient texture */ + } else if(keyword.substr(0, 4) == "map_") { + ArrayView line; + std::tie(line, pos) = nextLine(pos); + std::string texture{line.data(), line.size()}; - /* Position indices */ - positionIndices.push_back(std::stoul(indices[0]) - positionIndexOffset); + 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); - /* Texture coordinates */ - if(indices.size() == 2 || (indices.size() == 3 && !indices[1].empty())) - textureCoordinateIndices.push_back(std::stoul(indices[1]) - textureCoordinateIndexOffset); + _state->textureIds[texture] = index; + textureId = index; + } else { + textureId = _state->textureIds[texture]; + } - /* Normal indices */ - if(indices.size() == 3) - normalIndices.push_back(std::stoul(indices[2]) - normalIndexOffset); + 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 { + Warning() << "Trade::ObjImporter::parseMaterialLibrary(): unsupported texture type:" << suffix; } /* Ignore unsupported keywords, error out on unknown keywords */ - } else if(![&keyword](){ - /* Using lambda to emulate for-else construct like in Python */ - for(const std::string expected: {"mtllib", "usemtl", "g", "s"}) - if(keyword == expected) return true; - return false; - }()) { - Error() << "Trade::ObjImporter::mesh3D(): unknown keyword" << keyword; - return Containers::NullOpt; + } else { + Warning() << "Trade::ObjImporter::parseMaterialLibrary(): unknown keyword:" << keyword; } - }} catch(std::exception) { - Error() << "Trade::ObjImporter::mesh3D(): error while converting numeric data"; - return Containers::NullOpt; - } catch(...) { - /* Error message already printed */ - return Containers::NullOpt; + /* Ignore the rest of the line */ + pos = skipWhitespaces(ignoreLine(pos)); + } +} + +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; } - /* There should be at least indexed position data */ - if(positions.empty() || positionIndices.empty()) { - Error() << "Trade::ObjImporter::mesh3D(): incomplete position data"; - return Containers::NullOpt; + result = _state->meshIds.find(name); + if(result != _state->meshIds.end()) { + return result->second; } - /* If there are index data, there should be also vertex data (and also the other way) */ - if(normals.empty() != normalIndices.empty()) { - Error() << "Trade::ObjImporter::mesh3D(): incomplete normal data"; - return Containers::NullOpt; + return -1; +} + +std::string ObjImporter::doObject3DName(UnsignedInt id) { + const size_t lastMesh = _state->meshes.size() - 1; + if(id > lastMesh) { + return _state->meshlessObjects[id - lastMesh]; } - if(textureCoordinates.empty() != textureCoordinateIndices.empty()) { - Error() << "Trade::ObjImporter::mesh3D(): incomplete texture coordinate data"; - return Containers::NullOpt; + return _state->meshes[id]->name(); +} + +std::unique_ptr ObjImporter::doObject3D(UnsignedInt id) { + const size_t lastMesh = _state->meshes.size() - 1; + if(id > lastMesh) { + return std::unique_ptr{new ObjectData3D{{}, {}, &_state->meshlessObjects[id - lastMesh]}}; } + const ObjMesh& mesh = *_state->meshes[id]; + return std::unique_ptr{ + new MeshObjectData3D{{}, {}, id, mesh.data.materialId, _state->meshes[id].get()}}; +} + +UnsignedInt ObjImporter::doMesh3DCount() const { return _state->meshes.size(); } + +Int ObjImporter::doMesh3DForName(const std::string& name) { + return _state->meshIds[name]; +} + +std::string ObjImporter::doMesh3DName(UnsignedInt id) { + return _state->meshes[id]->name(); +} + +std::optional ObjImporter::doMesh3D(UnsignedInt id) { + const ObjMesh& mesh = *_state->meshes[id]; - /* All index arrays should have the same length */ - if(!normalIndices.empty() && normalIndices.size() != positionIndices.size()) { - CORRADE_INTERNAL_ASSERT(normalIndices.size() < positionIndices.size()); - Error() << "Trade::ObjImporter::mesh3D(): some normal indices are missing"; - return Containers::NullOpt; + 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); + } } - if(!textureCoordinates.empty() && textureCoordinateIndices.size() != positionIndices.size()) { - CORRADE_INTERNAL_ASSERT(textureCoordinateIndices.size() < positionIndices.size()); - Error() << "Trade::ObjImporter::mesh3D(): some texture coordinate indices are missing"; - return Containers::NullOpt; + + // 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(); + } } /* Merge index arrays, if there aren't just the positions */ std::vector indices; - if(!normalIndices.empty() || !textureCoordinateIndices.empty()) { - std::vector>> arrays; - arrays.reserve(3); - arrays.emplace_back(positionIndices); - if(!normalIndices.empty()) arrays.emplace_back(normalIndices); - if(!textureCoordinateIndices.empty()) arrays.emplace_back(textureCoordinateIndices); - indices = MeshTools::combineIndexArrays(arrays); - - /* Reindex data arrays */ - try { - reindex(positionIndices, positions); - if(!normalIndices.empty()) reindex(normalIndices, normals.front()); - if(!textureCoordinateIndices.empty()) reindex(textureCoordinateIndices, textureCoordinates.front()); - } catch(...) { - /* Error message already printed */ - return Containers::NullOpt; - } - /* Otherwise just use the original position index array. Don't forget to - check range */ + 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 */ + 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)); + + return MeshData3D(mesh.primitive, std::move(indices), std::move(positionLayers), + std::move(normalLayers), std::move(texCoordLayers), {}, &mesh); +} + +UnsignedInt ObjImporter::doMaterialCount() const { return _state->materials.size(); } + +std::unique_ptr ObjImporter::doMaterial(const UnsignedInt id) { + ObjMaterial& objMat = _state->materials[id]; + PhongMaterialData::Flags flags; + + if(objMat.ambientTexture != -1) { + flags |= PhongMaterialData::Flag::AmbientTexture; + } + if(objMat.diffuseTexture != -1) { + flags |= PhongMaterialData::Flag::DiffuseTexture; + } + if(objMat.specularTexture != -1) { + flags |= PhongMaterialData::Flag::SpecularTexture; + } + + PhongMaterialData* mat = new PhongMaterialData{ + flags, + objMat.specularity}; + + if(objMat.ambientTexture == -1) { + mat->ambientColor() = objMat.ambient; } else { - indices = std::move(positionIndices); - for(UnsignedInt i: indices) if(i >= positions.size()) { - Error() << "Trade::ObjImporter::mesh3D(): index out of range"; - return Containers::NullOpt; - } + mat->ambientTexture() = objMat.ambientTexture; + } + + if(objMat.diffuseTexture == -1) { + mat->diffuseColor() = objMat.diffuse; + } else { + mat->diffuseTexture() = objMat.diffuseTexture; + } + + if(objMat.specularTexture == -1) { + mat->specularColor() = objMat.specular; + } else { + mat->specularTexture() = objMat.specularTexture; } - return MeshData3D{*primitive, std::move(indices), {std::move(positions)}, std::move(normals), std::move(textureCoordinates), {}, nullptr}; + return std::unique_ptr(mat); } +UnsignedInt ObjImporter::doImage2DCount() const { return _state->textures.size(); } + +std::optional ObjImporter::doImage2D(UnsignedInt id) { + CORRADE_ASSERT(manager(), "Trade::ObjImporter::image2D(): the plugin must be instantiated with access to plugin manager in order to open image files", {}); + + AnyImageImporter imageImporter = AnyImageImporter{manager()} + if(!imageImporter->openFile(_state->fileRoot + _state->textures[id])) { + return std::nullopt; + } + + return imageImporter->image2D(0); +} + + }} diff --git a/src/MagnumPlugins/ObjImporter/ObjImporter.h b/src/MagnumPlugins/ObjImporter/ObjImporter.h index 81ec77dce7..efc8d33651 100644 --- a/src/MagnumPlugins/ObjImporter/ObjImporter.h +++ b/src/MagnumPlugins/ObjImporter/ObjImporter.h @@ -33,6 +33,8 @@ #include "MagnumPlugins/ObjImporter/configure.h" +#include + #ifndef DOXYGEN_GENERATING_OUTPUT #ifndef MAGNUM_OBJIMPORTER_BUILD_STATIC #if defined(ObjImporter_EXPORTS) || defined(ObjImporterObjects_EXPORTS) @@ -93,14 +95,26 @@ 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; MAGNUM_OBJIMPORTER_LOCAL Containers::Optional doMesh3D(UnsignedInt id) override; - MAGNUM_OBJIMPORTER_LOCAL void parseMeshNames(); + MAGNUM_OBJIMPORTER_LOCAL UnsignedInt doMaterialCount() const override; + MAGNUM_OBJIMPORTER_LOCAL std::unique_ptr doMaterial(UnsignedInt id) override; + + MAGNUM_OBJIMPORTER_LOCAL UnsignedInt doImage2DCount() const; + MAGNUM_OBJIMPORTER_LOCAL std::optional doImage2D(UnsignedInt id); + + MAGNUM_OBJIMPORTER_LOCAL void parse(); + MAGNUM_OBJIMPORTER_LOCAL void parseMaterialLibrary(Containers::ArrayView libname); - std::unique_ptr _file; + std::unique_ptr _state; }; }} diff --git a/src/MagnumPlugins/ObjImporter/Test/Test.cpp b/src/MagnumPlugins/ObjImporter/Test/Test.cpp index ac39dec0df..3d6e8b359c 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(); @@ -83,6 +82,8 @@ struct ObjImporterTest: TestSuite::Tester { void missingNormalIndices(); void missingTextureCoordinateIndices(); + void multiMaterialObject(); + void wrongTextureCoordinateIndexCount(); void wrongNormalIndexCount(); @@ -102,7 +103,6 @@ ObjImporterTest::ObjImporterTest() { &ObjImporterTest::textureCoordinatesNormals, &ObjImporterTest::emptyFile, - &ObjImporterTest::unnamedMesh, &ObjImporterTest::namedMesh, &ObjImporterTest::moreMeshes, &ObjImporterTest::unnamedFirstMesh, @@ -134,6 +134,8 @@ ObjImporterTest::ObjImporterTest() { &ObjImporterTest::missingNormalIndices, &ObjImporterTest::missingTextureCoordinateIndices, + &ObjImporterTest::multiMaterialObject, + &ObjImporterTest::wrongTextureCoordinateIndexCount, &ObjImporterTest::wrongNormalIndexCount, @@ -152,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 })); } @@ -202,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() { @@ -257,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); @@ -274,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 })); } @@ -285,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); @@ -311,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() { @@ -353,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"); @@ -389,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() { @@ -406,8 +428,8 @@ 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"); + CORRADE_VERIFY(!importer.mesh3D(id)); } void ObjImporterTest::wrongInteger() { @@ -702,6 +724,31 @@ void ObjImporterTest::wrongTextureCoordinateIndexCount() { CORRADE_COMPARE(out.str(), "Trade::ObjImporter::mesh3D(): some texture coordinate indices are missing\n"); } +void ObjImporterTest::multiMaterialObject() { + ObjImporter importer; + CORRADE_VERIFY(importer.openFile(Utility::Directory::join(OBJIMPORTER_TEST_DIR, "multiMaterial.obj"))); + CORRADE_VERIFY(importer.mesh3DCount() == 2); + CORRADE_VERIFY(importer.materialCount() == 2); + + /* Everything should be parsed properly */ + std::optional data = importer.mesh3D(0); + CORRADE_VERIFY(data); + CORRADE_COMPARE(data->primitive(), MeshPrimitive::Triangles); + CORRADE_COMPARE(data->positionArrayCount(), 1); + CORRADE_COMPARE(data->positions(0), (std::vector{ + {{1.72414f, 18.9233f, -3.20162f}, + {2.74428f, -0.499733f, -3.50576f}, + {-1.92235f, -0.846268f, 2.9722f}, + {1.72414f, 18.9233f, -3.20162f}, + {-1.92235f, -0.846268f, 2.9722f}, + {2.43556f, 18.8755f, 2.23745f}} + })); + + data = importer.mesh3D(1); + CORRADE_VERIFY(data); + CORRADE_COMPARE(data->primitive(), MeshPrimitive::Triangles); +} + void ObjImporterTest::unsupportedKeyword() { ObjImporter importer; CORRADE_VERIFY(importer.openFile(Utility::Directory::join(OBJIMPORTER_TEST_DIR, "keywords.obj"))); diff --git a/src/MagnumPlugins/ObjImporter/Test/moreMeshes.obj b/src/MagnumPlugins/ObjImporter/Test/moreMeshes.obj index e4820d5b9f..0a779c0c03 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.mtl b/src/MagnumPlugins/ObjImporter/Test/multiMaterial.mtl new file mode 100644 index 0000000000..5a4b92bc7d --- /dev/null +++ b/src/MagnumPlugins/ObjImporter/Test/multiMaterial.mtl @@ -0,0 +1,9 @@ +newmtl mat_0 +Ka 1 1 1 +map_Ka Textures/mat_0.tga + + +newmtl mat_1 +Ka 1 1 1 +map_Ka Textures/mat_1.tga + diff --git a/src/MagnumPlugins/ObjImporter/Test/multiMaterial.obj b/src/MagnumPlugins/ObjImporter/Test/multiMaterial.obj new file mode 100644 index 0000000000..f809fe2fca --- /dev/null +++ b/src/MagnumPlugins/ObjImporter/Test/multiMaterial.obj @@ -0,0 +1,59 @@ +mtllib multiMaterial.mtl + +# Billboard + +# 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 +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 +vt 0 1 +vt 0 0 +vt 1 0 +vt 1 1 + +usemtl mat_0 + +# faces +f 1/1/1 2/2/2 3/3/3 +f 1/1/1 3/3/3 4/4/4 + + +# Billboard + +# 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 +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 +vt 0 1 +vt 0 0 +vt 1 0 +vt 1 1 + +usemtl mat_1 + +# 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 15711d5ca2..deb4a897f7 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 c1750ecb2e..3ab1cab91b 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 71752c6d98..7131e199b0 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