/* This file is part of Magnum. Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022 Vladimír Vondruš Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ #include #include #include #include #include #include /** @todo remove once Arguments is std::string-free */ #include #include #include "Magnum/MeshTools/Concatenate.h" #include "Magnum/MeshTools/Reference.h" #include "Magnum/MeshTools/RemoveDuplicates.h" #include "Magnum/MeshTools/Transform.h" #include "Magnum/SceneTools/FlattenMeshHierarchy.h" #include "Magnum/Trade/AbstractImporter.h" #include "Magnum/Trade/MeshData.h" #include "Magnum/Trade/AbstractImageConverter.h" #include "Magnum/Trade/AbstractSceneConverter.h" #include "Magnum/Implementation/converterUtilities.h" #include "Magnum/SceneTools/Implementation/sceneConverterUtilities.h" namespace Magnum { /** @page magnum-sceneconverter Scene conversion utility @brief Converts scenes of different formats @m_since{2020,06} @tableofcontents @m_footernavigation @m_keywords{magnum-sceneconverter sceneconverter} This utility is built if `MAGNUM_WITH_SCENECONVERTER` is enabled when building Magnum. To use this utility with CMake, you need to request the `sceneconverter` component of the `Magnum` package and use the `Magnum::sceneconverter` target for example in a custom command: @code{.cmake} find_package(Magnum REQUIRED imageconverter) add_custom_command(OUTPUT ... COMMAND Magnum::sceneconverter ...) @endcode See @ref building and @ref cmake and the @ref Trade namespace for more information. There's also a corresponding @ref magnum-imageconverter "image conversion utility". @section magnum-sceneconverter-example Example usage Listing contents of a glTF file, implicitly using @relativeref{Trade,AnySceneImporter} that delegates to @relativeref{Trade,GltfImporter}, @relativeref{Trade,AssimpImporter} or @ref file-formats "any other plugin capable of glTF import" depending on what's available: @m_class{m-code-figure} @parblock @code{.sh} magnum-sceneconverter --info Box.gltf @endcode @m_class{m-nopad} @include sceneconverter-info.ansi @endparblock Converting an OBJ file to a PLY, implicitly using @relativeref{Trade,AnySceneConverter} that delegates to @relativeref{Trade,StanfordSceneConverter} or @ref file-formats "any other plugin capable of PLY export" depending on what's available: @code{.sh} magnum-sceneconverter chair.obj chair.ply @endcode Processing an OBJ file with @relativeref{Trade,MeshOptimizerSceneConverter}, setting @ref Trade-MeshOptimizerSceneConverter-configuration "plugin-specific configuration options" to reduce the index count to half, saving as a PLY, with verbose output showing the processing stats: @code{.sh} magnum-sceneconverter chair.obj -C MeshOptimizerSceneConverter \ -c simplify=true,simplifyTargetIndexCountThreshold=0.5 chair.ply -v @endcode @section magnum-sceneconverter-usage Full usage documentation @code{.sh} magnum-sceneconverter [-h|--help] [-I|--importer PLUGIN] [-C|--converter PLUGIN]... [-M|--mesh-converter PLUGIN]... [--plugin-dir DIR] [--map] [--only-mesh-attributes N1,N2-N3…] [--remove-duplicate-vertices] [--remove-duplicate-vertices-fuzzy EPSILON] [-i|--importer-options key=val,key2=val2,…] [-c|--converter-options key=val,key2=val2,…]... [-m|--mesh-converter-options key=val,key2=val2,…]... [--mesh ID] [--mesh-level INDEX] [--concatenate-meshes] [--info-animations] [--info-images] [--info-lights] [--info-cameras] [--info-materials] [--info-meshes] [--info-objects] [--info-scenes] [--info-skins] [--info-textures] [--info] [--color on|4bit|off|auto] [--bounds] [-v|--verbose] [--profile] [--] input output @endcode Arguments: - `input` --- input file - `output` --- output file; ignored if `--info` is present - `-h`, `--help` --- display this help message and exit - `-I`, `--importer PLUGIN` --- scene importer plugin (default: @ref Trade::AnySceneImporter "AnySceneImporter") - `-C`, `--converter PLUGIN` --- scene converter plugin(s) - `-M`, `--mesh-converter PLUGIN` --- converter plugin(s) to apply to each mesh in the scene - `--plugin-dir DIR` --- override base plugin dir - `--map` --- memory-map the input for zero-copy import (works only for standalone files) - `--only-mesh-attributes N1,N2-N3…` --- include only mesh attributes of given IDs in the output. See @ref Utility::String::parseNumberSequence() for syntax description. - `--remove-duplicate-vertices` --- remove duplicate vertices using @ref MeshTools::removeDuplicates(const Trade::MeshData&) in all meshes after import - `--remove-duplicate-vertices-fuzzy EPSILON` --- remove duplicate vertices using @ref MeshTools::removeDuplicatesFuzzy(const Trade::MeshData&, Float, Double) in all meshes after import - `-i`, `--importer-options key=val,key2=val2,…` --- configuration options to pass to the importer - `-c`, `--converter-options key=val,key2=val2,…` --- configuration options to pass to scene converter(s) - `-m`, `--mesh-converter-options key=val,key2=val2,…` --- configuration options to pass to mesh converter(s) - `--mesh ID` --- convert just a single mesh instead of the whole scene - `--mesh-level LEVEL` --- level to select for single-mesh conversion - `--concatenate-meshes` -- flatten mesh hierarchy and concatenate them all together @m_class{m-label m-warning} **experimental** - `--info-animations` --- print into about animations in the input file and exit - `--info-images` --- print into about images in the input file and exit - `--info-lights` --- print into about lights in the input file and exit - `--info-cameras` --- print into about cameras in the input file and exit - `--info-materials` --- print into about materials in the input file and exit - `--info-meshes` --- print into about meshes in the input file and exit - `--info-objects` --- print into about objects in the input file and exit - `--info-scenes` --- print into about scenes in the input file and exit - `--info-skins` --- print into about skins in the input file and exit - `--info-textures` --- print into about textures in the input file and exit - `--info` --- print info about everything in the input file and exit, same as specifying all other `--info-*` options together - `--color` --- colored output for `--info` (default: `auto`) - `--bounds` --- show bounds of known attributes in `--info` output - `-v`, `--verbose` --- verbose output from importer and converter plugins - `--profile` --- measure import and conversion time If any of the `--info-*` options are given, the utility will print information about given data present in the file. In this case no conversion is done and output file doesn't need to be specified. In case one data references another and both `--info-*` options are specified, the output will also list reference count (for example, `--info-scenes` together with `--info-meshes` will print how many objects reference given mesh). The `-i`, `-c` and `-m` arguments accept a comma-separated list of key/value pairs to set in the importer / converter plugin configuration. If the `=` character is omitted, it's equivalent to saying `key=true`; configuration subgroups are delimited with `/`. It's possible to specify the `-C` option (and correspondingly also `-c`) multiple times in order to chain more converters together. All converters in the chain have to support the @ref Trade::SceneConverterFeature::ConvertMultiple or @relativeref{Trade::SceneCOnverterFeature,ConvertMesh} feature, the last converter either @ref Trade::SceneConverterFeature::ConvertMultiple, @relativeref{Trade::SceneCOnverterFeature,ConvertMesh}, @relativeref{Trade::SceneCOnverterFeature,ConvertMultipleToFile} or @relativeref{Trade::SceneCOnverterFeature,ConvertMeshToFile}. If the last converter doesn't support conversion to a file, @relativeref{Trade,AnySceneConverter} is used to save its output. If no `-C` is specified, @relativeref{Trade,AnySceneConverter} is used. Similarly, the `-M` option (and correspondingly also `-m`) can be specified multiple times in order to chain more mesh converters together. All mesh converters in the chain have to support the ConvertMesh feature. If no `-M` is specified, the imported meshes are passed directly to the scene converter. The `--remove-duplicate-vertices` operations are performed before passing them to any converter. If `--concatenate-meshes` is given, all meshes of the input file are first concatenated into a single mesh using @ref MeshTools::concatenate(), with the scene hierarchy transformation baked in using @ref SceneTools::flattenMeshHierarchy3D(), and then passed through the remaining operations. Only attributes that are present in the first mesh are taken, if `--only-mesh-attributes` is specified as well, the IDs reference attributes of the first mesh. */ } using namespace Magnum; using namespace Containers::Literals; namespace { bool isInfoRequested(const Utility::Arguments& args) { return args.isSet("info-animations") || args.isSet("info-images") || args.isSet("info-lights") || args.isSet("info-cameras") || args.isSet("info-materials") || args.isSet("info-meshes") || args.isSet("info-objects") || args.isSet("info-scenes") || args.isSet("info-skins") || args.isSet("info-textures") || args.isSet("info"); } } int main(int argc, char** argv) { Utility::Arguments args; args.addArgument("input").setHelp("input", "input file") .addArgument("output").setHelp("output", "output file; ignored if --info is present") .addOption('I', "importer", "AnySceneImporter").setHelp("importer", "scene importer plugin", "PLUGIN") .addArrayOption('C', "converter").setHelp("converter", "scene converter plugin(s)", "PLUGIN") .addArrayOption('M', "mesh-converter").setHelp("mesh-converter", "converter plugin(s) to apply to each mesh in the scene", "PLUGIN") .addOption("plugin-dir").setHelp("plugin-dir", "override base plugin dir", "DIR") #if defined(CORRADE_TARGET_UNIX) || (defined(CORRADE_TARGET_WINDOWS) && !defined(CORRADE_TARGET_WINDOWS_RT)) .addBooleanOption("map").setHelp("map", "memory-map the input for zero-copy import (works only for standalone files)") #endif .addOption("only-mesh-attributes").setHelp("only-mesh-attributes", "include only mesh attributes of given IDs in the output", "N1,N2-N3…") .addBooleanOption("remove-duplicate-vertices").setHelp("remove-duplicate-vertices", "remove duplicate vertices in all meshes after import") .addOption("remove-duplicate-vertices-fuzzy").setHelp("remove-duplicate-vertices-fuzzy", "remove duplicate vertices with fuzzy comparison in all meshes after import", "EPSILON") .addOption('i', "importer-options").setHelp("importer-options", "configuration options to pass to the importer", "key=val,key2=val2,…") .addArrayOption('c', "converter-options").setHelp("converter-options", "configuration options to pass to the converter(s)", "key=val,key2=val2,…") .addArrayOption('m', "mesh-converter-options").setHelp("mesh-converter-options", "configuration options to pass to the mesh converter(s)", "key=val,key2=val2,…") .addOption("mesh").setHelp("mesh", "convert just a single mesh instead of the whole scene, ignored if --concatenate-meshes is specified", "ID") .addOption("mesh-level").setHelp("mesh-level", "level to select for single-mesh conversion", "index") .addBooleanOption("concatenate-meshes").setHelp("concatenate-meshes", "flatten mesh hierarchy and concatenate them all together") .addBooleanOption("info-animations").setHelp("info-animations", "print info about animations in the input file and exit") .addBooleanOption("info-images").setHelp("info-images", "print info about images in the input file and exit") .addBooleanOption("info-lights").setHelp("info-lights", "print info about images in the input file and exit") .addBooleanOption("info-cameras").setHelp("info-cameras", "print info about cameras in the input file and exit") .addBooleanOption("info-materials").setHelp("info-materials", "print info about materials in the input file and exit") .addBooleanOption("info-meshes").setHelp("info-meshes", "print info about meshes in the input file and exit") .addBooleanOption("info-objects").setHelp("info-objects", "print info about objects in the input file and exit") .addBooleanOption("info-scenes").setHelp("info-scenes", "print info about scenes in the input file and exit") .addBooleanOption("info-skins").setHelp("info-skins", "print info about skins in the input file and exit") .addBooleanOption("info-textures").setHelp("info-textures", "print info about textures in the input file and exit") .addBooleanOption("info").setHelp("info", "print info about everything in the input file and exit, same as specifying all other --info-* options together") .addOption("color", "auto").setHelp("color", "colored output for --info", "on|4bit|off|auto") .addBooleanOption("bounds").setHelp("bounds", "show bounds of known attributes in --info output") .addBooleanOption('v', "verbose").setHelp("verbose", "verbose output from importer and converter plugins") .addBooleanOption("profile").setHelp("profile", "measure import and conversion time") .setParseErrorCallback([](const Utility::Arguments& args, Utility::Arguments::ParseError error, const std::string& key) { /* If --info is passed, we don't need the output argument */ if(error == Utility::Arguments::ParseError::MissingArgument && key == "output" && isInfoRequested(args)) return true; /* Handle all other errors as usual */ return false; }) .setGlobalHelp(R"(Converts scenes of different formats. If any of the --info-* options are given, the utility will print information about given data present in the file. In this case no conversion is done and output file doesn't need to be specified. In case one data references another and both --info-* options are specified, the output will also list reference count (for example, --info-scenes together with --info-meshes will print how many objects reference given mesh). The -i, -c and -m arguments accept a comma-separated list of key/value pairs to set in the importer / converter plugin configuration. If the = character is omitted, it's equivalent to saying key=true; configuration subgroups are delimited with /. It's possible to specify the -C option (and correspondingly also -c) multiple times in order to chain more scene converters together. All converters in the chain have to support the ConvertMultiple or ConvertMesh feature, the last converter either ConvertMultiple, ConvertMesh, ConvertMultipleToFile or ConvertMeshToFile. If the last converter doesn't support conversion to a file, AnySceneConverter is used to save its output. If no -C is specified, AnySceneConverter is used. Similarly, the -M option (and correspondingly also -m) can be specified multiple times in order to chain more mesh converters together. All mesh converters in the chain have to support the ConvertMesh feature. If no -M is specified, the imported meshes are passed directly to the scene converter. The --remove-duplicate-vertices operations are performed on meshes before passing them to any converter. If --concatenate-meshes is given, all meshes of the input file are first concatenated into a single mesh, with the scene hierarchy transformation baked in, and then passed through the remaining operations. Only attributes that are present in the first mesh are taken, if --only-mesh-attributes is specified as well, the IDs reference attributes of the first mesh.)") .parse(argc, argv); /* Colored output. Enable only if a TTY. */ Debug::Flags useColor; bool useColor24; if(args.value("color") == "on") { useColor = Debug::Flags{}; useColor24 = true; } else if(args.value("color") == "4bit") { useColor = Debug::Flags{}; useColor24 = false; } else if(args.value("color") == "off") { useColor = Debug::Flag::DisableColors; useColor24 = false; } else if(Debug::isTty()) { useColor = Debug::Flags{}; /* https://unix.stackexchange.com/a/450366, not perfect but good enough I'd say */ /** @todo make this more robust and put directly on Debug, including a "Disable 24 colors" flag */ const Containers::StringView colorterm = std::getenv("COLORTERM"); useColor24 = colorterm == "truecolor"_s || colorterm == "24bit"_s; } else { useColor = Debug::Flag::DisableColors; useColor24 = false; } /* Generic checks */ if(args.value("output")) { /* Not an error in this case, it should be possible to just append --info* to existing command line without having to remove anything. But print a warning at least, it could also be a mistyped option. */ if(isInfoRequested(args)) Warning{} << "Ignoring output file for --info:" << args.value("output"); } if(args.isSet("concatenate-meshes") && args.value("mesh")) { Error{} << "The --mesh and --concatenate-meshes options are mutually exclusive"; return 1; } if(args.value("mesh-level") && !args.value("mesh")) { Error{} << "The --mesh-level option can only be used with --mesh"; return 1; } /** @todo remove this once only-attributes can work with attribute names and thus for more meshes */ if(args.value("only-mesh-attributes") && !args.value("mesh") && !args.isSet("concatenate-meshes")) { Error{} << "The --only-mesh-attributes option can only be used with --mesh or --concatenate-meshes"; return 1; } /* Importer manager */ PluginManager::Manager importerManager{ args.value("plugin-dir").empty() ? Containers::String{} : Utility::Path::join(args.value("plugin-dir"), Utility::Path::split(Trade::AbstractImporter::pluginSearchPaths().back()).second())}; /* Image converter manager for potential dependencies. Needs to be constructed before the scene converter manager for proper destruction order. */ PluginManager::Manager imageConverterManager{ args.value("plugin-dir").empty() ? Containers::String{} : Utility::Path::join(args.value("plugin-dir"), Trade::AbstractImageConverter::pluginSearchPaths().back())}; /* Scene converter manager, register the image converter manager with it */ PluginManager::Manager converterManager{ args.value("plugin-dir").empty() ? Containers::String{} : Utility::Path::join(args.value("plugin-dir"), Utility::Path::split(Trade::AbstractSceneConverter::pluginSearchPaths().back()).second())}; converterManager.registerExternalManager(imageConverterManager); Containers::Pointer importer = importerManager.loadAndInstantiate(args.value("importer")); if(!importer) { Debug{} << "Available importer plugins:" << ", "_s.join(importerManager.aliasList()); return 1; } /* Set options, if passed */ if(args.isSet("verbose")) importer->addFlags(Trade::ImporterFlag::Verbose); Implementation::setOptions(*importer, "AnySceneImporter", args.value("importer-options")); /* Wow, C++, you suck. This implicitly initializes to random shit?! Also, because of addSupportedImporterContents() it's not really possible to distinguish between time spent importing and time spent converting. So it's lumped into a single variable. Steps that are really just conversion are measured separately. */ std::chrono::high_resolution_clock::duration importConversionTime{}; /* Open the file or map it if requested */ #if defined(CORRADE_TARGET_UNIX) || (defined(CORRADE_TARGET_WINDOWS) && !defined(CORRADE_TARGET_WINDOWS_RT)) Containers::Optional> mapped; if(args.isSet("map")) { Trade::Implementation::Duration d{importConversionTime}; if(!(mapped = Utility::Path::mapRead(args.value("input"))) || !importer->openMemory(*mapped)) { Error() << "Cannot memory-map file" << args.value("input"); return 3; } } else #endif { Trade::Implementation::Duration d{importConversionTime}; if(!importer->openFile(args.value("input"))) { Error() << "Cannot open file" << args.value("input"); return 3; } } /* Print file info, if requested */ if(isInfoRequested(args)) { const bool error = SceneTools::Implementation::printInfo(useColor, useColor24, args, *importer, importConversionTime); if(args.isSet("profile")) { Debug{} << "Import took" << UnsignedInt(std::chrono::duration_cast(importConversionTime).count())/1.0e3f << "seconds"; } return error ? 1 : 0; } /* Wow, C++, you suck. This implicitly initializes to random shit?! */ std::chrono::high_resolution_clock::duration conversionTime{}; /* Take a single mesh or concatenate all meshes together, if requested. After that, the importer is changed to one that contains just a single mesh... */ bool singleMesh = false; if(args.isSet("concatenate-meshes") || args.value("mesh")) { singleMesh = true; /* ... and subsequent conversion deals with just meshes, throwing away materials and everything else (if present). */ Containers::Optional mesh; /* Concatenate all meshes together */ if(args.isSet("concatenate-meshes")) { if(!importer->meshCount()) { Error{} << "No meshes found in" << args.value("input"); return 1; } Containers::Array meshes; arrayReserve(meshes, importer->meshCount()); /** @todo handle mesh levels here, once any plugin is capable of importing them */ for(std::size_t i = 0, iMax = importer->meshCount(); i != iMax; ++i) { Trade::Implementation::Duration d{importConversionTime}; Containers::Optional meshToConcatenate = importer->mesh(i); if(!meshToConcatenate) { Error{} << "Cannot import mesh" << i; return 1; } arrayAppend(meshes, *std::move(meshToConcatenate)); } /* If there's a scene, use it to flatten mesh hierarchy. If not, assume all meshes are in the root. */ /** @todo make it possible to choose the scene */ if(importer->defaultScene() != -1) { Containers::Optional scene; { Trade::Implementation::Duration d{importConversionTime}; if(!(scene = importer->scene(importer->defaultScene()))) { Error{} << "Cannot import scene" << importer->defaultScene() << "for mesh concatenation"; return 1; } } Containers::Array flattenedMeshes; { Trade::Implementation::Duration d{conversionTime}; /** @todo once there are 2D scenes, check the scene is 3D */ for(const Containers::Triple& meshTransformation: SceneTools::flattenMeshHierarchy3D(*scene)) arrayAppend(flattenedMeshes, MeshTools::transform3D(meshes[meshTransformation.first()], meshTransformation.third())); } meshes = std::move(flattenedMeshes); } { Trade::Implementation::Duration d{conversionTime}; /** @todo this will assert if the meshes have incompatible primitives (such as some triangles, some lines), or if they have loops/strips/fans -- handle that explicitly */ mesh = MeshTools::concatenate(meshes); } /* Otherwise import just one */ } else { Trade::Implementation::Duration d{importConversionTime}; if(!(mesh = importer->mesh(args.value("mesh"), args.value("mesh-level")))) { Error{} << "Cannot import the mesh"; return 4; } } /* Filter mesh attributes, if requested */ /** @todo move outside of the --mesh / --concatenate-meshes branch once it's possible to filter attributes by name */ if(Containers::StringView onlyAttributes = args.value("only-mesh-attributes")) { const Containers::Optional> only = Utility::String::parseNumberSequence(onlyAttributes, 0, mesh->attributeCount()); if(!only) return 2; /** @todo use MeshTools::filterOnlyAttributes() once it has a rvalue overload that transfers ownership */ Containers::Array attributes; arrayReserve(attributes, only->size()); for(UnsignedInt i: *only) arrayAppend(attributes, mesh->attributeData(i)); const Trade::MeshIndexData indices{mesh->indices()}; const UnsignedInt vertexCount = mesh->vertexCount(); mesh = Trade::MeshData{mesh->primitive(), mesh->releaseIndexData(), indices, mesh->releaseVertexData(), std::move(attributes), vertexCount}; } /* Create an importer instance that contains just the single mesh and related metadata for further steps, without anything else */ /** @todo might be useful to have this split out of the file and tested directly if the complexity grows even further */ struct SingleMeshImporter: Trade::AbstractImporter { explicit SingleMeshImporter(Trade::MeshData&& mesh_, Containers::String&& name, Trade::AbstractImporter& original): mesh{std::move(mesh_)}, name{std::move(name)} { for(UnsignedInt i = 0; i != mesh.attributeCount(); ++i) { const Trade::MeshAttribute name = mesh.attributeName(i); if(!isMeshAttributeCustom(name)) continue; /* Appending even empty ones so we don't have to special-case "not found" in doMeshAttributeName() */ arrayAppend(attributeNames, InPlaceInit, meshAttributeCustom(name), original.meshAttributeName(name)); } } Trade::ImporterFeatures doFeatures() const override { return {}; } /* LCOV_EXCL_LINE */ bool doIsOpened() const override { return true; } void doClose() override {} /* LCOV_EXCL_LINE */ UnsignedInt doMeshCount() const override { return 1; } Containers::String doMeshName(UnsignedInt) override { return name; } Containers::String doMeshAttributeName(UnsignedShort name) override { for(const Containers::Pair& i: attributeNames) if(i.first() == name) return i.second(); /* All custom attributes, including the unnamed, are in the attributeNames array and both our attribute name propagation here and addSupportedImporterContents() call meshAttributeName() only with attributes present in the actual mesh, so this should never be reached */ CORRADE_INTERNAL_ASSERT_UNREACHABLE(); /* LCOV_EXCL_LINE */ } Containers::Optional doMesh(UnsignedInt, UnsignedInt) override { return MeshTools::reference(mesh); } Trade::MeshData mesh; Containers::String name; Containers::Array> attributeNames; }; /* Save the previous importer so we can pass it to the constructor in emplace(), otherwise the constructor would access a deleted pointer */ /** @todo would it make sense for emplace() to first construct and only then delete so I wouldn't need to juggle the previous value manually? */ Containers::Pointer previousImporter = std::move(importer); importer.emplace(*std::move(mesh), /* Propagate the name only in case of a single mesh, for concatenation it wouldn't make sense */ args.value("mesh") ? previousImporter->meshName(args.value("mesh")) : Containers::String{}, *previousImporter); } /* Operations to perform on all meshes in the importer. If there are any, meshes are supplied manually to the converter from the array below. */ Containers::Array meshes; if(args.isSet("remove-duplicate-vertices") || args.value("remove-duplicate-vertices-fuzzy") || args.arrayValueCount("mesh-converter")) { for(UnsignedInt i = 0; i != importer->meshCount(); ++i) { Containers::Optional mesh; { /** @todo handle mesh levels here, once any plugin is capable of importing them */ Trade::Implementation::Duration d{importConversionTime}; if(!(mesh = importer->mesh(i))) { Error{} << "Cannot import mesh" << i; return 1; } } /* Duplicate removal */ if(args.isSet("remove-duplicate-vertices") || args.value("remove-duplicate-vertices-fuzzy")) { const UnsignedInt beforeVertexCount = mesh->vertexCount(); const bool fuzzy = !!args.value("remove-duplicate-vertices-fuzzy"); /** @todo accept two values for float and double fuzzy comparison, or maybe also different for positions, normals and texcoords? ugh... */ if(fuzzy) { Trade::Implementation::Duration d{conversionTime}; mesh = MeshTools::removeDuplicatesFuzzy(*std::move(mesh), args.value("remove-duplicate-vertices-fuzzy")); } else { Trade::Implementation::Duration d{conversionTime}; mesh = MeshTools::removeDuplicates(*std::move(mesh)); } if(args.isSet("verbose")) { Debug d; /* Mesh index 0 would be confusing in case of --concatenate-meshes and plain wrong with --mesh, so don't even print it */ if(singleMesh) d << (fuzzy ? "Fuzzy duplicate removal:" : "Duplicate removal:"); else d << "Mesh" << i << (fuzzy ? "fuzzy duplicate removal:" : "duplicate removal:"); d << beforeVertexCount << "->" << mesh->vertexCount() << "vertices"; } } /* Arbitrary mesh converters */ for(std::size_t j = 0, meshConverterCount = args.arrayValueCount("mesh-converter"); j != meshConverterCount; ++j) { const Containers::StringView meshConverterName = args.arrayValue("mesh-converter", j); if(args.isSet("verbose")) { Debug d; d << "Processing mesh" << i; if(meshConverterCount > 1) d << "(" << Debug::nospace << (j+1) << Debug::nospace << "/" << Debug::nospace << meshConverterCount << Debug::nospace << ")"; d << "with" << meshConverterName << Debug::nospace << "..."; } Containers::Pointer meshConverter = converterManager.loadAndInstantiate(meshConverterName); if(!meshConverter) { Debug{} << "Available mesh converter plugins:" << ", "_s.join(converterManager.aliasList()); return 2; } /* Set options, if passed. The AnySceneConverter check makes no sense here, is just there because the helper wants it */ if(args.isSet("verbose")) meshConverter->addFlags(Trade::SceneConverterFlag::Verbose); if(j < args.arrayValueCount("mesh-converter-options")) Implementation::setOptions(*meshConverter, "AnySceneConverter", args.arrayValue("mesh-converter-options", j)); if(!(meshConverter->features() & (Trade::SceneConverterFeature::ConvertMesh))) { Error{} << meshConverterName << "doesn't support mesh conversion, only" << Debug::packed << meshConverter->features(); return 1; } /** @todo handle mesh levels here, once any plugin is capable of converting them */ if(!(mesh = meshConverter->convert(*mesh))) { Error{} << "Cannot process mesh" << i << "with" << meshConverterName; return 1; } } arrayAppend(meshes, *std::move(mesh)); } } /* Assume there's always one passed --converter option less, and the last is implicitly AnySceneConverter. All converters except the last one are expected to support ConvertMesh and the mesh is "piped" from one to the other. If the last converter supports ConvertMeshToFile instead of ConvertMesh, it's used instead of the last implicit AnySceneConverter. */ for(std::size_t i = 0, converterCount = args.arrayValueCount("converter"); i <= converterCount; ++i) { /* Load converter plugin */ const Containers::StringView converterName = i == converterCount ? "AnySceneConverter"_s : args.arrayValue("converter", i); Containers::Pointer converter = converterManager.loadAndInstantiate(converterName); if(!converter) { Debug{} << "Available converter plugins:" << ", "_s.join(converterManager.aliasList()); return 2; } /* Set options, if passed */ if(args.isSet("verbose")) converter->addFlags(Trade::SceneConverterFlag::Verbose); if(i < args.arrayValueCount("converter-options")) Implementation::setOptions(*converter, "AnySceneConverter", args.arrayValue("converter-options", i)); /* Decide if this is the last converter, capable of saving to a file */ const bool isLastConverter = i + 1 >= converterCount && (converter->features() & (Trade::SceneConverterFeature::ConvertMeshToFile|Trade::SceneConverterFeature::ConvertMultipleToFile)); /* No verbose output for just one converter */ if(converterCount > 1 && args.isSet("verbose")) { if(isLastConverter) { Debug{} << "Saving output (" << Debug::nospace << (i+1) << Debug::nospace << "/" << Debug::nospace << converterCount << Debug::nospace << ") with" << converterName << Debug::nospace << "..."; } else { CORRADE_INTERNAL_ASSERT(i < converterCount); Debug{} << "Processing (" << Debug::nospace << (i+1) << Debug::nospace << "/" << Debug::nospace << converterCount << Debug::nospace << ") with" << converterName << Debug::nospace << "..."; } } /* This is the last --converter (or the implicit AnySceneConverter at the end), output to a file */ if(isLastConverter) { { Trade::Implementation::Duration d{conversionTime}; if(!converter->beginFile(args.value("output"))) { Error{} << "Cannot begin conversion of file" << args.value("output"); return 1; } } /* This is not the last converter, expect that it's capable of converting to an importer instance (or a MeshData wrapped in an importer instance) */ } else { if(!(converter->features() & (Trade::SceneConverterFeature::ConvertMesh|Trade::SceneConverterFeature::ConvertMultiple))) { Error{} << converterName << "doesn't support importer conversion, only" << Debug::packed << converter->features(); return 6; } { Trade::Implementation::Duration d{conversionTime}; if(!converter->begin()) { Error{} << "Cannot begin importer conversion"; return 1; } } } /* Contents to convert, by default all of them */ /** @todo make it possible to filter this on the command line, once the converters receive this for SceneData, MaterialData and TextureData as well */ Trade::SceneContents contents = ~Trade::SceneContents{}; /* If there are any loose meshes from previous conversion steps, add them directly, and clear the array so the next iteration (if any) takes them from the importer instead */ if(meshes) { if(!(Trade::sceneContentsFor(*converter) & Trade::SceneContent::Meshes)) { Warning{} << "Ignoring" << meshes.size() << "meshes not supported by the converter"; } else for(UnsignedInt i = 0; i != meshes.size(); ++i) { Trade::Implementation::Duration d{conversionTime}; const Trade::MeshData& mesh = meshes[i]; /* Propagate custom attribute names, skip ones that are empty. Compared to data names this is done always to avoid information loss. */ for(UnsignedInt j = 0; j != mesh.attributeCount(); ++j) { /** @todo have some kind of a map to not have to query the same custom attribute again for each mesh */ const Trade::MeshAttribute name = mesh.attributeName(j); if(!isMeshAttributeCustom(name)) continue; /* The expectation here is that the meshes are coming from the importer instance. If --mesh or --concatenate-meshes was used, the original importer is replaced a new one containing just one mesh, so in that case it works too. */ if(const Containers::String nameString = importer->meshAttributeName(name)) { converter->setMeshAttributeName(name, nameString); } } if(!converter->add(mesh, contents & Trade::SceneContent::Names ? importer->meshName(i) : Containers::String{})) { Error{} << "Cannot add mesh" << i; return 1; } } /* Ensure the meshes are not added by addSupportedImporterContents() below. Do this also in case the converter actually doesn't support mesh addition, as it would otherwise cause two warnings about the same thing being printed. */ contents &= ~Trade::SceneContent::Meshes; /* Delete the list to avoid adding them again for the next converter (at which point they would be stale) */ /** @todo this line is untested, needs two chained conversion steps that each change the output to verify the old meshes don't get reused in the next step again */ meshes = {}; } { Trade::Implementation::Duration d{importConversionTime}; if(!converter->addSupportedImporterContents(*importer, contents)) { Error{} << "Cannot add importer contents"; return 5; } } /* This is the last --converter (or the implicit AnySceneConverter at the end), end the file and exit the loop */ if(isLastConverter) { Trade::Implementation::Duration d{conversionTime}; if(!converter->endFile()) { Error{} << "Cannot end conversion of file" << args.value("output"); return 5; } break; /* This is not the last converter, save the resulting importer instance for the next loop iteration. By design, the importer should not depend on any data from the converter instance, only on the converter plugin, so we should be fine replacing the converter with a different one in the next iteration and keeping just the importer returned from it. */ } else { Trade::Implementation::Duration d{conversionTime}; if(!(importer = converter->end())) { Error{} << "Cannot end importer conversion"; return 1; } } } if(args.isSet("profile")) { Debug{} << "Import and conversion took" << UnsignedInt(std::chrono::duration_cast(importConversionTime).count())/1.0e3f << "seconds, conversion" << UnsignedInt(std::chrono::duration_cast(conversionTime).count())/1.0e3f << "seconds"; } }