diff --git a/doc/changelog.dox b/doc/changelog.dox index 0e3981ddb..34c758338 100644 --- a/doc/changelog.dox +++ b/doc/changelog.dox @@ -1097,7 +1097,7 @@ See also: consistent with @ref Trade::SceneData::findFieldId(). - Added @ref Trade::MeshData::attributeId() that returns ID of an attribute in a set of attributes of the same name -- @relativeref{Trade,ObjImporter} now supports negative indices +- @relativeref{Trade,ObjImporter} now supports quads and negative indices - Added @ref Trade::TextureType::Texture1DArray, @relativeref{Trade::TextureType,Texture2DArray} and @relativeref{Trade::TextureType,CubeMapArray} in order to be able to diff --git a/src/MagnumPlugins/ObjImporter/ObjImporter.cpp b/src/MagnumPlugins/ObjImporter/ObjImporter.cpp index e5c5af798..c5ba91e68 100644 --- a/src/MagnumPlugins/ObjImporter/ObjImporter.cpp +++ b/src/MagnumPlugins/ObjImporter/ObjImporter.cpp @@ -383,18 +383,19 @@ Containers::Optional ObjImporter::doMesh(const UnsignedInt id, Unsigne /* Indices */ } else if(keyword == "p"_s || keyword == "l"_s || keyword == "f"_s) { - /* Decide on how many tuples we expect */ - std::size_t indexTupleCount; - if(keyword == "p"_s) indexTupleCount = 1; - else if(keyword == "l"_s) indexTupleCount = 2; - else if(keyword == "f"_s) indexTupleCount = 3; + /* Decide on how many tuples we expect. Since we handle both + triangles and quads, it can't be an exact count. */ + std::size_t maxIndexTupleCount; + if(keyword == "p"_s) maxIndexTupleCount = 1; + else if(keyword == "l"_s) maxIndexTupleCount = 2; + else if(keyword == "f"_s) maxIndexTupleCount = 4; else CORRADE_INTERNAL_ASSERT_UNREACHABLE(); /* LCOV_EXCL_LINE */ /* Parse them all. If there's less than expected, `i` would be too small; if there's more then `contents` would stay non-empty. */ - Vector3ui data[3]; + Vector3ui data[4]; std::size_t i = 0; - for(; i != indexTupleCount && contents; ++i) { + for(; i != maxIndexTupleCount && contents; ++i) { const Containers::StringView foundSpace = contents.findAnyOr(Whitespace, contents.end()); Containers::StringView indexTuple = contents.prefix(foundSpace.begin()); @@ -457,6 +458,9 @@ Containers::Optional ObjImporter::doMesh(const UnsignedInt id, Unsigne primitive = MeshPrimitive::Points; + /** @todo fix arrayAppend() to not need the cast here */ + arrayAppend(indices, Containers::ArrayView{data}.prefix(1)); + /* Lines */ } else if(keyword == "l") { if(primitive && primitive != MeshPrimitive::Lines) { @@ -470,6 +474,9 @@ Containers::Optional ObjImporter::doMesh(const UnsignedInt id, Unsigne primitive = MeshPrimitive::Lines; + /** @todo fix arrayAppend() to not need the cast here */ + arrayAppend(indices, Containers::ArrayView{data}.prefix(2)); + /* Faces */ } else if(keyword == "f") { if(primitive && primitive != MeshPrimitive::Triangles) { @@ -477,17 +484,45 @@ Containers::Optional ObjImporter::doMesh(const UnsignedInt id, Unsigne return Containers::NullOpt; } if(i < 3 || contents) { - Error() << "Trade::ObjImporter::mesh(): expected exactly 3 position index tuples for a triangle, got" << line.suffix(keywordEnd.end()); + Error() << "Trade::ObjImporter::mesh(): expected 3 or 4 position index tuples for a face, got" << line.suffix(keywordEnd.end()); return Containers::NullOpt; } + /* If it's a quad, convert it to two triangles */ + if(i == 4) { + /** @todo use MeshTools::generateQuadIndices() once it + can take extra index data into account */ + /* 0 0---3 + |\ \ | + | \ \ | + | \ \| + 1---2 2 */ + arrayAppend(indices, { + data[0], + data[1], + data[2], + data[0], + data[2], + data[3] + }); + + /* If we have texture coordinate / normal indices, add two + more to the counters as well. If they matched the index + array size before, they'll continue to match; if they + didn't, they'll continue to not match. */ + if(textureCoordinateIndexCount) + textureCoordinateIndexCount += 2; + if(normalIndexCount) + normalIndexCount += 2; + } else { + /** @todo fix arrayAppend() to not need the cast here */ + arrayAppend(indices, Containers::ArrayView{data}.prefix(3)); + } + primitive = MeshPrimitive::Triangles; } else CORRADE_INTERNAL_ASSERT_UNREACHABLE(); /* LCOV_EXCL_LINE */ - /** @todo fix arrayAppend() to not need the cast here */ - arrayAppend(indices, Containers::ArrayView{data}.prefix(i)); - /* Unknown keyword */ } else { Error{} << "Trade::ObjImporter::mesh(): unknown keyword" << keyword; diff --git a/src/MagnumPlugins/ObjImporter/ObjImporter.h b/src/MagnumPlugins/ObjImporter/ObjImporter.h index 60be371dd..ff7e6ed00 100644 --- a/src/MagnumPlugins/ObjImporter/ObjImporter.h +++ b/src/MagnumPlugins/ObjImporter/ObjImporter.h @@ -108,9 +108,10 @@ positions with optional @ref VertexFormat::Vector3 normals and Negative indices (where @cpp -1 @ce is the last position / texture coordinate / normal known at given point in the file, @cpp -2 @ce is the second-to-last, etc.), produced for example by 3ds Max or [Mineways](http://mineways.com), are +supported. Quads are converted to two triangles, higher-order polygons are not supported. -Polygons (quads etc.) and material properties are currently not supported. +Material properties are currently not supported. */ class MAGNUM_OBJIMPORTER_EXPORT ObjImporter: public AbstractImporter { public: diff --git a/src/MagnumPlugins/ObjImporter/Test/CMakeLists.txt b/src/MagnumPlugins/ObjImporter/Test/CMakeLists.txt index 138af9d11..7366d23cd 100644 --- a/src/MagnumPlugins/ObjImporter/Test/CMakeLists.txt +++ b/src/MagnumPlugins/ObjImporter/Test/CMakeLists.txt @@ -66,6 +66,7 @@ corrade_add_test(ObjImporterTest ObjImporterTest.cpp mesh-primitive-lines.obj mesh-primitive-points.obj mesh-primitive-triangles.obj + mesh-quads.obj mesh-texture-coordinates.obj mesh-texture-coordinates-normals.obj mesh-texture-coordinates-optional-coordinate.obj) diff --git a/src/MagnumPlugins/ObjImporter/Test/ObjImporterTest.cpp b/src/MagnumPlugins/ObjImporter/Test/ObjImporterTest.cpp index 376eb64eb..4913470df 100644 --- a/src/MagnumPlugins/ObjImporter/Test/ObjImporterTest.cpp +++ b/src/MagnumPlugins/ObjImporter/Test/ObjImporterTest.cpp @@ -57,6 +57,7 @@ struct ObjImporterTest: TestSuite::Tester { void meshTextureCoordinatesNormals(); void meshNegativeIndices(); + void meshQuads(); void meshIgnoredKeyword(); @@ -153,8 +154,8 @@ const struct { {"four-component index tuple", "invalid integer literal 1/1"}, {"point with two indices", "expected exactly 1 position index tuple for a point, got 9 9"}, {"line with one index", "expected exactly 2 position index tuples for a line, got 10"}, - {"triangle with two indices", "expected exactly 3 position index tuples for a triangle, got 11 11"}, - {"quad", "expected exactly 3 position index tuples for a triangle, got 12 12 12 12"} + {"triangle with two indices", "expected 3 or 4 position index tuples for a face, got 11 11"}, + {"five-vertex face", "expected 3 or 4 position index tuples for a face, got 12 12 12 12 12"} }; const struct { @@ -199,6 +200,7 @@ ObjImporterTest::ObjImporterTest() { &ObjImporterTest::meshTextureCoordinatesNormals, &ObjImporterTest::meshNegativeIndices, + &ObjImporterTest::meshQuads, &ObjImporterTest::meshIgnoredKeyword, @@ -509,6 +511,63 @@ void ObjImporterTest::meshNegativeIndices() { }), TestSuite::Compare::Container); } +void ObjImporterTest::meshQuads() { + Containers::Pointer importer = _manager.instantiate("ObjImporter"); + CORRADE_VERIFY(importer->openFile(Utility::Path::join(OBJIMPORTER_TEST_DIR, "mesh-quads.obj"))); + CORRADE_COMPARE(importer->meshCount(), 1); + + /* 1 1 + / \ / \ + / \ / \ + 2 --- 5 2 --- 3 + | | -> 4 --- 7 + | | | | + 3 --- 4 5 --- 6 */ + const Containers::Optional data = importer->mesh(0); + CORRADE_VERIFY(data); + CORRADE_COMPARE(data->primitive(), MeshPrimitive::Triangles); + CORRADE_COMPARE(data->attributeCount(), 3); + CORRADE_COMPARE_AS(data->attribute(MeshAttribute::Position), + Containers::arrayView({ + {0.0f, 3.0f, 0.0f}, + {-1.0f, 1.0f, 0.0f}, + {1.0f, 1.0f, 0.0f}, + + {-1.0f, 1.0f, 0.0f}, + {-1.0f, -1.0f, 0.0f}, + {1.0f, -1.0f, 0.0f}, + {1.0f, 1.0f, 0.0f}, + }), TestSuite::Compare::Container); + CORRADE_COMPARE_AS(data->attribute(MeshAttribute::TextureCoordinates), + Containers::arrayView({ + {0.5f, 1.0f}, + {0.0f, 0.5f}, + {1.0f, 0.5f}, + + {0.0f, 0.5f}, + {0.0f, 0.0f}, + {1.0f, 0.0f}, + {1.0f, 0.5f}, + }), TestSuite::Compare::Container); + CORRADE_COMPARE_AS(data->attribute(MeshAttribute::Normal), + Containers::arrayView({ + {0.0f, 1.0f, 0.0f}, + {-1.0f, 0.0f, 0.0f}, + {1.0f, 0.0f, 0.0f}, + + {0.0f, -1.0f, 0.0f}, + {0.0f, -1.0f, 0.0f}, + {0.0f, -1.0f, 0.0f}, + {0.0f, -1.0f, 0.0f}, + }), TestSuite::Compare::Container); + CORRADE_VERIFY(data->isIndexed()); + CORRADE_COMPARE(data->indexType(), MeshIndexType::UnsignedInt); + CORRADE_COMPARE_AS(data->indices(), + Containers::arrayView({ + 0, 1, 2, 3, 4, 5, 3, 5, 6 + }), TestSuite::Compare::Container); +} + void ObjImporterTest::meshIgnoredKeyword() { Containers::Pointer importer = _manager.instantiate("ObjImporter"); CORRADE_VERIFY(importer->openFile(Utility::Path::join(OBJIMPORTER_TEST_DIR, "mesh-ignored-keyword.obj"))); diff --git a/src/MagnumPlugins/ObjImporter/Test/invalid-number-count.obj b/src/MagnumPlugins/ObjImporter/Test/invalid-number-count.obj index a5b682b06..f2324dc03 100644 --- a/src/MagnumPlugins/ObjImporter/Test/invalid-number-count.obj +++ b/src/MagnumPlugins/ObjImporter/Test/invalid-number-count.obj @@ -57,6 +57,6 @@ o triangle with two indices v 1 2 3 f 11 11 -o quad +o five-vertex face v 1 2 3 -f 12 12 12 12 +f 12 12 12 12 12 diff --git a/src/MagnumPlugins/ObjImporter/Test/mesh-quads.obj b/src/MagnumPlugins/ObjImporter/Test/mesh-quads.obj new file mode 100644 index 000000000..ed3404dc1 --- /dev/null +++ b/src/MagnumPlugins/ObjImporter/Test/mesh-quads.obj @@ -0,0 +1,31 @@ +# A house. +# +# 1 +# / \ +# / \ +# 2 --- 5 +# | | +# | | +# 3 --- 4 + +v 0 3 0 +v -1 1 0 +v -1 -1 0 +v 1 -1 0 +v 1 1 0 + +vt 0.5 1 +vt 0 0.5 +vt 0 0 +vt 1 0 +vt 1 0.5 + +# Normals of the roof are pointing up, to the left and to the right +vn 0 1 0 +vn -1 0 0 +vn 1 0 0 +f 1/1/1 2/2/2 5/5/3 + +# Normals of the walls are all pointing down +vn 0 -1 0 +f 2/2/4 3/3/4 4/4/4 5/5/4