diff --git a/src/Magnum/Trade/Implementation/sceneTools.h b/src/Magnum/Trade/Implementation/sceneTools.h index 98f3e41b5..db41c1373 100644 --- a/src/Magnum/Trade/Implementation/sceneTools.h +++ b/src/Magnum/Trade/Implementation/sceneTools.h @@ -28,9 +28,11 @@ #include #include #include +#include #include #include +#include "Magnum/Math/Functions.h" #include "Magnum/Math/PackingBatch.h" #include "Magnum/Trade/SceneData.h" @@ -188,6 +190,119 @@ inline SceneData sceneCombine(const SceneObjectType objectType, const UnsignedLo return SceneData{objectType, objectCount, std::move(outData), std::move(outFields)}; } +/* Creates a SceneData copy where each object has at most one of the fields + listed in the passed array. This is done by enlarging the parents array + and moving extraneous features to new objects that are marked as a child of + the original. No transformations or other fields are added for the new + objects. Fields that are connected together (such as meshes and materials) + are assumed to share the same object mapping with only one of them passed in + the fieldsToConvert array, which will result for all fields from the same + set being reassociated to the new object. + + Requies a SceneField::Parent to be present -- otherwise it wouldn't be + possible to know where to attach the new objects. */ +/** @todo when published, (again) add an initializer_list overload and turn all + internal asserts into (tested!) message asserts */ +inline SceneData sceneConvertToSingleFunctionObjects(const SceneData& scene, Containers::ArrayView fieldsToConvert, const UnsignedInt newObjectOffset) { + /** @todo assert for really high object counts (where this cast would fail) */ + Containers::Array objectAttachmentCount{ValueInit, std::size_t(scene.objectCount())}; + for(const SceneField field: fieldsToConvert) { + CORRADE_INTERNAL_ASSERT(field != SceneField::Parent); + + /* Skip fields that are not present -- is it's not present, then it + definitely won't be responsible for multi-function objects */ + const Containers::Optional fieldId = scene.findFieldId(field); + if(!fieldId) continue; + + /** @todo use a statically-allocated array & Into() in a loop instead + once this is more than a private backwards-compatibility utility + where PERF WHATEVER WHO CARES */ + for(const UnsignedInt object: scene.objectsAsArray(*fieldId)) { + CORRADE_INTERNAL_ASSERT(object < objectAttachmentCount.size()); + ++objectAttachmentCount[object]; + } + } + + UnsignedInt objectsToAdd = 0; + for(const UnsignedInt count: objectAttachmentCount) + if(count > 1) objectsToAdd += count - 1; + + /* Ensure we don't overflow the 32-bit object count with the objects to + add. This should also cover the case when the parent field would not be + representable in 32 bits. */ + CORRADE_INTERNAL_ASSERT(newObjectOffset + objectsToAdd >= newObjectOffset); + + /* Copy the fields over, enlarging them as necessary */ + const UnsignedInt parentFieldId = scene.fieldId(SceneField::Parent); + Containers::Array fields{scene.fieldCount()}; + for(std::size_t i = 0; i != scene.fieldCount(); ++i) { + const SceneFieldData& field = scene.fieldData(i); + + /* If this is a parent, enlarge it for the newly added objects */ + if(field.name() == SceneField::Parent) { + /** @todo some nicer constructor for placeholders once this is in + public interest */ + fields[i] = SceneFieldData{SceneField::Parent, Containers::ArrayView{nullptr, std::size_t(field.size() + objectsToAdd)}, Containers::ArrayView{nullptr, std::size_t(field.size() + objectsToAdd)}}; + + /* All other fields are copied as-is */ + } else fields[i] = field; + } + + /* Combine the fields into a new SceneData */ + SceneData out = sceneCombine(SceneObjectType::UnsignedInt, Math::max(scene.objectCount(), UnsignedLong(newObjectOffset) + objectsToAdd), fields); + + /* Copy existing parent object/field data to a prefix of the output */ + const Containers::StridedArrayView1D outParentObjects = out.mutableObjects(parentFieldId); + const Containers::StridedArrayView1D outParents = out.mutableField(parentFieldId); + CORRADE_INTERNAL_ASSERT_OUTPUT(scene.objectsInto(parentFieldId, 0, outParentObjects) == scene.fieldSize(parentFieldId)); + CORRADE_INTERNAL_ASSERT_OUTPUT(scene.parentsInto(0, outParents) == scene.fieldSize(parentFieldId)); + + /* List new objects at the end of the extended parent field */ + const Containers::StridedArrayView1D newParentObjects = outParentObjects.suffix(scene.fieldSize(parentFieldId)); + const Containers::StridedArrayView1D newParents = outParents.suffix(scene.fieldSize(parentFieldId)); + for(std::size_t i = 0; i != newParentObjects.size(); ++i) { + newParentObjects[i] = newObjectOffset + i; + newParents[i] = -1; + } + + /* Clear the objectAttachmentCount array to reuse it below */ + /** @todo use a BitArray instead once it exists? */ + constexpr UnsignedInt zero[1]{}; + Utility::copy(Containers::stridedArrayView(zero).broadcasted<0>(scene.objectCount()), objectAttachmentCount); + + /* For objects with multiple fields move the extra fields to newly added + children */ + { + std::size_t newParentIndex = 0; + for(const SceneField field: fieldsToConvert) { + const Containers::Optional fieldId = scene.findFieldId(field); + if(!fieldId) continue; + + for(UnsignedInt& fieldObject: out.mutableObjects(*fieldId)) { + /* If the object is not new (could happen when an object + mapping array is shared among multiple fields, in which case + it *might* have been updated already to an ID larger than + the mapping array size) and it already has something + attached, then attach the field to a new object and make + that new object a child of the previous one. */ + if(fieldObject < objectAttachmentCount.size() && objectAttachmentCount[fieldObject]) { + /* Find an index of the old object and then use that index + to denote the parent of the new object */ + newParents[newParentIndex] = out.fieldObjectOffset(parentFieldId, fieldObject); + /* Assign the field to the new object */ + fieldObject = newParentObjects[newParentIndex]; + /* Move to the next reserved object */ + ++newParentIndex; + } else ++objectAttachmentCount[fieldObject]; + } + } + + CORRADE_INTERNAL_ASSERT(newParentIndex == objectsToAdd); + } + + return out; +} + }}} #endif diff --git a/src/Magnum/Trade/Test/SceneToolsTest.cpp b/src/Magnum/Trade/Test/SceneToolsTest.cpp index f5fda2dfa..983ac8006 100644 --- a/src/Magnum/Trade/Test/SceneToolsTest.cpp +++ b/src/Magnum/Trade/Test/SceneToolsTest.cpp @@ -41,6 +41,8 @@ struct SceneToolsTest: TestSuite::Tester { void combineObjectsShared(); void combineObjectsPlaceholderFieldPlaceholder(); void combineObjectSharedFieldPlaceholder(); + + void convertToSingleFunctionObjects(); }; struct { @@ -53,6 +55,15 @@ struct { {"UnsignedLong output", SceneObjectType::UnsignedLong}, }; +struct { + const char* name; + UnsignedLong originalObjectCount; + UnsignedLong expectedObjectCount; +} ConvertToSingleFunctionObjectsData[]{ + {"original object count smaller than new", 64, 67}, + {"original object count larger than new", 96, 96} +}; + SceneToolsTest::SceneToolsTest() { addInstancedTests({&SceneToolsTest::combine}, Containers::arraySize(CombineData)); @@ -61,6 +72,9 @@ SceneToolsTest::SceneToolsTest() { &SceneToolsTest::combineObjectsShared, &SceneToolsTest::combineObjectsPlaceholderFieldPlaceholder, &SceneToolsTest::combineObjectSharedFieldPlaceholder}); + + addInstancedTests({&SceneToolsTest::convertToSingleFunctionObjects}, + Containers::arraySize(ConvertToSingleFunctionObjectsData)); } using namespace Math::Literals; @@ -319,6 +333,124 @@ void SceneToolsTest::combineObjectSharedFieldPlaceholder() { CORRADE_COMPARE(scene.field(SceneField::MeshMaterial).stride()[0], 4); } +void SceneToolsTest::convertToSingleFunctionObjects() { + auto&& data = ConvertToSingleFunctionObjectsData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + /* Haha now I can use sceneCombine() to conveniently prepare the initial + state here, without having to mess with an ArrayTuple */ + + const UnsignedShort parentObjects[]{15, 21, 22, 23, 1}; + const Byte parents[]{-1, -1, 1, 2, -1}; + + /* Two objects have two and three mesh assignments respectively, meaning we + need three extra */ + const UnsignedShort meshObjects[]{15, 23, 23, 23, 1, 15, 21}; + const Containers::Pair meshesMaterials[]{ + {6, 4}, + {1, 0}, + {2, 3}, + {4, 2}, + {7, 2}, + {3, 1}, + {5, -1} + }; + + /* One camera is attached to an object that already has a mesh, meaning we + need a third extra object */ + const UnsignedShort cameraObjects[]{22, 1}; + const UnsignedInt cameras[]{1, 5}; + SceneData original = Implementation::sceneCombine(SceneObjectType::UnsignedShort, data.originalObjectCount, Containers::arrayView({ + SceneFieldData{SceneField::Parent, Containers::arrayView(parentObjects), Containers::arrayView(parents)}, + SceneFieldData{SceneField::Mesh, Containers::arrayView(meshObjects), Containers::StridedArrayView1D{meshesMaterials, &meshesMaterials[0].first(), Containers::arraySize(meshesMaterials), sizeof(meshesMaterials[0])}}, + SceneFieldData{SceneField::MeshMaterial, Containers::arrayView(meshObjects), Containers::StridedArrayView1D{meshesMaterials, &meshesMaterials[0].second(), Containers::arraySize(meshesMaterials), sizeof(meshesMaterials[0])}}, + SceneFieldData{SceneField::Camera, Containers::arrayView(cameraObjects), Containers::arrayView(cameras)}, + })); + + SceneData scene = Implementation::sceneConvertToSingleFunctionObjects(original, Containers::arrayView({ + SceneField::Mesh, + /* Deliberately not including MeshMaterial in the list -- these should + get automatically updated as they share the same object mapping. + OTOH including them would break the output. */ + SceneField::Camera, + /* Include also a field that's not present -- it should get skipped */ + SceneField::ImporterState + }), 63); + + /* There should be three more objects, or the original count preserved if + it's large enough */ + CORRADE_COMPARE(scene.objectCount(), data.expectedObjectCount); + + /* Object 1 should have a new child that has the camera, as it has a mesh */ + CORRADE_COMPARE_AS(scene.childrenFor(1), + Containers::arrayView({66}), + TestSuite::Compare::Container); + CORRADE_COMPARE_AS(scene.meshesMaterialsFor(1), + (Containers::arrayView>({{7, 2}})), + TestSuite::Compare::Container); + CORRADE_COMPARE_AS(scene.camerasFor(1), + Containers::arrayView({}), + TestSuite::Compare::Container); + CORRADE_COMPARE_AS(scene.camerasFor(66), + Containers::arrayView({5}), + TestSuite::Compare::Container); + + /* Object 15 should have a new child that has the second mesh */ + CORRADE_COMPARE_AS(scene.childrenFor(15), + Containers::arrayView({65}), + TestSuite::Compare::Container); + CORRADE_COMPARE_AS(scene.meshesMaterialsFor(15), + (Containers::arrayView>({{6, 4}})), + TestSuite::Compare::Container); + CORRADE_COMPARE_AS(scene.meshesMaterialsFor(65), + (Containers::arrayView>({{3, 1}})), + TestSuite::Compare::Container); + + /* Object 23 should have two new children that have the second and third + mesh */ + CORRADE_COMPARE_AS(scene.childrenFor(23), + Containers::arrayView({63, 64}), + TestSuite::Compare::Container); + CORRADE_COMPARE_AS(scene.meshesMaterialsFor(23), + (Containers::arrayView>({{1, 0}})), + TestSuite::Compare::Container); + CORRADE_COMPARE_AS(scene.meshesMaterialsFor(63), + (Containers::arrayView>({{2, 3}})), + TestSuite::Compare::Container); + CORRADE_COMPARE_AS(scene.meshesMaterialsFor(64), + (Containers::arrayView>({{4, 2}})), + TestSuite::Compare::Container); + + /* To be extra sure, verify the actual data. Parents have a few objects + added, the rest is the same */ + CORRADE_COMPARE_AS(scene.objectsAsArray(SceneField::Parent), Containers::arrayView({ + 15, 21, 22, 23, 1, 63, 64, 65, 66 + }), TestSuite::Compare::Container); + CORRADE_COMPARE_AS(scene.parentsAsArray(), Containers::arrayView({ + -1, -1, 1, 2, -1, 3, 3, 0, 4 + }), TestSuite::Compare::Container); + + /* Meshes have certain objects reassigned (and materials as well, as they + share the same object mapping view), field data stay the same */ + CORRADE_COMPARE_AS(scene.objectsAsArray(SceneField::Mesh), Containers::arrayView({ + 15, 23, 63, 64, 1, 65, 21 + }), TestSuite::Compare::Container); + CORRADE_COMPARE_AS(scene.objectsAsArray(SceneField::MeshMaterial), Containers::arrayView({ + 15, 23, 63, 64, 1, 65, 21 + }), TestSuite::Compare::Container); + CORRADE_COMPARE_AS(scene.meshesMaterialsAsArray(), + Containers::arrayView(meshesMaterials), + TestSuite::Compare::Container); + + /* Cameras have certain objects reassigned, field data stay the same */ + CORRADE_COMPARE_AS(scene.objectsAsArray(SceneField::Camera), Containers::arrayView({ + 22, 66 + }), TestSuite::Compare::Container); + CORRADE_COMPARE_AS(scene.camerasAsArray(), + Containers::arrayView(cameras), + TestSuite::Compare::Container); +} + }}}} CORRADE_TEST_MAIN(Magnum::Trade::Test::SceneToolsTest)