BBL Port Color Mix Base

This commit is contained in:
Ian Bassi
2026-08-23 22:11:48 +08:00
committed by SoftFever
parent 550e234a37
commit 8fea099d99
75 changed files with 34174 additions and 51 deletions
+147 -3
View File
@@ -1,6 +1,8 @@
#include "../libslic3r.h"
#include "../Model.hpp"
#include "../TriangleMesh.hpp"
#include "../TexturePainting.hpp"
#include "ResourcePathUtils.hpp"
#include "OBJ.hpp"
#include "objparser.hpp"
@@ -21,7 +23,7 @@
namespace Slic3r {
bool load_obj(const char *path, TriangleMesh *meshptr, ObjInfo& obj_info, std::string &message)
bool load_obj(const char *path, TriangleMesh *meshptr, ObjInfo& obj_info, std::string &message, ObjParser::MtlData *out_mtl)
{
if (meshptr == nullptr)
return false;
@@ -98,6 +100,7 @@ bool load_obj(const char *path, TriangleMesh *meshptr, ObjInfo& obj_info, std::s
its.indices.reserve(num_faces + num_quads);
if (exist_mtl) {
obj_info.is_single_mtl = data.usemtls.size() == 1 && mtl_data.new_mtl_unmap.size() == 1;
obj_info.usemtls = data.usemtls;
obj_info.face_colors.reserve(num_faces + num_quads);
}
bool has_color = data.has_vertex_color;
@@ -210,14 +213,17 @@ bool load_obj(const char *path, TriangleMesh *meshptr, ObjInfo& obj_info, std::s
}
if (meshptr->volume() < 0)
meshptr->flip_triangles();
// Hand the parsed material table back so callers can build a TexturedMesh from it.
if (out_mtl)
*out_mtl = mtl_data;
return true;
}
bool load_obj(const char *path, Model *model, ObjInfo& obj_info, std::string &message, const char *object_name_in)
bool load_obj(const char *path, Model *model, ObjInfo& obj_info, std::string &message, const char *object_name_in, ObjParser::MtlData *out_mtl)
{
TriangleMesh mesh;
bool ret = load_obj(path, &mesh, obj_info, message);
bool ret = load_obj(path, &mesh, obj_info, message, out_mtl);
if (ret) {
std::string object_name;
@@ -232,6 +238,144 @@ bool load_obj(const char *path, Model *model, ObjInfo& obj_info, std::string &me
return ret;
}
bool obj_to_textured_mesh(
const ObjInfo& obj_info,
const indexed_triangle_set& its,
const ObjParser::MtlData& mtl_data,
const std::string& obj_directory,
TexturedMesh& out)
{
if (its.vertices.empty() || its.indices.empty() || !obj_info.has_uv_png)
return false;
const size_t nv = its.vertices.size();
const size_t nf = its.indices.size();
// 1. Copy vertices
out.vertices.resize(nv);
for (size_t i = 0; i < nv; ++i)
out.vertices[i] = {its.vertices[i].x(), its.vertices[i].y(), its.vertices[i].z()};
// 2. Copy face indices
out.indices.resize(nf);
for (size_t i = 0; i < nf; ++i)
out.indices[i] = {its.indices[i][0], its.indices[i][1], its.indices[i][2]};
// 3. Build per-face UV (uv_coords + uv_indices)
// OBJ UV convention: V=0 at bottom (OpenGL); texture sampling expects V=0 at top (like glTF/OpenCV).
// Flip V here so downstream code works uniformly.
if (!obj_info.uvs.empty()) {
const size_t uv_face_count = obj_info.uvs.size();
out.uv_coords.resize(uv_face_count * 3);
out.uv_indices.resize(nf);
for (size_t fi = 0; fi < nf; ++fi) {
if (fi < uv_face_count) {
int base = static_cast<int>(fi * 3);
out.uv_coords[base + 0] = {obj_info.uvs[fi][0].x(), 1.f - obj_info.uvs[fi][0].y()};
out.uv_coords[base + 1] = {obj_info.uvs[fi][1].x(), 1.f - obj_info.uvs[fi][1].y()};
out.uv_coords[base + 2] = {obj_info.uvs[fi][2].x(), 1.f - obj_info.uvs[fi][2].y()};
out.uv_indices[fi] = {base, base + 1, base + 2};
} else {
out.uv_indices[fi] = {0, 0, 0};
}
}
}
// 4. Build material list and load textures from disk
// Map: material name -> material index
std::map<std::string, int> mtl_name_to_idx;
for (size_t i = 0; i < mtl_data.mtl_orders.size(); ++i)
mtl_name_to_idx[mtl_data.mtl_orders[i]] = static_cast<int>(i);
const int num_materials = static_cast<int>(mtl_data.mtl_orders.size());
out.material_colors.resize(num_materials, {1.f, 1.f, 1.f, 1.f});
out.material_texture_map.resize(num_materials, -1);
// Map: texture filename -> index in out.textures
std::map<std::string, int> png_to_tex_idx;
for (int mi = 0; mi < num_materials; ++mi) {
const std::string& name = mtl_data.mtl_orders[mi];
auto it = mtl_data.new_mtl_unmap.find(name);
if (it == mtl_data.new_mtl_unmap.end())
continue;
const auto& mtl = *(it->second);
// Material color from Kd
out.material_colors[mi] = {mtl.Kd[0], mtl.Kd[1], mtl.Kd[2], mtl.Tr};
// Texture from map_Kd
if (mtl.map_Kd.empty())
continue;
auto tex_it = png_to_tex_idx.find(mtl.map_Kd);
if (tex_it != png_to_tex_idx.end()) {
out.material_texture_map[mi] = tex_it->second;
continue;
}
// Resolve texture file path.
const boost::filesystem::path requested_tex_path(mtl.map_Kd);
const boost::filesystem::path tex_path = requested_tex_path.is_absolute() ?
resource_path::resolve_existing_path_case_insensitive(requested_tex_path, "obj_to_textured_mesh: map_Kd") :
resource_path::resolve_existing_relative_path_case_insensitive(
boost::filesystem::path(obj_directory), requested_tex_path, "obj_to_textured_mesh: map_Kd");
if (tex_path.empty()) {
BOOST_LOG_TRIVIAL(warning) << "obj_to_textured_mesh: texture not found: " << requested_tex_path;
continue;
}
// Read raw file bytes
boost::nowide::ifstream file(tex_path.string(), std::ios::binary | std::ios::ate);
if (!file.is_open())
continue;
auto file_size = file.tellg();
if (file_size <= 0)
continue;
file.seekg(0, std::ios::beg);
TextureImage ti;
ti.data.resize(static_cast<size_t>(file_size));
file.read(reinterpret_cast<char*>(ti.data.data()), file_size);
ti.width = -1;
ti.height = -1;
ti.channels = 0;
int new_idx = static_cast<int>(out.textures.size());
out.textures.push_back(std::move(ti));
png_to_tex_idx[mtl.map_Kd] = new_idx;
out.material_texture_map[mi] = new_idx;
}
// 5. Build per-face material_ids from usemtls ranges
out.material_ids.resize(nf, -1);
if (!obj_info.usemtls.empty()) {
for (size_t fi = 0; fi < nf; ++fi) {
int face_idx = static_cast<int>(fi);
for (size_t k = 0; k < obj_info.usemtls.size(); ++k) {
const auto& um = obj_info.usemtls[k];
if (face_idx >= um.face_start && face_idx <= um.face_end) {
auto name_it = mtl_name_to_idx.find(um.name);
if (name_it != mtl_name_to_idx.end())
out.material_ids[fi] = name_it->second;
break;
}
}
}
}
if (out.textures.empty()) {
BOOST_LOG_TRIVIAL(warning) << "obj_to_textured_mesh: no textures loaded";
return false;
}
BOOST_LOG_TRIVIAL(info) << "obj_to_textured_mesh: " << nf << " faces, "
<< out.textures.size() << " textures, "
<< num_materials << " materials";
return true;
}
bool store_obj(const char *path, TriangleMesh *mesh)
{
//FIXME returning false even if write failed.
+14 -2
View File
@@ -1,6 +1,7 @@
#ifndef slic3r_Format_OBJ_hpp_
#define slic3r_Format_OBJ_hpp_
#include "libslic3r/Color.hpp"
#include "objparser.hpp"
#include <unordered_map>
namespace Slic3r {
@@ -18,6 +19,7 @@ struct ObjInfo {
std::map<std::string,bool> pngs;
std::unordered_map<int, std::string> uv_map_pngs;
bool has_uv_png{false};
std::vector<ObjParser::ObjUseMtl> usemtls; // material spans, for texture import
};
struct ObjDialogInOut
@@ -32,8 +34,18 @@ struct ObjDialogInOut
std::string lost_material_name{""};
};
typedef std::function<void(ObjDialogInOut &in_out)> ObjImportColorFn;
extern bool load_obj(const char *path, TriangleMesh *mesh, ObjInfo &vertex_colors, std::string &message);
extern bool load_obj(const char *path, Model *model, ObjInfo &vertex_colors, std::string &message, const char *object_name = nullptr);
extern bool load_obj(const char *path, TriangleMesh *mesh, ObjInfo &vertex_colors, std::string &message, ObjParser::MtlData *out_mtl = nullptr);
extern bool load_obj(const char *path, Model *model, ObjInfo &vertex_colors, std::string &message, const char *object_name = nullptr, ObjParser::MtlData *out_mtl = nullptr);
struct TexturedMesh;
// Build a TexturedMesh (vertices + per-face UVs + decoded texture images) from a parsed OBJ
// plus its material table, so the texture-to-color importer can sample face colours.
extern bool obj_to_textured_mesh(
const ObjInfo& obj_info,
const indexed_triangle_set& its,
const ObjParser::MtlData& mtl_data,
const std::string& obj_directory,
TexturedMesh& out);
extern bool store_obj(const char *path, TriangleMesh *mesh);
extern bool store_obj(const char *path, ModelObject *model);
+240
View File
@@ -0,0 +1,240 @@
#ifndef slic3r_Format_ResourcePathUtils_hpp_
#define slic3r_Format_ResourcePathUtils_hpp_
#include <algorithm>
#include <cctype>
#include <cstddef>
#include <string>
#include <vector>
#include <boost/filesystem.hpp>
#include <boost/log/trivial.hpp>
namespace Slic3r {
namespace resource_path {
inline std::string ascii_lower_copy(const std::string& value)
{
std::string lowered;
lowered.reserve(value.size());
for (unsigned char ch : value)
lowered.push_back(static_cast<char>(std::tolower(ch)));
return lowered;
}
inline boost::filesystem::path portable_path_copy(const boost::filesystem::path& value)
{
std::string portable = value.string();
std::replace(portable.begin(), portable.end(), '\\', '/');
return boost::filesystem::path(portable);
}
inline int hex_digit_value(char ch)
{
if (ch >= '0' && ch <= '9') return ch - '0';
if (ch >= 'a' && ch <= 'f') return ch - 'a' + 10;
if (ch >= 'A' && ch <= 'F') return ch - 'A' + 10;
return -1;
}
// Byte-level percent decoding. Per RFC 3986 the %XX byte stream is expected to be
// UTF-8 when produced from URIs / Assimp aiString; this function performs no
// transcoding, so callers must treat both input and output as raw UTF-8 bytes.
inline std::string percent_decode_copy(const std::string& value)
{
std::string decoded;
decoded.reserve(value.size());
for (std::size_t i = 0; i < value.size(); ++i) {
if (value[i] == '%' && i + 2 < value.size()) {
const int hi = hex_digit_value(value[i + 1]);
const int lo = hex_digit_value(value[i + 2]);
if (hi >= 0 && lo >= 0) {
decoded.push_back(static_cast<char>((hi << 4) | lo));
i += 2;
continue;
}
}
decoded.push_back(value[i]);
}
return decoded;
}
inline std::string strip_file_uri_prefix_copy(const std::string& value)
{
const std::string lower = ascii_lower_copy(value);
if (lower.rfind("file://", 0) != 0)
return value;
std::string path = value.substr(7);
if (ascii_lower_copy(path).rfind("localhost/", 0) == 0)
path.erase(0, std::string("localhost").size());
else if (!path.empty() && path.front() != '/')
path = "//" + path;
// file:///C:/... should become C:/..., while file:///tmp/... keeps /tmp/...
if (path.size() >= 3 && path[0] == '/' && std::isalpha(static_cast<unsigned char>(path[1])) && path[2] == ':')
path.erase(path.begin());
return path;
}
inline bool file_uri_has_remote_authority(const std::string& value)
{
const std::string lower = ascii_lower_copy(value);
if (lower.rfind("file://", 0) != 0)
return false;
const std::string path = value.substr(7);
if (path.empty() || path.front() == '/')
return false;
const std::size_t slash = path.find('/');
const std::string authority = path.substr(0, slash);
return ascii_lower_copy(authority) != "localhost";
}
inline bool looks_like_windows_absolute_path(const boost::filesystem::path& path)
{
const std::string portable = portable_path_copy(path).string();
return portable.size() >= 3
&& std::isalpha(static_cast<unsigned char>(portable[0]))
&& portable[1] == ':'
&& portable[2] == '/';
}
inline boost::filesystem::path filename_from_portable_path(const boost::filesystem::path& value)
{
const boost::filesystem::path portable = portable_path_copy(value);
return portable.filename();
}
inline boost::filesystem::path find_child_case_insensitive(
const boost::filesystem::path& directory,
const boost::filesystem::path& requested_name,
const char* context)
{
if (!boost::filesystem::exists(directory) || !boost::filesystem::is_directory(directory))
return {};
const std::string requested_lower = ascii_lower_copy(requested_name.filename().string());
std::vector<boost::filesystem::path> matches;
boost::system::error_code ec;
for (boost::filesystem::directory_iterator it(directory, ec), end; !ec && it != end; it.increment(ec)) {
if (ascii_lower_copy(it->path().filename().string()) == requested_lower)
matches.push_back(it->path());
}
if (matches.size() == 1)
return matches.front();
if (matches.size() > 1) {
BOOST_LOG_TRIVIAL(warning) << context << ": ambiguous case-insensitive resource match for "
<< requested_name << " in " << directory;
}
return {};
}
inline boost::filesystem::path resolve_existing_path_case_insensitive(
const boost::filesystem::path& requested_path,
const char* context = "resource_path")
{
const boost::filesystem::path normalized_path = portable_path_copy(requested_path);
if (normalized_path.empty())
return {};
if (boost::filesystem::exists(normalized_path))
return normalized_path;
boost::filesystem::path current;
bool initialized = false;
for (const boost::filesystem::path& part : normalized_path) {
if (part == normalized_path.root_name() || part == normalized_path.root_directory()) {
current /= part;
initialized = true;
continue;
}
if (!initialized) {
current = boost::filesystem::current_path();
initialized = true;
}
boost::filesystem::path exact = current / part;
if (boost::filesystem::exists(exact)) {
current = exact;
continue;
}
boost::filesystem::path matched = find_child_case_insensitive(current, part, context);
if (matched.empty())
return {};
BOOST_LOG_TRIVIAL(info) << context << ": resolved resource path case-insensitively from "
<< exact << " to " << matched;
current = matched;
}
return boost::filesystem::exists(current) ? current : boost::filesystem::path();
}
inline boost::filesystem::path resolve_existing_relative_path_case_insensitive(
const boost::filesystem::path& base_dir,
const boost::filesystem::path& resource_path,
const char* context = "resource_path")
{
const boost::filesystem::path requested = resource_path.is_absolute() ? resource_path : base_dir / resource_path;
return resolve_existing_path_case_insensitive(requested, context);
}
// Resolve a resource path that originated outside our own code (e.g. a glTF/FBX
// material texture reference or a file:// URI inside a 3MF descriptor).
//
// `raw_path` is expected to be UTF-8 regardless of host platform: file URIs are
// UTF-8 by spec, and Assimp aiString uses UTF-8 internally. Cross-platform
// correctness on Windows additionally relies on the process having called
// boost::nowide::nowide_filesystem() during startup (see src/BambuStudio.cpp),
// which imbues boost::filesystem::path with a UTF-8 codecvt so that
// `path(std::string)` constructs from UTF-8 byte sequences. Callers that bypass
// the main entry point (standalone CLI tools, unit tests) must reproduce that
// setup themselves before invoking this helper.
inline boost::filesystem::path resolve_external_resource_path(
const boost::filesystem::path& base_dir,
const std::string& raw_path,
const char* context = "resource_path",
bool allow_basename_fallback = true)
{
if (raw_path.empty())
return {};
const bool remote_file_uri = file_uri_has_remote_authority(raw_path);
const std::string decoded_path = percent_decode_copy(strip_file_uri_prefix_copy(raw_path));
const boost::filesystem::path requested = portable_path_copy(boost::filesystem::path(decoded_path));
boost::filesystem::path resolved = (requested.is_absolute() || looks_like_windows_absolute_path(requested)) ?
resolve_existing_path_case_insensitive(requested, context) :
resolve_existing_relative_path_case_insensitive(base_dir, requested, context);
if (!resolved.empty())
return resolved;
if (!allow_basename_fallback || remote_file_uri)
return {};
const boost::filesystem::path basename = filename_from_portable_path(requested);
if (basename.empty())
return {};
resolved = resolve_existing_relative_path_case_insensitive(base_dir, basename, context);
if (!resolved.empty()) {
BOOST_LOG_TRIVIAL(info) << context << ": resolved resource by basename from "
<< requested << " to " << resolved;
}
return resolved;
}
} // namespace resource_path
} // namespace Slic3r
#endif /* slic3r_Format_ResourcePathUtils_hpp_ */
+1
View File
@@ -394,6 +394,7 @@ static bool mtl_parseline(const char *line, MtlData &data)
ObjNewMtl new_mtl;
cur_mtl_name = line;
data.new_mtl_unmap[cur_mtl_name] = std::make_shared<ObjNewMtl>();
data.mtl_orders.emplace_back(cur_mtl_name);
break;
}
case 'm': {
+3
View File
@@ -122,6 +122,9 @@ struct MtlData
// Version of the data structure for load / store in the private binary format.
int version;
std::unordered_map<std::string, std::shared_ptr<ObjNewMtl>> new_mtl_unmap;
// Material names in declaration order. new_mtl_unmap is unordered, but OBJ material
// indices are positional, so texture import needs the original order.
std::vector<std::string> mtl_orders;
};
extern bool objparse(const char *path, ObjData &data);
extern bool mtlparse(const char *path, MtlData &data);