diff --git a/meshroom/aliceVision/ConvertMesh.py b/meshroom/aliceVision/ConvertMesh.py index 7d61aa34a0..9365c7751d 100644 --- a/meshroom/aliceVision/ConvertMesh.py +++ b/meshroom/aliceVision/ConvertMesh.py @@ -1,4 +1,4 @@ -__version__ = "1.0" +__version__ = "2.0" from meshroom.core import desc from meshroom.core.utils import VERBOSE_LEVEL @@ -6,25 +6,39 @@ class ConvertMesh(desc.AVCommandLineNode): commandLine = "aliceVision_convertMesh {allParams}" - category = "Utils" - documentation = """This node allows to convert a mesh to another format.""" + documentation = """Convert a mesh to another mesh format.""" inputs = [ desc.File( name="inputMesh", label="Input Mesh", - description="Input mesh (*.obj, *.mesh, *.meshb, *.ply, *.off, *.stl).", + description="Input mesh (*.obj, *.fbx, *.gltf, *.glb, *.stl, *.ply).", value="", ), desc.ChoiceParam( name="outputMeshFileType", label="Output File Type", - description="Output mesh format (*.obj, *.gltf, *.fbx, *.stl).", + description="Output mesh format (*.obj, *.fbx, *.gltf, *.glb, *.stl, *.ply).", value="obj", - values=["gltf", "obj", "fbx", "stl"], + values=["obj", "fbx", "gltf", "glb", "stl", "ply"], group="", ), + desc.BoolParam( + name="flipNormals", + label="Flip Normals", + description="Flip face normals. It can be needed as it depends on the vertices order " + "in triangles and the convention changes from one software to another.", + value=False, + advanced=True, + ), + desc.BoolParam( + name="copyTextures", + label="Copy Textures", + description="Copy input mesh texture files to the output mesh folder.", + value=True, + advanced=True, + ), desc.ChoiceParam( name="verboseLevel", label="Verbose Level", @@ -38,7 +52,7 @@ class ConvertMesh(desc.AVCommandLineNode): desc.File( name="output", label="Mesh", - description="Output mesh (*.obj, *.mesh, *.meshb, *.ply, *.off, *.stl).", + description="Output mesh (*.obj, *.fbx, *.gltf, *.glb, *.stl, *.ply).", value="{nodeCacheFolder}/mesh.{outputMeshFileTypeValue}", ), ] diff --git a/src/aliceVision/mesh/Material.cpp b/src/aliceVision/mesh/Material.cpp index dca4ecd3b6..ec91c0a08f 100644 --- a/src/aliceVision/mesh/Material.cpp +++ b/src/aliceVision/mesh/Material.cpp @@ -93,17 +93,17 @@ const StaticVector& Material::getTextures(TextureType type) const } } -StaticVector Material::getAllTextures() const +std::vector Material::getAllTextures() const { - StaticVector textures; - textures.resize(_bumpTextures.size() + _diffuseTextures.size() + _displacementTextures.size() + _normalTextures.size()); + std::vector texturePaths; + texturePaths.resize(_bumpTextures.size() + _diffuseTextures.size() + _displacementTextures.size() + _normalTextures.size()); - auto last = std::copy(_bumpTextures.begin(), _bumpTextures.end(), textures.begin()); + auto last = std::copy(_bumpTextures.begin(), _bumpTextures.end(), texturePaths.begin()); last = std::copy(_diffuseTextures.begin(), _diffuseTextures.end(), last); last = std::copy(_displacementTextures.begin(), _displacementTextures.end(), last); std::copy(_normalTextures.begin(), _normalTextures.end(), last); - - return textures; + + return texturePaths; } bool Material::hasTextures(TextureType type) const diff --git a/src/aliceVision/mesh/Material.hpp b/src/aliceVision/mesh/Material.hpp index d95b2166c5..c7c1f124b6 100644 --- a/src/aliceVision/mesh/Material.hpp +++ b/src/aliceVision/mesh/Material.hpp @@ -54,8 +54,8 @@ class Material /// Get textures by type const StaticVector& getTextures(TextureType type) const; - /// Get all textures used in the material - StaticVector getAllTextures() const; + /// Get all texture paths used in the material + std::vector getAllTextures() const; /// Check if material has textures of a given type bool hasTextures(TextureType type) const; diff --git a/src/aliceVision/mesh/Mesh.cpp b/src/aliceVision/mesh/Mesh.cpp index 2c731999b9..235a4ce1ea 100644 --- a/src/aliceVision/mesh/Mesh.cpp +++ b/src/aliceVision/mesh/Mesh.cpp @@ -44,10 +44,14 @@ std::string EFileType_enumToString(const EFileType meshFileType) return "obj"; case EFileType::FBX: return "fbx"; - case EFileType::STL: - return "stl"; case EFileType::GLTF: return "gltf"; + case EFileType::GLB: + return "glb"; + case EFileType::STL: + return "stl"; + case EFileType::PLY: + return "ply"; } throw std::out_of_range("Unrecognized EMeshFileType"); } @@ -61,10 +65,15 @@ EFileType EFileType_stringToEnum(const std::string& meshFileType) return EFileType::OBJ; if (m == "fbx") return EFileType::FBX; - if (m == "stl") - return EFileType::STL; if (m == "gltf") return EFileType::GLTF; + if (m == "glb") + return EFileType::GLB; + if (m == "stl") + return EFileType::STL; + if (m == "ply") + return EFileType::PLY; + throw std::out_of_range("Invalid mesh file type " + meshFileType); } @@ -81,26 +90,42 @@ void Mesh::save(const std::string& filepath) const std::string fileTypeStr = std::filesystem::path(filepath).extension().string().substr(1); const EFileType fileType = mesh::EFileType_stringToEnum(fileTypeStr); - ALICEVISION_LOG_INFO("Save " << fileTypeStr << " mesh file"); + ALICEVISION_LOG_INFO("Saving " << fileTypeStr << " mesh file using Assimp."); + // Assimp scene setup + // create scene and root node aiScene scene; + scene.mRootNode = new aiNode(); - scene.mRootNode = new aiNode; - + // create default material + scene.mMaterials = new aiMaterial*[1]; + scene.mMaterials[0] = new aiMaterial(); + scene.mNumMaterials = 1; + + // create mesh scene.mMeshes = new aiMesh*[1]; + scene.mMeshes[0] = new aiMesh(); scene.mNumMeshes = 1; + + // link mesh to root node scene.mRootNode->mMeshes = new unsigned int[1]; + scene.mRootNode->mMeshes[0] = 0; scene.mRootNode->mNumMeshes = 1; + + // fill mesh data + aiMesh* aimesh = scene.mMeshes[0]; - scene.mMaterials = new aiMaterial*[1]; - scene.mNumMaterials = 1; - scene.mMaterials[0] = new aiMaterial; + // set default material index + aimesh->mMaterialIndex = 0; - scene.mRootNode->mMeshes[0] = 0; - scene.mMeshes[0] = new aiMesh; - aiMesh* aimesh = scene.mMeshes[0]; - aimesh->mMaterialIndex = 0; + if (fileType == EFileType::GLTF || fileType == EFileType::GLB) + { + // set primitive types to triangles, required for gltf and glb export + // for other file types, primitive types is unspecified to avoid normal generation + aimesh->mPrimitiveTypes = aiPrimitiveType_TRIANGLE; + } + // fill mesh vertices aimesh->mNumVertices = pts.size(); aimesh->mVertices = new aiVector3D[pts.size()]; @@ -114,6 +139,7 @@ void Mesh::save(const std::string& filepath) ++index; } + // fill mesh faces aimesh->mNumFaces = tris.size(); aimesh->mFaces = new aiFace[tris.size()]; @@ -128,31 +154,57 @@ void Mesh::save(const std::string& filepath) } } - std::string formatId = fileTypeStr; + // exporter setup + std::string pFormatId = fileTypeStr; unsigned int pPreprocessing = 0u; - // If gltf, use gltf 2.0 - if (fileType == EFileType::GLTF) + + if (fileType == EFileType::GLTF || fileType == EFileType::GLB) { - formatId = "gltf2"; + if (fileType == EFileType::GLTF) + { + // gltf file, use gltf 2.0 + pFormatId = "gltf2"; + } + else + { + // glb file, use glb 2.0 + pFormatId = "glb2"; + } + // gen normals in order to have correct shading in Qt 3D Scene // but cause problems with assimp importer pPreprocessing |= aiProcess_GenNormals; } - // If obj, do not use material else if (fileType == EFileType::OBJ) { - formatId = "objnomtl"; + // obj file, do not use material + pFormatId = "objnomtl"; } + // export mesh Assimp::Exporter exporter; - exporter.Export(&scene, formatId, filepath, pPreprocessing); + const aiReturn ret = exporter.Export(&scene, pFormatId, filepath, pPreprocessing); + + // log mesh information + ALICEVISION_LOG_DEBUG("Mesh information:" << std::endl + << "\t- # vertices: " << pts.size() << std::endl + << "\t- # triangles: " << tris.size() << std::endl + << "\t- # UVs: " << uvCoords.size() << std::endl + << "\t- # normals: " << normals.size()); - ALICEVISION_LOG_INFO("Save mesh to " << fileTypeStr << " done."); + // check for errors + if (ret != AI_SUCCESS) + { + if (ret == AI_OUTOFMEMORY) + { + ALICEVISION_LOG_ERROR("Assimp exporter ran out of memory while exporting mesh to " << filepath); + } - ALICEVISION_LOG_DEBUG("Vertices: " << pts.size()); - ALICEVISION_LOG_DEBUG("Triangles: " << tris.size()); - ALICEVISION_LOG_DEBUG("UVs: " << uvCoords.size()); - ALICEVISION_LOG_DEBUG("Normals: " << normals.size()); + ALICEVISION_THROW_ERROR("Assimp exporter failed to export mesh to " << filepath << ", error: " << exporter.GetErrorString()); + return; + } + + ALICEVISION_LOG_INFO("Mesh saved."); } bool Mesh::loadFromBin(const std::string& binFilepath) @@ -2560,52 +2612,72 @@ void Mesh::load(const std::string& filepath, bool mergeCoincidentVerts, Material f.v[2] = oldToNewMap[f.v[2]]; } } + + // get number of materials used and materials properties + if (material== nullptr || scene->mNumMaterials <= 1) + { + std::unordered_set materialIds = std::unordered_set(_trisMtlIds.begin(), _trisMtlIds.end()); - // set number of materials used - const std::unordered_set materialIds = std::unordered_set(_trisMtlIds.begin(), _trisMtlIds.end()); - nmtls = static_cast(materialIds.size()); - - // store textures per atlas - if (material != nullptr) + // set number of materials used + nmtls = static_cast(materialIds.size()); + } + else { + std::unordered_set materialIds; // does not preserve insertion order + std::vector materialIdsWithTriangleOrder; + + // build materialIds and materialIdsWithTriangleOrder + for(int id : _trisMtlIds) + { + if(materialIds.insert(id).second) + materialIdsWithTriangleOrder.push_back(id); + } + + // set number of materials used + nmtls = static_cast(materialIds.size()); + // get material properties from the first material as they are shared across all others scene->mMaterials[1]->Get(AI_MATKEY_COLOR_AMBIENT, material->ambient); scene->mMaterials[1]->Get(AI_MATKEY_COLOR_DIFFUSE, material->diffuse); scene->mMaterials[1]->Get(AI_MATKEY_COLOR_SPECULAR, material->specular); scene->mMaterials[1]->Get(AI_MATKEY_SHININESS, material->shininess); - for (int id : materialIds) + // get textures from the next materials + for (int id : materialIdsWithTriangleOrder) { aiString diffuse; - if (scene->mMaterials[id + 1]->Get(AI_MATKEY_TEXTURE_DIFFUSE(0), diffuse) == aiReturn_SUCCESS) + if (scene->mMaterials[id + materialIdOffset]->Get(AI_MATKEY_TEXTURE_DIFFUSE(0), diffuse) == aiReturn_SUCCESS) { material->addTexture(Material::TextureType::DIFFUSE, std::string(diffuse.C_Str())); } aiString displacement; - if (scene->mMaterials[id + 1]->Get(AI_MATKEY_TEXTURE_DISPLACEMENT(0), displacement) == aiReturn_SUCCESS) + if (scene->mMaterials[id + materialIdOffset]->Get(AI_MATKEY_TEXTURE_DISPLACEMENT(0), displacement) == aiReturn_SUCCESS) { material->addTexture(Material::TextureType::DISPLACEMENT, std::string(displacement.C_Str())); } aiString normal; - if (scene->mMaterials[id + 1]->Get(AI_MATKEY_TEXTURE_NORMALS(0), normal) == aiReturn_SUCCESS) + if (scene->mMaterials[id + materialIdOffset]->Get(AI_MATKEY_TEXTURE_NORMALS(0), normal) == aiReturn_SUCCESS) { material->addTexture(Material::TextureType::NORMAL, std::string(normal.C_Str())); } aiString height; - if (scene->mMaterials[id + 1]->Get(AI_MATKEY_TEXTURE_HEIGHT(0), height) == aiReturn_SUCCESS) + if (scene->mMaterials[id + materialIdOffset]->Get(AI_MATKEY_TEXTURE_HEIGHT(0), height) == aiReturn_SUCCESS) { material->addTexture(Material::TextureType::BUMP, std::string(height.C_Str())); } } } - ALICEVISION_LOG_DEBUG("Vertices: " << pts.size()); - ALICEVISION_LOG_DEBUG("Triangles: " << tris.size()); - ALICEVISION_LOG_DEBUG("UVs: " << uvCoords.size()); - ALICEVISION_LOG_DEBUG("Num Materials: " + std::to_string(nmtls)); + // log mesh information + ALICEVISION_LOG_DEBUG("Mesh information:" << std::endl + << "\t- # vertices: " << pts.size() << std::endl + << "\t- # triangles: " << tris.size() << std::endl + << "\t- # UVs: " << uvCoords.size() << std::endl + << "\t- # normals: " << normals.size() << std::endl + << "\t- # materials: " << nmtls); } bool Mesh::getEdgeNeighTrisInterval(Pixel& itr, Pixel& edge, StaticVector& edgesXStat, StaticVector& edgesXYStat) diff --git a/src/aliceVision/mesh/Mesh.hpp b/src/aliceVision/mesh/Mesh.hpp index 7e091b88dc..96592cc170 100644 --- a/src/aliceVision/mesh/Mesh.hpp +++ b/src/aliceVision/mesh/Mesh.hpp @@ -50,7 +50,9 @@ enum class EFileType OBJ = 0, FBX, GLTF, - STL + GLB, + STL, + PLY }; EFileType EFileType_stringToEnum(const std::string& filetype); diff --git a/src/aliceVision/mesh/Texturing.cpp b/src/aliceVision/mesh/Texturing.cpp index 0c459c0270..76bf7e532a 100644 --- a/src/aliceVision/mesh/Texturing.cpp +++ b/src/aliceVision/mesh/Texturing.cpp @@ -1304,23 +1304,30 @@ void Texturing::saveAs(const fs::path& dir, const std::string& basename, EFileTy const std::string meshFileTypeStr = EFileType_enumToString(meshFileType); const std::string filepath = (dir / (basename + "." + meshFileTypeStr)).string(); - ALICEVISION_LOG_INFO("Save " << filepath << " mesh file"); + ALICEVISION_LOG_INFO("Saving " << meshFileTypeStr << " mesh file using Assimp."); if (_atlases.empty()) { + ALICEVISION_LOG_ERROR("No texture atlases available. Cannot save mesh."); return; } + // Assimp scene setup + // create scene and root node aiScene scene; + scene.mRootNode = new aiNode(); - scene.mRootNode = new aiNode; + // create material array + scene.mMaterials = new aiMaterial*[_atlases.size()]; + scene.mNumMaterials = _atlases.size(); + // create mesh array scene.mMeshes = new aiMesh*[_atlases.size()]; scene.mNumMeshes = _atlases.size(); + + // link mesh array to root node scene.mRootNode->mMeshes = new unsigned int[_atlases.size()]; scene.mRootNode->mNumMeshes = _atlases.size(); - scene.mMaterials = new aiMaterial*[_atlases.size()]; - scene.mNumMaterials = _atlases.size(); // define shared material properties const aiVector3D valcolor = {material.diffuse.r, material.diffuse.g, material.diffuse.b}; @@ -1340,7 +1347,7 @@ void Texturing::saveAs(const fs::path& dir, const std::string& basename, EFileTy // Set material for this atlas const aiString texName("material_" + Material::textureId(atlasId)); - scene.mMaterials[atlasId] = new aiMaterial; + scene.mMaterials[atlasId] = new aiMaterial(); scene.mMaterials[atlasId]->AddProperty(&valcolor, 1, AI_MATKEY_COLOR_DIFFUSE); scene.mMaterials[atlasId]->AddProperty(&valambient, 1, AI_MATKEY_COLOR_AMBIENT); scene.mMaterials[atlasId]->AddProperty(&valspecular, 1, AI_MATKEY_COLOR_SPECULAR); @@ -1376,11 +1383,18 @@ void Texturing::saveAs(const fs::path& dir, const std::string& basename, EFileTy } scene.mRootNode->mMeshes[atlasId] = atlasId; - scene.mMeshes[atlasId] = new aiMesh; + scene.mMeshes[atlasId] = new aiMesh(); aiMesh* aimesh = scene.mMeshes[atlasId]; aimesh->mMaterialIndex = atlasId; aimesh->mNumUVComponents[0] = 2; + if (meshFileType == EFileType::GLTF || meshFileType == EFileType::GLB) + { + // set primitive types to triangles, required for gltf and glb export + // for other file types, primitive types is unspecified to avoid normal generation + aimesh->mPrimitiveTypes = aiPrimitiveType_TRIANGLE; + } + // Assimp does not allow vertex indices different from uv indices // So we need to group and duplicate std::map, int> unique_pairs; @@ -1398,24 +1412,31 @@ void Texturing::saveAs(const fs::path& dir, const std::string& basename, EFileTy aimesh->mNumVertices = unique_pairs.size(); aimesh->mVertices = new aiVector3D[unique_pairs.size()]; - aimesh->mTextureCoords[0] = new aiVector3D[unique_pairs.size()]; + + if (hasUVs()) + { + aimesh->mTextureCoords[0] = new aiVector3D[unique_pairs.size()]; + } int index = 0; for (auto& p : unique_pairs) { - int vertexId = p.first.first; - int uvId = p.first.second; + const int vertexId = p.first.first; aimesh->mVertices[index].x = mesh->pts[vertexId].x; aimesh->mVertices[index].y = -mesh->pts[vertexId].y; aimesh->mVertices[index].z = -mesh->pts[vertexId].z; - aimesh->mTextureCoords[0][index].x = mesh->uvCoords[uvId].x; - aimesh->mTextureCoords[0][index].y = mesh->uvCoords[uvId].y; - aimesh->mTextureCoords[0][index].z = 0.0; + if (hasUVs()) + { + const int uvId = p.first.second; - p.second = index; + aimesh->mTextureCoords[0][index].x = mesh->uvCoords[uvId].x; + aimesh->mTextureCoords[0][index].y = mesh->uvCoords[uvId].y; + aimesh->mTextureCoords[0][index].z = 0.0; + } + p.second = index; ++index; } @@ -1440,21 +1461,49 @@ void Texturing::saveAs(const fs::path& dir, const std::string& basename, EFileTy } } - std::string formatId = meshFileTypeStr; + // exporter setup + std::string pFormatId = meshFileTypeStr; unsigned int pPreprocessing = 0u; - // If gltf, use gltf 2.0 - if (meshFileType == EFileType::GLTF) + + if (meshFileType == EFileType::GLTF || meshFileType == EFileType::GLB) { - formatId = "gltf2"; - // Flip UVs when exporting (issue with UV origin for gltf2) + if (meshFileType == EFileType::GLTF) + { + // gltf file, use gltf 2.0 + pFormatId = "gltf2"; + } + else + { + // glb file, use glb 2.0 + pFormatId = "glb2"; + } + + // flip UVs when exporting (issue with UV origin for gltf2) // https://github.com/around-media/ue4-custom-prompto/commit/044dbad90fc2172f4c5a8b67c779b80ceace5e1e - pPreprocessing |= aiPostProcessSteps::aiProcess_FlipUVs | aiProcess_GenNormals; + // pPreprocessing |= aiProcess_FlipUVs; + + // gen normals in order to have correct shading in Qt 3D Scene + // but cause problems with assimp importer + pPreprocessing |= aiProcess_GenNormals; } + // export mesh Assimp::Exporter exporter; - exporter.Export(&scene, formatId, filepath, pPreprocessing); + const aiReturn ret = exporter.Export(&scene, pFormatId, filepath, pPreprocessing); + + // check for errors + if (ret != AI_SUCCESS) + { + if (ret == AI_OUTOFMEMORY) + { + ALICEVISION_LOG_ERROR("Assimp exporter ran out of memory while exporting mesh to " << filepath); + } + + ALICEVISION_THROW_ERROR("Assimp exporter failed to export mesh to " << filepath << ", error: " << exporter.GetErrorString()); + return; + } - ALICEVISION_LOG_INFO("Save mesh to " << meshFileTypeStr << " done."); + ALICEVISION_LOG_INFO("Mesh saved."); } void Texturing::_generateNormalAndHeightMaps(const mvsUtils::MultiViewParams& mp, diff --git a/src/software/convert/main_convertMesh.cpp b/src/software/convert/main_convertMesh.cpp index 6f6d5d1e7b..dd41eac7da 100644 --- a/src/software/convert/main_convertMesh.cpp +++ b/src/software/convert/main_convertMesh.cpp @@ -22,7 +22,7 @@ // These constants define the current software version. // They must be updated when the command line is changed. -#define ALICEVISION_SOFTWARE_VERSION_MAJOR 1 +#define ALICEVISION_SOFTWARE_VERSION_MAJOR 2 #define ALICEVISION_SOFTWARE_VERSION_MINOR 0 using namespace aliceVision; @@ -36,25 +36,39 @@ namespace fs = std::filesystem; int aliceVision_main(int argc, char** argv) { // timer initialization - system::Timer timer; - // command-line parameters + // command-line required parameters std::string inputMeshPath; - std::string outputFilePath; + std::string outputMeshPath; + + // command-line optional parameters + bool flipNormals = false; + bool copyTextures = true; // clang-format off po::options_description requiredParams("Required parameters"); requiredParams.add_options() - ("inputMesh", po::value(&inputMeshPath)->default_value(inputMeshPath), - "Mesh file path (*.obj, *.mesh, *.meshb, *.ply, *.off, *.stl).") - ("output,o", po::value(&outputFilePath)->default_value(outputFilePath), - "Output file path for the new mesh file (*.obj, *.mesh, *.meshb, *.ply, *.off, *.stl)."); + ("inputMesh", po::value(&inputMeshPath)->required(), + "Mesh file path (*.obj, *.fbx, *.gltf, *.glb, *.stl, *.ply).") + ("output,o", po::value(&outputMeshPath)->required(), + "Output file path (*.obj, *.fbx, *.gltf, *.glb, *.stl *.ply)."); + + po::options_description optionalParams("Optional parameters"); + optionalParams.add_options() + ("flipNormals", po::value(&flipNormals)->default_value(flipNormals), + "Flip face normals. It can be needed as it depends on the vertices order in triangles and the " + "convention changes from one software to another.") + ("copyTextures", po::value(©Textures)->default_value(copyTextures), + "Copy input mesh texture files to the output mesh folder."); // clang-format on CmdLine cmdline("The program allows to convert a mesh to another mesh format.\n" "AliceVision convertMesh"); + cmdline.add(requiredParams); + cmdline.add(optionalParams); + if (!cmdline.execute(argc, argv)) { return EXIT_FAILURE; @@ -68,7 +82,7 @@ int aliceVision_main(int argc, char** argv) } // check output file path - if (outputFilePath.empty()) + if (outputMeshPath.empty()) { ALICEVISION_LOG_ERROR("Invalid output"); return EXIT_FAILURE; @@ -76,7 +90,7 @@ int aliceVision_main(int argc, char** argv) // ensure output folder exists { - const std::string outputFolderPart = fs::path(outputFilePath).parent_path().string(); + const std::string outputFolderPart = fs::path(outputMeshPath).parent_path().string(); if (!outputFolderPart.empty() && !utils::exists(outputFolderPart)) { @@ -89,26 +103,58 @@ int aliceVision_main(int argc, char** argv) } // load input mesh + ALICEVISION_LOG_INFO("Loading input mesh."); mesh::Texturing texturing; - texturing.loadWithAtlas(inputMeshPath); + texturing.loadWithMaterial(inputMeshPath, flipNormals); mesh::Mesh* inputMesh = texturing.mesh; + // check if mesh is loaded if (!inputMesh) { ALICEVISION_LOG_ERROR("Unable to read input mesh from the file: " << inputMeshPath); return EXIT_FAILURE; } + // check if mesh is not empty if (inputMesh->pts.empty() || inputMesh->tris.empty()) { - ALICEVISION_LOG_ERROR("Error: empty mesh from the file " << inputMeshPath); + ALICEVISION_LOG_ERROR("Empty input mesh from the file: " << inputMeshPath); ALICEVISION_LOG_ERROR("Input mesh: " << inputMesh->pts.size() << " vertices and " << inputMesh->tris.size() << " facets."); return EXIT_FAILURE; } + // copy textures files from the input mesh folder to the output mesh folder + if (copyTextures) + { + const std::string outTypeStr = std::filesystem::path(outputMeshPath).extension().string().substr(1); + const mesh::EFileType outType = mesh::EFileType_stringToEnum(outTypeStr); + + // only copy textures for mesh formats that support textures + if (outType == mesh::EFileType::OBJ || + outType == mesh::EFileType::FBX || + outType == mesh::EFileType::GLTF || + outType == mesh::EFileType::GLB) + { + for (const auto& texturePath : texturing.material.getAllTextures()) + { + ALICEVISION_LOG_DEBUG("Copying texture file: " << texturePath); + + const fs::path srcPath = fs::path(inputMeshPath).parent_path() / texturePath; + const fs::path dstPath = fs::path(outputMeshPath).parent_path() / texturePath; + + fs::copy(srcPath, dstPath); + } + } + } + // save output mesh - ALICEVISION_LOG_INFO("Convert mesh."); - inputMesh->save(outputFilePath); + ALICEVISION_LOG_INFO("Saving output mesh."); + { + const auto outPath = std::filesystem::path(outputMeshPath); + const mesh::EFileType outFileType = mesh::EFileType_stringToEnum(outPath.extension().string().substr(1)); + + texturing.saveAs(outPath.parent_path().string(), outPath.stem().string(), outFileType); + } ALICEVISION_LOG_INFO("Task done in (s): " + std::to_string(timer.elapsed()));