This commit is contained in:
Ian Bassi
2026-08-12 19:17:00 -03:00
committed by SoftFever
parent ccd34ab03a
commit 72a68e9a0f
7 changed files with 427 additions and 2 deletions

View File

@@ -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}

View File

@@ -0,0 +1,327 @@
#include "AssimpImport.hpp"
#include "../TexturePainting.hpp"
#include "ResourcePathUtils.hpp"
#include <assimp/Importer.hpp>
#include <assimp/config.h>
#include <assimp/material.h>
#include <assimp/postprocess.h>
#include <assimp/scene.h>
#include <boost/algorithm/string/predicate.hpp>
#include <boost/filesystem.hpp>
#include <boost/log/trivial.hpp>
#include <boost/nowide/fstream.hpp>
#include <array>
#include <cstdint>
#include <limits>
#include <sstream>
#include <string>
#include <vector>
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<uintmax_t>(size) > static_cast<uintmax_t>(std::numeric_limits<size_t>::max()))
return false;
file.seekg(0);
out.width = -1;
out.height = -1;
out.channels = 0;
out.data.resize(static_cast<size_t>(size));
file.read(reinterpret_cast<char*>(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<const unsigned char*>(texture.pcData),
reinterpret_cast<const unsigned char*>(texture.pcData) + texture.mWidth);
return !out.data.empty();
}
if (texture.mWidth == 0 || texture.mHeight == 0)
return false;
if (texture.mWidth > static_cast<unsigned int>(std::numeric_limits<int>::max()) ||
texture.mHeight > static_cast<unsigned int>(std::numeric_limits<int>::max())) {
return false;
}
const size_t width = static_cast<size_t>(texture.mWidth);
const size_t height = static_cast<size_t>(texture.mHeight);
if (width > std::numeric_limits<size_t>::max() / height ||
width * height > std::numeric_limits<size_t>::max() / 4) {
return false;
}
out.width = static_cast<int>(texture.mWidth);
out.height = static_cast<int>(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<float, 4> 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<size_t>(std::numeric_limits<int>::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<int>(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<int>(static_cast<size_t>(face.mIndices[0]) + vertex_offset),
static_cast<int>(static_cast<size_t>(face.mIndices[1]) + vertex_offset),
static_cast<int>(static_cast<size_t>(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<int>(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

View File

@@ -0,0 +1,11 @@
#pragma once
#include <string>
namespace Slic3r {
struct TexturedMesh;
bool load_assimp_textured_model(const std::string& path, TexturedMesh& out, std::string* error_message = nullptr);
} // namespace Slic3r

View File

@@ -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<TexturedMesh>();
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

View File

@@ -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} },