diff --git a/deps/Assimp/Assimp.cmake b/deps/Assimp/Assimp.cmake new file mode 100644 index 0000000000..8b4de03b09 --- /dev/null +++ b/deps/Assimp/Assimp.cmake @@ -0,0 +1,40 @@ +if(CMAKE_VERSION VERSION_LESS 3.22) + set(_assimp_url "https://github.com/assimp/assimp/archive/refs/tags/v5.3.1.tar.gz") + set(_assimp_hash "SHA256=a07666be71afe1ad4bc008c2336b7c688aca391271188eb9108d0c6db1be53f1") +else() + set(_assimp_url "https://github.com/assimp/assimp/archive/refs/tags/v5.4.3.tar.gz") + set(_assimp_hash "SHA256=66dfbaee288f2bc43172440a55d0235dfc7bf885dda6435c038e8000e79582cb") +endif() + +# Assimp's bundled zlib (contrib/zlib) is too old to compile against the modern +# macOS SDK: its zutil.h takes the classic-Mac branch under TARGET_OS_MAC and +# does `#define fdopen(fd,mode) NULL`, which then clobbers the SDK's real +# `fdopen` prototype in and breaks the build. On macOS use the system +# zlib (already found by find_package(ZLIB) in deps-unix-common) instead. +if(APPLE) + set(_assimp_build_zlib "-DASSIMP_BUILD_ZLIB=OFF") +else() + set(_assimp_build_zlib "-DASSIMP_BUILD_ZLIB=ON") +endif() + +orcaslicer_add_cmake_project(Assimp + URL ${_assimp_url} + URL_HASH ${_assimp_hash} + CMAKE_ARGS + -DASSIMP_BUILD_TESTS=OFF + -DASSIMP_BUILD_SAMPLES=OFF + -DASSIMP_BUILD_ASSIMP_TOOLS=OFF + -DASSIMP_INSTALL_PDB=OFF + -DASSIMP_NO_EXPORT=ON + -DASSIMP_BUILD_ALL_IMPORTERS_BY_DEFAULT=OFF + -DASSIMP_BUILD_GLTF_IMPORTER=ON + -DASSIMP_BUILD_OBJ_IMPORTER=ON + -DASSIMP_BUILD_FBX_IMPORTER=ON + ${_assimp_build_zlib} + -DASSIMP_WARNINGS_AS_ERRORS=OFF + -DBUILD_WITH_STATIC_CRT=OFF +) + +if (MSVC) + add_debug_dep(dep_Assimp) +endif () diff --git a/deps/CMakeLists.txt b/deps/CMakeLists.txt index 39cc5de182..8f4bc2a215 100644 --- a/deps/CMakeLists.txt +++ b/deps/CMakeLists.txt @@ -367,6 +367,9 @@ include(libnoise/libnoise.cmake) include(Draco/Draco.cmake) +# Assimp: glTF/GLB/FBX import for the texture-to-color feature. +include(Assimp/Assimp.cmake) + # I *think* 1.1 is used for *just* md5 hashing? # 3.1 has everything in the right place, but the md5 funcs used are deprecated @@ -448,6 +451,7 @@ set(_dep_list dep_libnoise dep_python3 dep_wxInspector + dep_Assimp ) if (MSVC) diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index b8e537c5aa..2880a3cc6b 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -205,6 +205,8 @@ set(lisbslic3r_sources format.hpp Format/OBJ.cpp Format/OBJ.hpp + Format/AssimpImport.hpp + Format/AssimpImport.cpp Format/ResourcePathUtils.hpp Format/objparser.cpp Format/objparser.hpp @@ -521,6 +523,7 @@ cmake_policy(SET CMP0011 NEW) set(CMAKE_POLICY_DEFAULT_CMP0167 NEW) find_package(CGAL REQUIRED) find_package(OpenCV REQUIRED core) +find_package(assimp REQUIRED) unset(CMAKE_POLICY_DEFAULT_CMP0167) cmake_policy(POP) @@ -609,6 +612,7 @@ target_link_libraries(libslic3r libnest2d miniz opencv_world + assimp::assimp PRIVATE ${CMAKE_DL_LIBS} ${EXPAT_LIBRARIES} diff --git a/src/libslic3r/Format/AssimpImport.cpp b/src/libslic3r/Format/AssimpImport.cpp new file mode 100644 index 0000000000..f0ae99506a --- /dev/null +++ b/src/libslic3r/Format/AssimpImport.cpp @@ -0,0 +1,327 @@ +#include "AssimpImport.hpp" + +#include "../TexturePainting.hpp" +#include "ResourcePathUtils.hpp" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace Slic3r { +namespace { + +void clear_textured_mesh(TexturedMesh& out) +{ + out.vertices.clear(); + out.indices.clear(); + out.uvs.clear(); + out.uv_coords.clear(); + out.uv_indices.clear(); + out.textures.clear(); + out.material_ids.clear(); + out.material_texture_map.clear(); + out.material_colors.clear(); +} + +void set_error_message(std::string* error_message, const std::string& message) +{ + if (error_message) + *error_message = message; +} + +bool is_fbx_path(const std::string& path) +{ + return boost::algorithm::iends_with(path, ".fbx"); +} + +bool should_flip_uvs(const std::string& path) +{ + return boost::algorithm::iends_with(path, ".fbx") || + boost::algorithm::iends_with(path, ".glb"); +} + +unsigned int assimp_import_flags(const std::string& path) +{ + unsigned int flags = aiProcess_Triangulate + | aiProcess_GenNormals + | aiProcess_PreTransformVertices + | aiProcess_SortByPType; + if (should_flip_uvs(path)) + flags |= aiProcess_FlipUVs; + return flags; +} + +void configure_importer(Assimp::Importer& importer, const std::string& path, unsigned int flags) +{ + importer.SetPropertyInteger(AI_CONFIG_PP_SBP_REMOVE, + aiPrimitiveType_POINT | aiPrimitiveType_LINE); + + if (flags & aiProcess_PreTransformVertices) + importer.SetPropertyBool(AI_CONFIG_PP_PTV_KEEP_HIERARCHY, true); + + if (is_fbx_path(path)) { + importer.SetPropertyBool(AI_CONFIG_IMPORT_FBX_READ_ALL_GEOMETRY_LAYERS, true); + importer.SetPropertyBool(AI_CONFIG_IMPORT_FBX_READ_MATERIALS, true); + importer.SetPropertyBool(AI_CONFIG_IMPORT_FBX_READ_TEXTURES, true); + importer.SetPropertyBool(AI_CONFIG_IMPORT_FBX_READ_ANIMATIONS, false); + importer.SetPropertyBool(AI_CONFIG_IMPORT_FBX_READ_LIGHTS, false); + importer.SetPropertyBool(AI_CONFIG_IMPORT_FBX_READ_CAMERAS, false); + } +} + +bool read_external_texture_file(const boost::filesystem::path& path, TextureImage& out) +{ + boost::nowide::ifstream file(path.string(), std::ios::binary | std::ios::ate); + if (!file.is_open()) + return false; + + const std::streamoff size = file.tellg(); + if (size <= 0) + return false; + if (static_cast(size) > static_cast(std::numeric_limits::max())) + return false; + + file.seekg(0); + out.width = -1; + out.height = -1; + out.channels = 0; + out.data.resize(static_cast(size)); + file.read(reinterpret_cast(out.data.data()), size); + if (!file && !file.eof()) { + out.data.clear(); + return false; + } + return true; +} + +bool read_embedded_texture(const aiTexture& texture, TextureImage& out) +{ + out.data.clear(); + if (texture.mHeight == 0) { + if (texture.mWidth == 0) + return false; + out.width = -1; + out.height = -1; + out.channels = 0; + out.data.assign( + reinterpret_cast(texture.pcData), + reinterpret_cast(texture.pcData) + texture.mWidth); + return !out.data.empty(); + } + + if (texture.mWidth == 0 || texture.mHeight == 0) + return false; + if (texture.mWidth > static_cast(std::numeric_limits::max()) || + texture.mHeight > static_cast(std::numeric_limits::max())) { + return false; + } + const size_t width = static_cast(texture.mWidth); + const size_t height = static_cast(texture.mHeight); + if (width > std::numeric_limits::max() / height || + width * height > std::numeric_limits::max() / 4) { + return false; + } + + out.width = static_cast(texture.mWidth); + out.height = static_cast(texture.mHeight); + out.channels = 4; + const size_t pixel_count = width * height; + out.data.resize(pixel_count * 4); + for (size_t i = 0; i < pixel_count; ++i) { + const aiTexel& texel = texture.pcData[i]; + out.data[i * 4 + 0] = texel.r; + out.data[i * 4 + 1] = texel.g; + out.data[i * 4 + 2] = texel.b; + out.data[i * 4 + 3] = texel.a; + } + return !out.data.empty(); +} + +bool get_material_texture(const aiMaterial& material, aiString& texture_path) +{ + if (material.GetTextureCount(aiTextureType_DIFFUSE) > 0 && + material.GetTexture(aiTextureType_DIFFUSE, 0, &texture_path) == AI_SUCCESS) { + return true; + } + + if (material.GetTextureCount(aiTextureType_BASE_COLOR) > 0 && + material.GetTexture(aiTextureType_BASE_COLOR, 0, &texture_path) == AI_SUCCESS) { + return true; + } + + return false; +} + +std::array get_material_color(const aiMaterial& material) +{ + aiColor4D color(1.f, 1.f, 1.f, 1.f); + if (material.Get(AI_MATKEY_BASE_COLOR, color) == AI_SUCCESS) + return {color.r, color.g, color.b, color.a}; + if (material.Get(AI_MATKEY_COLOR_DIFFUSE, color) == AI_SUCCESS) + return {color.r, color.g, color.b, color.a}; + return {1.f, 1.f, 1.f, 1.f}; +} + +bool collect_mesh(const aiMesh& mesh, size_t& vertex_offset, TexturedMesh& out, std::string& error) +{ + if (mesh.mNumVertices > static_cast(std::numeric_limits::max()) - vertex_offset) { + error = "Assimp mesh has too many vertices for TexturedMesh indices"; + return false; + } + + for (unsigned int i = 0; i < mesh.mNumVertices; ++i) { + const aiVector3D& v = mesh.mVertices[i]; + out.vertices.push_back({v.x, v.y, v.z}); + + if (mesh.HasTextureCoords(0)) { + const aiVector3D& uv = mesh.mTextureCoords[0][i]; + out.uvs.push_back({uv.x, uv.y}); + } else { + out.uvs.push_back({0.f, 0.f}); + } + } + + const int material_index = static_cast(mesh.mMaterialIndex); + for (unsigned int i = 0; i < mesh.mNumFaces; ++i) { + const aiFace& face = mesh.mFaces[i]; + if (face.mNumIndices != 3) + continue; + if (face.mIndices[0] >= mesh.mNumVertices || + face.mIndices[1] >= mesh.mNumVertices || + face.mIndices[2] >= mesh.mNumVertices) { + error = "Assimp mesh face index is out of bounds"; + return false; + } + out.indices.push_back({ + static_cast(static_cast(face.mIndices[0]) + vertex_offset), + static_cast(static_cast(face.mIndices[1]) + vertex_offset), + static_cast(static_cast(face.mIndices[2]) + vertex_offset)}); + out.material_ids.push_back(material_index); + } + + vertex_offset += mesh.mNumVertices; + return true; +} + +void collect_materials(const aiScene& scene, const boost::filesystem::path& base_dir, TexturedMesh& out) +{ + out.material_texture_map.assign(scene.mNumMaterials, -1); + out.material_colors.assign(scene.mNumMaterials, {1.f, 1.f, 1.f, 1.f}); + + for (unsigned int material_index = 0; material_index < scene.mNumMaterials; ++material_index) { + const aiMaterial* material = scene.mMaterials[material_index]; + if (!material) + continue; + + out.material_colors[material_index] = get_material_color(*material); + + aiString texture_path; + if (!get_material_texture(*material, texture_path)) + continue; + + TextureImage image; + const aiTexture* embedded_texture = scene.GetEmbeddedTexture(texture_path.C_Str()); + if (embedded_texture) { + if (!read_embedded_texture(*embedded_texture, image)) + continue; + } else { + const boost::filesystem::path resolved = resource_path::resolve_external_resource_path( + base_dir, texture_path.C_Str(), "Assimp texture"); + if (resolved.empty()) { + BOOST_LOG_TRIVIAL(warning) << "AssimpImport: texture file not found: " + << texture_path.C_Str(); + continue; + } + if (!read_external_texture_file(resolved, image)) { + BOOST_LOG_TRIVIAL(warning) << "AssimpImport: failed to read texture: " + << resolved; + continue; + } + } + + out.material_texture_map[material_index] = static_cast(out.textures.size()); + out.textures.push_back(std::move(image)); + } +} + +std::string scene_failure_summary(const std::string& path, const char* assimp_error) +{ + std::ostringstream ss; + ss << "Assimp failed to import " << path; + if (assimp_error && assimp_error[0] != '\0') + ss << ": " << assimp_error; + return ss.str(); +} + +} // namespace + +bool load_assimp_textured_model(const std::string& path, TexturedMesh& out, std::string* error_message) +{ + clear_textured_mesh(out); + + Assimp::Importer importer; + const unsigned int flags = assimp_import_flags(path); + configure_importer(importer, path, flags); + + const aiScene* scene = importer.ReadFile(path, flags); + if (!scene || (scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE) || !scene->mRootNode) { + const std::string message = scene_failure_summary(path, importer.GetErrorString()); + BOOST_LOG_TRIVIAL(error) << "AssimpImport: " << message; + set_error_message(error_message, message); + return false; + } + + if (scene->mNumMeshes == 0) { + const std::string message = "Assimp scene has no meshes: " + path; + BOOST_LOG_TRIVIAL(error) << "AssimpImport: " << message; + set_error_message(error_message, message); + return false; + } + + size_t vertex_offset = 0; + for (unsigned int mesh_index = 0; mesh_index < scene->mNumMeshes; ++mesh_index) { + const aiMesh* mesh = scene->mMeshes[mesh_index]; + if (!mesh || !mesh->HasPositions()) + continue; + std::string mesh_error; + if (!collect_mesh(*mesh, vertex_offset, out, mesh_error)) { + const std::string message = mesh_error + ": " + path; + BOOST_LOG_TRIVIAL(error) << "AssimpImport: " << message; + set_error_message(error_message, message); + clear_textured_mesh(out); + return false; + } + } + + if (out.vertices.empty() || out.indices.empty()) { + const std::string message = "Assimp extracted no valid triangles: " + path; + BOOST_LOG_TRIVIAL(error) << "AssimpImport: " << message; + set_error_message(error_message, message); + clear_textured_mesh(out); + return false; + } + + collect_materials(*scene, boost::filesystem::path(path).parent_path(), out); + + BOOST_LOG_TRIVIAL(info) << "AssimpImport: loaded " << out.vertices.size() + << " vertices, " << out.indices.size() + << " triangles, " << out.textures.size() + << " textures from " << path; + return true; +} + +} // namespace Slic3r diff --git a/src/libslic3r/Format/AssimpImport.hpp b/src/libslic3r/Format/AssimpImport.hpp new file mode 100644 index 0000000000..80c3e2dc91 --- /dev/null +++ b/src/libslic3r/Format/AssimpImport.hpp @@ -0,0 +1,11 @@ +#pragma once + +#include + +namespace Slic3r { + +struct TexturedMesh; + +bool load_assimp_textured_model(const std::string& path, TexturedMesh& out, std::string* error_message = nullptr); + +} // namespace Slic3r diff --git a/src/libslic3r/Model.cpp b/src/libslic3r/Model.cpp index 3617e85991..c9322eff3c 100644 --- a/src/libslic3r/Model.cpp +++ b/src/libslic3r/Model.cpp @@ -2,6 +2,7 @@ #include "libslic3r.h" #include "BuildVolume.hpp" #include "TexturePainting.hpp" +#include "Format/AssimpImport.hpp" #include "ClipperUtils.hpp" #include "Exception.hpp" #include "Model.hpp" @@ -242,6 +243,27 @@ _finished: // BBS: add part plate related logic // BBS: backup & restore // Loading model from a file, it may be a simple geometry file as STL or OBJ, however it may be a project file as well. +// Build a plain geometry ModelObject from a textured mesh. The texture itself is carried +// separately on Model::texture_mesh and consumed by the texture import dialog. +static void add_textured_mesh_to_model(Model& model, const TexturedMesh& tex_mesh, const std::string& input_file) +{ + std::string object_name = boost::filesystem::path(input_file).filename().string(); + + indexed_triangle_set its; + its.vertices.resize(tex_mesh.vertices.size()); + for (size_t i = 0; i < tex_mesh.vertices.size(); ++i) + its.vertices[i] = Vec3f(tex_mesh.vertices[i][0], tex_mesh.vertices[i][1], tex_mesh.vertices[i][2]); + its.indices.resize(tex_mesh.indices.size()); + for (size_t i = 0; i < tex_mesh.indices.size(); ++i) + its.indices[i] = Vec3i32(tex_mesh.indices[i][0], tex_mesh.indices[i][1], tex_mesh.indices[i][2]); + + its_merge_vertices(its); + its_remove_degenerate_faces(its); + its_compactify_vertices(its); + + model.add_object(object_name.c_str(), input_file.c_str(), std::move(TriangleMesh(std::move(its)))); +} + Model Model::read_from_file(const std::string& input_file, DynamicPrintConfig* config, ConfigSubstitutionContext* config_substitutions, @@ -325,6 +347,23 @@ Model Model::read_from_file(const std::string& }*/ } } + else if (boost::algorithm::iends_with(input_file, ".glb") || + boost::algorithm::iends_with(input_file, ".gltf") || + boost::algorithm::iends_with(input_file, ".fbx")) { + // These formats always carry material/texture data, so they go through the textured + // import path: the geometry becomes a normal object and the texture is handed to the + // texture-to-color dialog via Model::texture_mesh. + auto tex_mesh = std::make_shared(); + result = load_assimp_textured_model(input_file, *tex_mesh, &message); + if (result) { + model.texture_mesh = tex_mesh; + add_textured_mesh_to_model(model, *tex_mesh, input_file); + } else if (!message.empty()) { + BOOST_LOG_TRIVIAL(error) << "Assimp: failed to load model: " << message + << ", path=" << input_file; + message = _L("The file format is incompatible and cannot be parsed."); + } + } else if (boost::algorithm::iends_with(input_file, ".svg")) result = load_svg(input_file.c_str(), &model, message); //BBS: remove the old .amf.xml files diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index 6028ada640..12e8500dbc 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -521,10 +521,10 @@ static const FileWildcards file_wildcards_by_type[FT_SIZE] = { /* FT_GCODE */ { L("G-code files"), { ".gcode"sv} }, #ifdef __APPLE__ /* FT_MODEL */ - {L("Supported files"), {".3mf"sv, ".stl"sv, ".oltp"sv, ".stp"sv, ".step"sv, ".svg"sv, ".amf"sv, ".obj"sv, ".usd"sv, ".usda"sv, ".usdc"sv, ".usdz"sv, ".abc"sv, ".ply"sv, ".drc"sv}}, + {L("Supported files"), {".3mf"sv, ".stl"sv, ".oltp"sv, ".stp"sv, ".step"sv, ".svg"sv, ".amf"sv, ".obj"sv, ".gltf"sv, ".glb"sv, ".fbx"sv, ".usd"sv, ".usda"sv, ".usdc"sv, ".usdz"sv, ".abc"sv, ".ply"sv, ".drc"sv}}, #else /* FT_MODEL */ - {L("Supported files"), {".3mf"sv, ".stl"sv, ".oltp"sv, ".stp"sv, ".step"sv, ".svg"sv, ".amf"sv, ".obj"sv, ".drc"sv}}, + {L("Supported files"), {".3mf"sv, ".stl"sv, ".oltp"sv, ".stp"sv, ".step"sv, ".svg"sv, ".amf"sv, ".obj"sv, ".gltf"sv, ".glb"sv, ".fbx"sv, ".drc"sv}}, #endif /* FT_ZIP */ { L("ZIP files"), { ".zip"sv } }, /* FT_PROJECT */ { L("Project files"), { ".3mf"sv} },