mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-26 18:31:11 +00:00
Add color suport for textures
This commit is contained in:
@@ -3,6 +3,8 @@
|
||||
#include <boost/log/trivial.hpp>
|
||||
|
||||
#include "libslic3r/AABBTreeIndirect.hpp"
|
||||
#include "libslic3r/Color.hpp"
|
||||
#include "libslic3r/PresetBundle.hpp"
|
||||
#include "libslic3r/MeshBoolean.hpp"
|
||||
#include "libslic3r/Model.hpp"
|
||||
#include "libslic3r/Utils.hpp"
|
||||
@@ -15,6 +17,7 @@
|
||||
#include "slic3r/GUI/GUI.hpp"
|
||||
#include "slic3r/GUI/GUI_App.hpp"
|
||||
#include "slic3r/GUI/GUI_ObjectList.hpp"
|
||||
#include "slic3r/GUI/GuiColor.hpp"
|
||||
#include "slic3r/GUI/ImGuiWrapper.hpp"
|
||||
#include "slic3r/GUI/MainFrame.hpp" // wxGetApp().mainframe, as the projector window's parent
|
||||
#include "slic3r/GUI/MsgDialog.hpp"
|
||||
@@ -212,6 +215,14 @@ TriangleSelector::TriangleSplittingData remap_texture_paint_spatial(
|
||||
return dst_sel.serialize();
|
||||
}
|
||||
|
||||
// Edge of the RGB lookup cube make_palette_quantizer() builds. 24 gives 13824 cells - far finer than
|
||||
// the difference between any two printable colours - and costs one DeltaE00 per cell per palette
|
||||
// entry to fill.
|
||||
constexpr int PALETTE_LUT_EDGE = 24;
|
||||
|
||||
// Ceiling on the printable palette, which bounds that fill cost (and the shader's uniform array).
|
||||
constexpr int PALETTE_MAX_ENTRIES = 64;
|
||||
|
||||
std::unique_ptr<GLTexture> upload_height_thumbnail(const DecodedHeightTexture &decoded, int max_px = THUMBNAIL_MAX_PX)
|
||||
{
|
||||
if (decoded.empty())
|
||||
@@ -247,6 +258,46 @@ std::unique_ptr<GLTexture> upload_height_thumbnail(const DecodedHeightTexture &d
|
||||
return texture;
|
||||
}
|
||||
|
||||
// The same upload, of the texture's *colour* rather than its height, for the fast preview to quantize
|
||||
// per fragment. Null for a grayscale texture - there is nothing to show.
|
||||
//
|
||||
// Box-filtered down like the height is, and for a sharper reason: the fast preview quantizes every
|
||||
// fragment independently, so any texel-scale noise left in the image becomes a scatter of single-pixel
|
||||
// colour flips on screen. Filtering on the way to the GPU is where that is cheapest to remove.
|
||||
std::unique_ptr<GLTexture> upload_color_texture(const DecodedHeightTexture &decoded, int max_px)
|
||||
{
|
||||
if (!decoded.has_color())
|
||||
return nullptr;
|
||||
|
||||
const int scale = std::max(1, (std::max(decoded.width, decoded.height) + max_px - 1) / max_px);
|
||||
const int w = std::max(1, decoded.width / scale);
|
||||
const int h = std::max(1, decoded.height / scale);
|
||||
|
||||
std::vector<unsigned char> rgba(size_t(w) * size_t(h) * 4);
|
||||
for (int y = 0; y < h; ++y)
|
||||
for (int x = 0; x < w; ++x) {
|
||||
const int x0 = x * decoded.width / w, x1 = std::max(x0 + 1, (x + 1) * decoded.width / w);
|
||||
const int y0 = y * decoded.height / h, y1 = std::max(y0 + 1, (y + 1) * decoded.height / h);
|
||||
unsigned int sum[3] = { 0, 0, 0 };
|
||||
unsigned int n = 0;
|
||||
for (int sy = y0; sy < y1 && sy < decoded.height; ++sy)
|
||||
for (int sx = x0; sx < x1 && sx < decoded.width; ++sx, ++n) {
|
||||
const size_t si = (size_t(sy) * size_t(decoded.width) + size_t(sx)) * 3;
|
||||
for (int c = 0; c < 3; ++c)
|
||||
sum[c] += decoded.rgb[si + size_t(c)];
|
||||
}
|
||||
const size_t di = (size_t(y) * size_t(w) + size_t(x)) * 4;
|
||||
for (int c = 0; c < 3; ++c)
|
||||
rgba[di + size_t(c)] = (n > 0) ? static_cast<unsigned char>(sum[size_t(c)] / n) : 0;
|
||||
rgba[di + 3] = 255;
|
||||
}
|
||||
|
||||
auto texture = std::make_unique<GLTexture>();
|
||||
if (!texture->load_from_raw_data(std::move(rgba), (unsigned int) w, (unsigned int) h, false, false))
|
||||
return nullptr;
|
||||
return texture;
|
||||
}
|
||||
|
||||
// Intersects the camera ray through `mouse_pos` (screen coords) with the plane passing through
|
||||
// `plane_point_local`/`plane_normal_local` (mesh-local coords, transformed to world by `trafo`).
|
||||
// Returns false if the ray is parallel to the plane or the plane is behind the camera.
|
||||
@@ -887,7 +938,16 @@ void GLGizmoTextureDisplacement::render_preview_mesh()
|
||||
shader->set_uniform("projection_matrix", camera.get_projection_matrix());
|
||||
const Matrix3d view_normal_matrix = view_matrix.matrix().block(0, 0, 3, 3) * trafo_matrix.matrix().block(0, 0, 3, 3).inverse().transpose();
|
||||
shader->set_uniform("view_normal_matrix", view_normal_matrix);
|
||||
m_preview_glmodel.render();
|
||||
if (m_preview_color_runs.empty()) {
|
||||
m_preview_glmodel.render();
|
||||
} else {
|
||||
// set_color() writes the uniform the shader reads, so one call per group is all the
|
||||
// per-triangle colour this needs.
|
||||
for (const PreviewColorRun &run : m_preview_color_runs) {
|
||||
m_preview_glmodel.set_color(run.color);
|
||||
m_preview_glmodel.render(run.range, shader);
|
||||
}
|
||||
}
|
||||
shader->stop_using();
|
||||
}
|
||||
|
||||
@@ -972,6 +1032,13 @@ void GLGizmoTextureDisplacement::rebuild_bump_preview_mesh()
|
||||
if (!m_bump_preview_uses_vertex_uv)
|
||||
vertex_uv.clear();
|
||||
|
||||
// Colour is quantized per *fragment* in the shader now (see the .fs), so this mesh carries no
|
||||
// colour of its own - the palette and the colour texture are uniforms, and every pixel matches the
|
||||
// image rather than the facet it landed on. What the *bake* will produce, at facet resolution, is
|
||||
// what the Normal view shows.
|
||||
m_bump_preview_palette = (active != nullptr && active->color_enabled) ? cached_palette()
|
||||
: std::vector<PaletteEntry>{};
|
||||
|
||||
GLModel::Geometry init_data;
|
||||
// P3N3T2: normal.x carries the paint weight, tex_coord the precomputed uv (see the vertex shader).
|
||||
init_data.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3N3T2 };
|
||||
@@ -1003,8 +1070,10 @@ void GLGizmoTextureDisplacement::rebuild_bump_preview_mesh()
|
||||
vcount += 3;
|
||||
}
|
||||
};
|
||||
emit_triangles(patch, 1.f); // painted -> bumped
|
||||
emit_triangles(rest, 0.f); // untouched surface -> flat, so it still shows but isn't bumped
|
||||
emit_triangles(patch, 1.f); // painted -> bumped, and coloured by the shader
|
||||
// Untouched surface: flat, so it still shows but isn't bumped - and uncoloured, which is what the
|
||||
// bake leaves it as (EnforcerBlockerType::NONE, i.e. the volume's own filament).
|
||||
emit_triangles(rest, 0.f);
|
||||
|
||||
m_bump_preview_glmodel.init_from(std::move(init_data));
|
||||
// GLModel::render() unconditionally re-sets the shader's "uniform_color" from this internal
|
||||
@@ -1189,6 +1258,29 @@ void GLGizmoTextureDisplacement::render_bump_preview_mesh()
|
||||
// When set, the shader samples at the per-vertex uv baked into the mesh (LSCM) rather than
|
||||
// projecting; see rebuild_bump_preview_mesh().
|
||||
shader->set_uniform("use_vertex_uv", m_bump_preview_uses_vertex_uv);
|
||||
|
||||
// The filament palette the mesh's per-triangle indices refer to. Count 0 means "no layer is
|
||||
// colouring", and the shader keeps the model's own colour for every fragment.
|
||||
// The printable palette, in RGB for display and in Lab for the match. Uploaded rather than
|
||||
// matched on the CPU because the quantization is per fragment here.
|
||||
const GLTexture *color_tex = get_layer_color_texture(*layer);
|
||||
const int palette_count =
|
||||
(color_tex != nullptr) ? int(std::min(m_bump_preview_palette.size(), size_t(PALETTE_MAX_ENTRIES))) : 0;
|
||||
shader->set_uniform("palette_count", palette_count);
|
||||
shader->set_uniform("has_color_tex", color_tex != nullptr);
|
||||
for (int i = 0; i < palette_count; ++i) {
|
||||
const Vec3f &rgb = m_bump_preview_palette[size_t(i)].rgb;
|
||||
float l, a, b;
|
||||
RGB2Lab(rgb.x() * 255.f, rgb.y() * 255.f, rgb.z() * 255.f, &l, &a, &b);
|
||||
shader->set_uniform(("palette_rgb[" + std::to_string(i) + "]").c_str(), rgb);
|
||||
shader->set_uniform(("palette_lab[" + std::to_string(i) + "]").c_str(), Vec3f(l, a, b));
|
||||
}
|
||||
if (color_tex != nullptr) {
|
||||
shader->set_uniform("color_tex", 1);
|
||||
glsafe(::glActiveTexture(GL_TEXTURE1));
|
||||
glsafe(::glBindTexture(GL_TEXTURE_2D, (GLuint) color_tex->get_id()));
|
||||
glsafe(::glActiveTexture(GL_TEXTURE0));
|
||||
}
|
||||
// The live UV-editor island drag rides this 2x3 affine (identity except mid-drag); only the flagged
|
||||
// island's vertices apply it, so a drag is a uniform update rather than a mesh rebuild.
|
||||
const Eigen::Matrix<float, 2, 3> &d = m_bump_island_delta;
|
||||
@@ -1564,11 +1656,20 @@ void GLGizmoTextureDisplacement::queue_preview_job()
|
||||
input.options = mv->texture_displacement_options;
|
||||
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i)
|
||||
input.facets_data[size_t(i)] = mv->texture_displacement_facet(i).get_data();
|
||||
// Captured here rather than read in the handler: get_extruders_colors() is main-thread state and
|
||||
// the preview has to be grouped against the same palette it was computed with, not whatever is
|
||||
// loaded by the time it lands.
|
||||
input.color = color_settings_for(*mv);
|
||||
// The filament list the result's indices refer to, captured with the job rather than read back
|
||||
// when it lands - loading a filament meanwhile must not recolour a preview computed against a
|
||||
// different list.
|
||||
const std::vector<ColorRGBA> filaments = m_palette_filaments;
|
||||
|
||||
m_preview_job_running = true;
|
||||
auto &worker = wxGetApp().plater()->get_ui_job_worker();
|
||||
queue_job(worker, std::make_unique<TextureDisplacementPreviewJob>(std::move(input), generation, m_preview_generation,
|
||||
[this](indexed_triangle_set its, uint64_t result_generation) {
|
||||
[this, filaments](TextureDisplacementPreviewResult result, uint64_t result_generation) {
|
||||
indexed_triangle_set its = std::move(result.mesh);
|
||||
m_preview_job_running = false;
|
||||
if (result_generation != m_preview_generation->load()) {
|
||||
// Superseded while this was computing (it will have aborted early and come back
|
||||
@@ -1580,7 +1681,29 @@ void GLGizmoTextureDisplacement::queue_preview_job()
|
||||
// rather than blanking it; there is no new result to show, not a new empty one.
|
||||
} else {
|
||||
m_preview_glmodel.reset();
|
||||
m_preview_glmodel.init_from(its);
|
||||
m_preview_color_runs.clear();
|
||||
if (result.triangle_color.size() == its.indices.size() && !filaments.empty()) {
|
||||
// Group by *filament*, not by palette entry: what the bake wrote is the resolved
|
||||
// filament, interleaving already applied, so this shows the real banding rather
|
||||
// than the flat average the eye will turn it into.
|
||||
indexed_triangle_set sorted;
|
||||
sorted.vertices = its.vertices;
|
||||
sorted.indices.reserve(its.indices.size());
|
||||
for (int want = 0; want <= int(filaments.size()); ++want) {
|
||||
const size_t first = sorted.indices.size();
|
||||
for (size_t i = 0; i < its.indices.size(); ++i)
|
||||
if (int(result.triangle_color[i]) == want)
|
||||
sorted.indices.push_back(its.indices[i]);
|
||||
if (sorted.indices.size() == first)
|
||||
continue;
|
||||
m_preview_color_runs.push_back(
|
||||
{ { first * 3, sorted.indices.size() * 3 },
|
||||
want == 0 ? GLVolume::NEUTRAL_COLOR : filaments[size_t(want - 1)] });
|
||||
}
|
||||
m_preview_glmodel.init_from(sorted);
|
||||
} else {
|
||||
m_preview_glmodel.init_from(its);
|
||||
}
|
||||
m_preview_glmodel.set_color(GLVolume::NEUTRAL_COLOR);
|
||||
// Keep the displaced mesh so the wireframe overlay can be drawn on it (the
|
||||
// true-displacement view), then refresh the wireframe from it.
|
||||
@@ -3277,10 +3400,199 @@ TextureDisplacementFacetsData GLGizmoTextureDisplacement::facets_data_of(const M
|
||||
return out;
|
||||
}
|
||||
|
||||
bool GLGizmoTextureDisplacement::any_layer_colors(const ModelVolume &mv)
|
||||
{
|
||||
for (const TextureDisplacementLayer &layer : mv.texture_displacement_layers)
|
||||
if (layer.color_enabled && !layer.empty() && decode_height_texture(layer).has_color())
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
TextureColorSettings GLGizmoTextureDisplacement::color_settings_for(const ModelVolume &mv)
|
||||
{
|
||||
TextureColorSettings out;
|
||||
if (!any_layer_colors(mv))
|
||||
return out; // nothing is colouring: every colour path stays switched off
|
||||
out.palette = cached_palette();
|
||||
out.mix_mode = mv.texture_displacement_options.color_mix_mode;
|
||||
out.despeckle_passes = mv.texture_displacement_options.color_despeckle;
|
||||
out.layer_height = print_layer_height();
|
||||
// The dither cell is tied to the colour-detail target: a cell much smaller than a facet cannot be
|
||||
// drawn at all, and one much larger stops reading as a blend and starts reading as a check.
|
||||
out.dither_cell_mm = std::max(m_subdivide_color_mm, 0.05f) * 2.f;
|
||||
return out;
|
||||
}
|
||||
|
||||
const std::vector<GLGizmoTextureDisplacement::PaletteEntry> &GLGizmoTextureDisplacement::cached_palette()
|
||||
{
|
||||
// Rebuilt only when the loaded filaments or the mixing setting actually change. The bump preview
|
||||
// rebuilds on every paint stroke and the subdivide preview on every slider frame, and filling the
|
||||
// quantizer's lookup cube for a 64-entry palette is tens of milliseconds - paying that per stroke
|
||||
// is the difference between painting that keeps up and painting that stutters.
|
||||
const ModelVolume *mv = texture_volume();
|
||||
const bool mixing = mv != nullptr && mv->texture_displacement_options.color_mix_enabled;
|
||||
std::vector<ColorRGBA> filaments = filament_palette();
|
||||
if (m_palette_cache.empty() || filaments != m_palette_filaments || mixing != m_palette_mixing) {
|
||||
m_palette_filaments = std::move(filaments);
|
||||
m_palette_mixing = mixing;
|
||||
m_palette_cache = make_palette(m_palette_filaments, mixing);
|
||||
m_palette_quantizer = make_palette_quantizer(m_palette_cache);
|
||||
}
|
||||
return m_palette_cache;
|
||||
}
|
||||
|
||||
std::vector<ColorRGBA> GLGizmoTextureDisplacement::filament_palette()
|
||||
{
|
||||
std::vector<ColorRGBA> palette = wxGetApp().plater()->get_extruders_colors();
|
||||
// mmu_segmentation_facets encodes the filament in a 6-bit prefix code and stops at Extruder16.
|
||||
if (palette.size() > size_t(EnforcerBlockerType::ExtruderMax))
|
||||
palette.resize(size_t(EnforcerBlockerType::ExtruderMax));
|
||||
return palette;
|
||||
}
|
||||
|
||||
float GLGizmoTextureDisplacement::print_layer_height()
|
||||
{
|
||||
try {
|
||||
const DynamicPrintConfig &cfg = wxGetApp().preset_bundle->prints.get_edited_preset().config;
|
||||
if (const ConfigOptionFloat *opt = cfg.option<ConfigOptionFloat>("layer_height"); opt != nullptr)
|
||||
if (opt->value > 1e-3)
|
||||
return float(opt->value);
|
||||
} catch (...) {
|
||||
}
|
||||
return 0.2f;
|
||||
}
|
||||
|
||||
std::vector<GLGizmoTextureDisplacement::PaletteEntry> GLGizmoTextureDisplacement::make_palette(
|
||||
const std::vector<ColorRGBA> &filaments, bool mixing)
|
||||
{
|
||||
std::vector<PaletteEntry> out;
|
||||
const int n = int(filaments.size());
|
||||
for (int i = 0; i < n; ++i)
|
||||
out.push_back({ Vec3f(filaments[size_t(i)].r(), filaments[size_t(i)].g(), filaments[size_t(i)].b()),
|
||||
i, i, 1, 1 });
|
||||
if (!mixing || n < 2)
|
||||
return out;
|
||||
|
||||
// How many intermediate steps each pair gets, chosen so the whole palette stays under
|
||||
// PALETTE_MAX_ENTRIES. Fewer filaments means more room for mixes, which is also what you want:
|
||||
// with two filaments the mixes are the only way to get anywhere, and with sixteen there is little
|
||||
// point mixing at all. `den` is also the band/dither repeat, so a small one is a short pattern.
|
||||
const int pairs = n * (n - 1) / 2;
|
||||
int steps = 0;
|
||||
for (int s = 5; s >= 1; --s)
|
||||
if (n + pairs * s <= PALETTE_MAX_ENTRIES) {
|
||||
steps = s;
|
||||
break;
|
||||
}
|
||||
if (steps == 0)
|
||||
return out;
|
||||
const int den = steps + 1;
|
||||
|
||||
for (int i = 0; i < n; ++i)
|
||||
for (int j = i + 1; j < n; ++j) {
|
||||
float la, aa, ba, lb, ab, bb;
|
||||
RGB2Lab(filaments[size_t(i)].r() * 255.f, filaments[size_t(i)].g() * 255.f,
|
||||
filaments[size_t(i)].b() * 255.f, &la, &aa, &ba);
|
||||
RGB2Lab(filaments[size_t(j)].r() * 255.f, filaments[size_t(j)].g() * 255.f,
|
||||
filaments[size_t(j)].b() * 255.f, &lb, &ab, &bb);
|
||||
for (int k = 1; k <= steps; ++k) {
|
||||
// k/den of filament i, the rest of j - averaged in Lab, which is what the eye does
|
||||
// when the two are interleaved too finely to resolve.
|
||||
const float t = float(k) / float(den);
|
||||
float r, g, b;
|
||||
Lab2RGB(la * t + lb * (1.f - t), aa * t + ab * (1.f - t), ba * t + bb * (1.f - t), &r, &g, &b);
|
||||
out.push_back({ Vec3f(std::clamp(r / 255.f, 0.f, 1.f), std::clamp(g / 255.f, 0.f, 1.f),
|
||||
std::clamp(b / 255.f, 0.f, 1.f)),
|
||||
i, j, k, den });
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
ColorResolveFn GLGizmoTextureDisplacement::make_mix_resolver(const std::vector<PaletteEntry> &palette,
|
||||
ColorMixMode mode, float layer_height,
|
||||
float cell_mm)
|
||||
{
|
||||
if (palette.empty())
|
||||
return nullptr;
|
||||
auto entries = std::make_shared<std::vector<PaletteEntry>>(palette);
|
||||
const float band = std::max(layer_height, 0.01f);
|
||||
const float cell = std::max(cell_mm, 0.01f);
|
||||
|
||||
return [entries, mode, band, cell](int index, const Vec3f &pos) -> int {
|
||||
if (index < 0 || size_t(index) >= entries->size())
|
||||
return -1;
|
||||
const PaletteEntry &e = (*entries)[size_t(index)];
|
||||
if (!e.is_mix())
|
||||
return e.a;
|
||||
|
||||
// Which of the two filaments this point falls on. Both patterns are *ordered*, never random:
|
||||
// the eye blends a regular pattern into a flat colour, and turns a random one into noise.
|
||||
if (mode == ColorMixMode::ZBands) {
|
||||
// One band per print layer. floorf, not a cast, so this stays correct below z = 0.
|
||||
const int slot = int(std::floor(pos.z() / band));
|
||||
const int phase = ((slot % e.den) + e.den) % e.den;
|
||||
return phase < e.num ? e.a : e.b;
|
||||
}
|
||||
// Ordered 4x4 Bayer over the surface, indexed by position so the pattern is stable in space
|
||||
// rather than in triangle order (which would move under any remesh, and read as noise).
|
||||
static const int BAYER[16] = { 0, 8, 2, 10, 12, 4, 14, 6, 3, 11, 1, 9, 15, 7, 13, 5 };
|
||||
const int gx = ((int(std::floor(pos.x() / cell)) % 4) + 4) % 4;
|
||||
const int gy = ((int(std::floor(pos.y() / cell)) % 4) + 4) % 4;
|
||||
// A third axis would be ideal, but the two dominant ones are enough for a surface pattern and
|
||||
// keep the cell square on the faces that matter.
|
||||
const float threshold = (float(BAYER[gy * 4 + gx]) + 0.5f) / 16.f;
|
||||
return (float(e.num) / float(e.den)) > threshold ? e.a : e.b;
|
||||
};
|
||||
}
|
||||
|
||||
ColorQuantizeFn GLGizmoTextureDisplacement::make_palette_quantizer(const std::vector<PaletteEntry> &palette)
|
||||
{
|
||||
if (palette.empty())
|
||||
return nullptr;
|
||||
|
||||
// Lab once per entry, not once per lookup.
|
||||
struct Lab { float l, a, b; };
|
||||
std::vector<Lab> palette_lab(palette.size());
|
||||
for (size_t i = 0; i < palette.size(); ++i)
|
||||
RGB2Lab(palette[i].rgb.x() * 255.f, palette[i].rgb.y() * 255.f, palette[i].rgb.z() * 255.f,
|
||||
&palette_lab[i].l, &palette_lab[i].a, &palette_lab[i].b);
|
||||
|
||||
constexpr int E = PALETTE_LUT_EDGE;
|
||||
auto lut = std::make_shared<std::vector<uint8_t>>(size_t(E) * E * E, 0);
|
||||
tbb::parallel_for(tbb::blocked_range<int>(0, E), [&](const tbb::blocked_range<int> &range) {
|
||||
for (int r = range.begin(); r < range.end(); ++r)
|
||||
for (int g = 0; g < E; ++g)
|
||||
for (int b = 0; b < E; ++b) {
|
||||
// Cell centre, so the quantization error is symmetric across the cell.
|
||||
float l0, a0, b0;
|
||||
RGB2Lab((r + 0.5f) / E * 255.f, (g + 0.5f) / E * 255.f, (b + 0.5f) / E * 255.f, &l0, &a0, &b0);
|
||||
int best = 0;
|
||||
float best_d = std::numeric_limits<float>::max();
|
||||
for (size_t i = 0; i < palette_lab.size(); ++i) {
|
||||
const float d = DeltaE00(l0, a0, b0, palette_lab[i].l, palette_lab[i].a, palette_lab[i].b);
|
||||
if (d < best_d) {
|
||||
best_d = d;
|
||||
best = int(i);
|
||||
}
|
||||
}
|
||||
(*lut)[(size_t(r) * E + size_t(g)) * E + size_t(b)] = uint8_t(best);
|
||||
}
|
||||
});
|
||||
|
||||
return [lut](const Vec3f &rgb) -> int {
|
||||
constexpr int E = PALETTE_LUT_EDGE;
|
||||
const int r = std::clamp(int(rgb.x() * E), 0, E - 1);
|
||||
const int g = std::clamp(int(rgb.y() * E), 0, E - 1);
|
||||
const int b = std::clamp(int(rgb.z() * E), 0, E - 1);
|
||||
return int((*lut)[(size_t(r) * E + size_t(g)) * E + size_t(b)]);
|
||||
};
|
||||
}
|
||||
|
||||
TextureDisplacementPrepareResult GLGizmoTextureDisplacement::prepare_mesh(
|
||||
const indexed_triangle_set &base, const TextureDisplacementFacetsData &masks,
|
||||
const std::vector<TextureDisplacementLayer> &layers, const TextureDisplacementPrepareParams ¶ms,
|
||||
const DisplacementProgressFn &progress)
|
||||
const std::vector<PrintableColor> &palette, const DisplacementProgressFn &progress)
|
||||
{
|
||||
TextureDisplacementPrepareResult out;
|
||||
const auto report = [&progress](int pct) { return !progress || progress(pct); };
|
||||
@@ -3335,6 +3647,13 @@ TextureDisplacementPrepareResult GLGizmoTextureDisplacement::prepare_mesh(
|
||||
HeightFieldSampler sampler;
|
||||
if (params.subdiv_feature)
|
||||
sampler = make_combined_displacement_sampler(mesh.its, layers, current);
|
||||
// Colour boundaries need triangles of their own - the chord test cannot see them, since
|
||||
// the height field is perfectly smooth across a change of filament.
|
||||
ColorFieldSampler color;
|
||||
if (params.subdiv_color_edge_mm > 0.f && !palette.empty())
|
||||
color = make_combined_color_sampler(mesh.its, layers, current, make_palette_quantizer(palette));
|
||||
// Note the sampler is built on the *quantizer* alone - the refinement follows perceived
|
||||
// colour, never the interleaving that realises a mix (see ColorResolveFn).
|
||||
// "Min edge" is a feature-mode control (it is the floor the curvature test refines down
|
||||
// to); in plain adaptive mode the target edge length is the only criterion, so the floor
|
||||
// must not be allowed to silently override a target the user set below it.
|
||||
@@ -3350,7 +3669,8 @@ TextureDisplacementPrepareResult GLGizmoTextureDisplacement::prepare_mesh(
|
||||
sampler, tol, floor, params.subdiv_border_mm, [&](int pct) {
|
||||
aborted = !report(50 + pct / 2);
|
||||
return !aborted;
|
||||
});
|
||||
},
|
||||
color, params.subdiv_color_edge_mm);
|
||||
if (aborted)
|
||||
return {};
|
||||
if (refined.indices.size() != mesh.its.indices.size()) {
|
||||
@@ -3387,6 +3707,7 @@ void GLGizmoTextureDisplacement::subdivide_model_adaptive()
|
||||
params.subdiv_detail_mm = m_subdivide_detail_mm;
|
||||
params.subdiv_min_edge_mm = m_subdivide_min_edge_mm;
|
||||
params.subdiv_border_mm = m_subdivide_border_mm;
|
||||
params.subdiv_color_edge_mm = m_subdivide_color_mm;
|
||||
params.subdiv_feature = m_subdivide_feature;
|
||||
params.subdiv_added_triangles = m_subdivide_budget_k * 1000;
|
||||
queue_prepare(params, _u8L("Adaptive subdivide for texture displacement"), /* then_bake */ false,
|
||||
@@ -3560,9 +3881,17 @@ void GLGizmoTextureDisplacement::rebuild_subdivide_preview()
|
||||
// Min-edge floor. Plain adaptive: tol 0, so only the target-edge-length criterion applies.
|
||||
const float tol = m_subdivide_feature ? m_subdivide_detail_mm : 0.f;
|
||||
const float floor = m_subdivide_feature ? m_subdivide_min_edge_mm : 0.f;
|
||||
// Same colour criterion Apply will use, so the previewed wireframe is the mesh that commits.
|
||||
ColorFieldSampler color;
|
||||
if (m_subdivide_color_mm > 0.f && any_layer_colors(*mv)) {
|
||||
cached_palette(); // refreshes m_palette_quantizer if the filaments changed
|
||||
color = make_combined_color_sampler(mv->mesh().its, mv->texture_displacement_layers, facets,
|
||||
m_palette_quantizer);
|
||||
}
|
||||
its = subdivide_mesh_adaptive(mv->mesh().its, region, m_subdivide_target_mm,
|
||||
int(mv->mesh().its.indices.size()) + m_subdivide_budget_k * 1000,
|
||||
nullptr, sampler, tol, floor, m_subdivide_border_mm);
|
||||
nullptr, sampler, tol, floor, m_subdivide_border_mm, nullptr, color,
|
||||
m_subdivide_color_mm);
|
||||
} else {
|
||||
if (m_subdivide_count < 1)
|
||||
return;
|
||||
@@ -3634,6 +3963,25 @@ GLTexture *GLGizmoTextureDisplacement::get_layer_thumbnail(const TextureDisplace
|
||||
return m_thumbnails[slot].get();
|
||||
}
|
||||
|
||||
GLTexture *GLGizmoTextureDisplacement::get_layer_color_texture(const TextureDisplacementLayer &layer)
|
||||
{
|
||||
if (layer.empty() || !layer.color_enabled)
|
||||
return nullptr;
|
||||
if (m_color_tex && m_color_tex_source == layer.image_data.get() && m_color_tex_smoothing == layer.smoothing)
|
||||
return m_color_tex.get();
|
||||
|
||||
std::unique_ptr<GLTexture> texture = upload_color_texture(decode_height_texture(layer), HEIGHT_TEX_MAX_PX);
|
||||
if (!texture) {
|
||||
m_color_tex.reset();
|
||||
m_color_tex_source = nullptr;
|
||||
return nullptr;
|
||||
}
|
||||
m_color_tex = std::move(texture);
|
||||
m_color_tex_source = layer.image_data.get();
|
||||
m_color_tex_smoothing = layer.smoothing;
|
||||
return m_color_tex.get();
|
||||
}
|
||||
|
||||
GLTexture *GLGizmoTextureDisplacement::get_layer_height_texture(const TextureDisplacementLayer &layer)
|
||||
{
|
||||
if (layer.empty())
|
||||
@@ -3670,7 +4018,7 @@ void GLGizmoTextureDisplacement::bake(bool own_snapshot)
|
||||
}
|
||||
|
||||
m_bake_in_progress = true;
|
||||
queue_texture_displacement_bake(*mv, [this]() {
|
||||
queue_texture_displacement_bake(*mv, color_settings_for(*mv), [this]() {
|
||||
m_bake_in_progress = false;
|
||||
// Baking replaces the volume's mesh (new id, new topology) without changing the object's
|
||||
// id or volume count, so GLGizmoPainterBase::data_changed()'s usual change-detection never
|
||||
@@ -3700,6 +4048,10 @@ static constexpr float STD_SUBDIV_MIN_EDGE_MM = 0.02f;
|
||||
// how clean the rim of an unpainted island looks: the bake steps the surface from full displacement to
|
||||
// zero across that band, and nothing else in the criteria can see the step (see collect_paint_region()).
|
||||
static constexpr float STD_SUBDIV_BORDER_MM = 0.4f;
|
||||
// Edge length a colour boundary is refined to. Finer than the border band because a colour edge is
|
||||
// what the eye actually lands on - a stepped outline around a printed decal reads as a defect in a
|
||||
// way a slightly coarse relief transition does not - and it costs triangles along an outline only.
|
||||
static constexpr float STD_SUBDIV_COLOR_MM = 0.25f;
|
||||
|
||||
bool GLGizmoTextureDisplacement::apply_standard_mode_presets(ModelVolume *mv)
|
||||
{
|
||||
@@ -3721,6 +4073,7 @@ bool GLGizmoTextureDisplacement::apply_standard_mode_presets(ModelVolume *mv)
|
||||
pin(m_subdivide_detail_mm, STD_SUBDIV_DETAIL_MM);
|
||||
pin(m_subdivide_min_edge_mm, STD_SUBDIV_MIN_EDGE_MM);
|
||||
pin(m_subdivide_border_mm, STD_SUBDIV_BORDER_MM);
|
||||
pin(m_subdivide_color_mm, STD_SUBDIV_COLOR_MM);
|
||||
// Deliberately *not* pinned: the triangle budget stays visible and editable in Standard mode, so
|
||||
// pinning it would fight the user's own slider every frame.
|
||||
pin(m_remesh_target_edge_mm, STD_REMESH_EDGE_MM);
|
||||
@@ -3752,6 +4105,7 @@ void GLGizmoTextureDisplacement::bake_standard()
|
||||
params.subdiv_detail_mm = STD_SUBDIV_DETAIL_MM;
|
||||
params.subdiv_min_edge_mm = STD_SUBDIV_MIN_EDGE_MM;
|
||||
params.subdiv_border_mm = STD_SUBDIV_BORDER_MM;
|
||||
params.subdiv_color_edge_mm = STD_SUBDIV_COLOR_MM;
|
||||
params.subdiv_feature = true;
|
||||
params.subdiv_added_triangles = m_subdivide_budget_k * 1000;
|
||||
queue_prepare(params, _u8L("Bake texture displacement"), /* then_bake */ true, {});
|
||||
@@ -3772,6 +4126,8 @@ void GLGizmoTextureDisplacement::queue_prepare(const TextureDisplacementPrepareP
|
||||
input.layers = mv->texture_displacement_layers;
|
||||
input.params = params;
|
||||
input.snapshot_name = snapshot_name;
|
||||
if (params.subdiv_color_edge_mm > 0.f)
|
||||
input.color = color_settings_for(*mv);
|
||||
|
||||
m_prepare_in_progress = true;
|
||||
queue_texture_displacement_prepare(std::move(input), [this, then_bake, unchanged_msg](
|
||||
@@ -4216,6 +4572,84 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
|
||||
m_preview_params_dirty |= ImGui::Checkbox(_u8L("Invert").c_str(), &layer->invert);
|
||||
|
||||
// Colour. Only offered for a texture that actually has some - the shipped library is
|
||||
// grayscale, and a checkbox that silently does nothing on nine textures out of ten is
|
||||
// worse than no checkbox. Disabled rather than hidden so it is clear the feature exists
|
||||
// and what it wants.
|
||||
{
|
||||
const bool has_color = decode_height_texture(*layer).has_color();
|
||||
m_imgui->disabled_begin(!has_color);
|
||||
bool color_enabled = layer->color_enabled && has_color;
|
||||
if (ImGui::Checkbox(_u8L("Use image colours").c_str(), &color_enabled)) {
|
||||
layer->color_enabled = color_enabled;
|
||||
m_preview_params_dirty = true;
|
||||
}
|
||||
m_imgui->disabled_end();
|
||||
if (ImGui::IsItemHovered())
|
||||
m_imgui->tooltip(has_color ?
|
||||
_u8L("Colours the painted area from the texture's own colours, as well as "
|
||||
"displacing it. Each colour is matched to the closest printable "
|
||||
"colour, and the result is written as multi-material paint - so the "
|
||||
"area prints in those filaments. Everything you did not paint keeps "
|
||||
"the object's own filament.") :
|
||||
_u8L("This texture is a grayscale height map, so it has no colours to "
|
||||
"apply. Import a colour image to use this."),
|
||||
m_imgui->scaled(20.f));
|
||||
|
||||
// The rest of colour belongs to the whole stack, not to this layer, so it only appears
|
||||
// once - under whichever layer turned colour on.
|
||||
if (color_enabled && mv != nullptr) {
|
||||
TextureDisplacementOptions &opts = mv->texture_displacement_options;
|
||||
|
||||
if (ImGui::Checkbox(_u8L("Mix filaments").c_str(), &opts.color_mix_enabled))
|
||||
m_preview_params_dirty = true;
|
||||
if (ImGui::IsItemHovered())
|
||||
m_imgui->tooltip(_u8L("Interleave pairs of filaments to reach colours between them, so a few "
|
||||
"filaments cover far more than a few colours. Off means every triangle "
|
||||
"prints in one of the filaments exactly."),
|
||||
m_imgui->scaled(20.f));
|
||||
|
||||
if (opts.color_mix_enabled) {
|
||||
m_imgui->text(_u8L("Mix by"));
|
||||
ImGui::SameLine();
|
||||
const std::string mix_z = _u8L("Layers");
|
||||
const std::string mix_xy = _u8L("Surface");
|
||||
const char *mix_items[] = { mix_z.c_str(), mix_xy.c_str() };
|
||||
int mix_mode = int(opts.color_mix_mode);
|
||||
ImGui::PushItemWidth(m_imgui->scaled(8.4f));
|
||||
if (scoped_combo("##color_mix_mode", &mix_mode, mix_items, IM_ARRAYSIZE(mix_items))) {
|
||||
opts.color_mix_mode = ColorMixMode(mix_mode);
|
||||
m_preview_params_dirty = true;
|
||||
}
|
||||
ImGui::PopItemWidth();
|
||||
if (ImGui::IsItemHovered())
|
||||
m_imgui->tooltip(_u8L("Layers: the two filaments alternate between print layers, which "
|
||||
"blends smoothly on upright surfaces but disappears on flat-facing "
|
||||
"ones, where a whole layer is a single band.\n"
|
||||
"Surface: a fine checkerboard across the surface, which works at "
|
||||
"any angle but can read as texture rather than as a blend."),
|
||||
m_imgui->scaled(20.f));
|
||||
|
||||
const int count = int(cached_palette().size());
|
||||
ImGui::TextDisabled("%s", from_u8(Slic3r::format(
|
||||
_u8L("%1% printable colours from %2% filaments"), count,
|
||||
int(m_palette_filaments.size()))).ToUTF8().data());
|
||||
}
|
||||
|
||||
ImGui::PushItemWidth(m_imgui->scaled(8.4f));
|
||||
if (ImGui::SliderInt(_u8L("Denoise").c_str(), &opts.color_despeckle, 0, 6))
|
||||
m_preview_params_dirty = true;
|
||||
ImGui::PopItemWidth();
|
||||
if (ImGui::IsItemHovered())
|
||||
m_imgui->tooltip(_u8L("Removes stray single triangles of the wrong colour, which is what "
|
||||
"detail in the image finer than the mesh leaves behind. Each step "
|
||||
"replaces a triangle's colour with the one most of its neighbours "
|
||||
"have, so features wider than a triangle are kept. Raise it if the "
|
||||
"result looks speckled; lower it if fine detail is being eaten."),
|
||||
m_imgui->scaled(20.f));
|
||||
}
|
||||
}
|
||||
|
||||
// The lowest painted layer has nothing underneath it to combine with - it *is* the
|
||||
// base - so a blend mode would be meaningless (and Multiply/Divide against an implicit
|
||||
// zero would annihilate it). build_texture_displacement() forces the first layer to
|
||||
@@ -4784,6 +5218,21 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
"0 turns it off."),
|
||||
m_imgui->scaled(20.f));
|
||||
|
||||
// Only worth showing when a layer is actually colouring: with no colour there is no boundary
|
||||
// for it to refine and the control would do nothing whatever it is set to.
|
||||
if (mv != nullptr && any_layer_colors(*mv)) {
|
||||
if (m_imgui->slider_float(std::string(_u8L("Colour detail (mm)")) + "##subdivcolor",
|
||||
&m_subdivide_color_mm, 0.f, 5.f, "%.3f"))
|
||||
preview_live();
|
||||
if (ImGui::IsItemHovered())
|
||||
m_imgui->tooltip(_u8L("Triangle size along a boundary between two colours. Colour is assigned per "
|
||||
"triangle, so an edge between two filaments can only be as clean as the "
|
||||
"triangles along it - and the relief criteria cannot see it at all, because "
|
||||
"the surface is perfectly smooth across a change of colour. Costs triangles "
|
||||
"along the boundaries only. 0 turns it off."),
|
||||
m_imgui->scaled(20.f));
|
||||
}
|
||||
|
||||
ImGui::PopItemWidth();
|
||||
budget_slider();
|
||||
|
||||
|
||||
@@ -45,11 +45,80 @@ public:
|
||||
const TextureDisplacementFacetsData &masks,
|
||||
const std::vector<TextureDisplacementLayer> &layers,
|
||||
const TextureDisplacementPrepareParams ¶ms,
|
||||
const std::vector<PrintableColor> &palette,
|
||||
const DisplacementProgressFn &progress);
|
||||
// The volume's eight texture-displacement masks, gathered into the array every pure function here
|
||||
// (and every job input) takes.
|
||||
static TextureDisplacementFacetsData facets_data_of(const ModelVolume &mv);
|
||||
|
||||
using PaletteEntry = PrintableColor;
|
||||
|
||||
// The printable palette: the loaded filaments (clamped to the sixteen mmu_segmentation_facets can
|
||||
// address), plus - when `mixing` - every pair of them at evenly spaced ratios.
|
||||
//
|
||||
// Mixes are averaged in **CIELAB**, not RGB and not subtractively: two filaments interleaved too
|
||||
// finely to resolve are averaged by the eye, which is what a perceptual space models. Yellow and
|
||||
// blue banded together read as a desaturated grey-green, and that is what the preview must promise
|
||||
// - blending them subtractively would show a green the printer cannot produce this way.
|
||||
//
|
||||
// How many ratios depends on how many filaments there are, so the palette stays bounded: the
|
||||
// quantizer's lookup cube costs one DeltaE00 per cell per entry to fill, and with sixteen
|
||||
// filaments there are already plenty of colours without mixing any of them.
|
||||
static std::vector<PaletteEntry> make_palette(const std::vector<ColorRGBA> &filaments, bool mixing);
|
||||
|
||||
// Maps an image colour to the closest entry of `palette`, perceptually (CIEDE2000 over CIELAB - a
|
||||
// plain RGB distance picks visibly wrong filaments, most obviously between a saturated colour and
|
||||
// a grey of similar brightness).
|
||||
//
|
||||
// Precomputed into a lookup cube rather than matched per call: the subdivision's colour criterion
|
||||
// samples up to seven points per triangle and re-samples both children of every split, so a live
|
||||
// match would dominate the refinement. The returned closure owns the cube, so it is safe to hand
|
||||
// to a worker thread and outlives the palette it was built from.
|
||||
static ColorQuantizeFn make_palette_quantizer(const std::vector<PaletteEntry> &palette);
|
||||
|
||||
// Turns a palette index plus a position into the filament to print there, interleaving the two
|
||||
// filaments of a mixed entry per `mode`. `layer_height` sizes the Z bands; `cell_mm` the dither
|
||||
// cells. See ColorResolveFn for why this is separate from the quantizer.
|
||||
static ColorResolveFn make_mix_resolver(const std::vector<PaletteEntry> &palette, ColorMixMode mode,
|
||||
float layer_height, float cell_mm);
|
||||
|
||||
// Everything the jobs need to colour with, for the current volume: palette, mix mode, layer
|
||||
// height, despeckle. Empty when no layer is actually colouring.
|
||||
TextureColorSettings color_settings_for(const ModelVolume &mv);
|
||||
|
||||
// The printable palette for the current filaments and mixing setting, rebuilt only when either
|
||||
// actually changes - see the definition for why that caching is not optional.
|
||||
const std::vector<PaletteEntry> &cached_palette();
|
||||
std::vector<PaletteEntry> m_palette_cache;
|
||||
std::vector<ColorRGBA> m_palette_filaments;
|
||||
bool m_palette_mixing = false;
|
||||
ColorQuantizeFn m_palette_quantizer;
|
||||
|
||||
// The loaded filaments, clamped to the sixteen mmu_segmentation_facets can address.
|
||||
static std::vector<ColorRGBA> filament_palette();
|
||||
// The print's layer height, which sizes ColorMixMode::ZBands. Falls back to 0.2 mm if it cannot be
|
||||
// read - a wrong band size is a cosmetic error, not a reason to refuse to colour anything.
|
||||
static float print_layer_height();
|
||||
|
||||
// The Normal preview's triangles, grouped by the filament they will print in. Colour is per facet
|
||||
// and there are at most sixteen filaments, so the mesh is uploaded once with its index buffer
|
||||
// sorted by colour and drawn as one GLModel::render(range) per group - which needs no per-vertex
|
||||
// colour attribute, and so no change to GLModel's vertex layouts.
|
||||
//
|
||||
// The *index buffer* is what gets reordered, never m_preview_its: the paint overlay and the
|
||||
// wireframe index into that by the volume's own triangle numbering (the bake is
|
||||
// topology-preserving), and permuting it would silently misplace both.
|
||||
struct PreviewColorRun
|
||||
{
|
||||
std::pair<size_t, size_t> range; // into the GLModel's index buffer, in elements
|
||||
ColorRGBA color;
|
||||
};
|
||||
std::vector<PreviewColorRun> m_preview_color_runs;
|
||||
// True if any of the volume's layers would actually colour something: colour turned on, and a
|
||||
// texture that has colour to give. What decides whether a palette is captured into a job at all,
|
||||
// and so whether the colour criterion and the mmu write ever run.
|
||||
static bool any_layer_colors(const ModelVolume &mv);
|
||||
|
||||
void render_painter_gizmo() override;
|
||||
|
||||
// Intercepts mouse input while "Adjust Texture" mode is on (dragging the on-canvas offset/
|
||||
@@ -214,6 +283,9 @@ private:
|
||||
// The same texture at full resolution, for the fast-preview shader. One slot, shared by whichever
|
||||
// layer is active, because that is the only one the bump shader ever shades.
|
||||
GLTexture *get_layer_height_texture(const TextureDisplacementLayer &layer);
|
||||
// The layer's colour texture for the fast preview's per-fragment quantization. Null when the
|
||||
// layer is not colouring or its texture is grayscale.
|
||||
GLTexture *get_layer_color_texture(const TextureDisplacementLayer &layer);
|
||||
|
||||
// A texture from the picker's library (see slic3r/GUI/TextureLibrary.hpp), read and uploaded
|
||||
// once and then kept for the gizmo's lifetime. The decoded bytes are held alongside the GPU
|
||||
@@ -423,6 +495,13 @@ private:
|
||||
// transition keeps the input's density and the rim of an unpainted island comes out as a ring of
|
||||
// large, steeply tilted triangles. See collect_paint_region() and subdivide_mesh_adaptive().
|
||||
float m_subdivide_border_mm = 0.4f;
|
||||
// Edge length triangles straddling a *colour* boundary are refined to (0 = ignore colour). Its own
|
||||
// control rather than a share of "Detail (mm)" because the two measure different things: Detail is
|
||||
// a chord error in mm of surface deviation, this is a triangle size in mm along a step the chord
|
||||
// test cannot see at all - the height field is perfectly smooth across a change of filament, so
|
||||
// without this a colour boundary lands on whatever triangles the relief happened to need, which on
|
||||
// a flat surface is none. Only ever costs anything where a boundary actually runs.
|
||||
float m_subdivide_color_mm = 0.3f;
|
||||
// How many thousand triangles refinement may *add* (the mesh's own count is added on before it is
|
||||
// passed as subdivide_mesh_adaptive()'s absolute cap, so the control still means something on a
|
||||
// dense model). Refinement is worst-error-first, so hitting the budget still yields the best mesh
|
||||
@@ -504,6 +583,12 @@ private:
|
||||
// Whether the current bump mesh carries a precomputed per-vertex uv (LSCM) that the shader
|
||||
// should sample at directly, rather than projecting in-shader. Set by rebuild_bump_preview_mesh().
|
||||
bool m_bump_preview_uses_vertex_uv = false;
|
||||
// The palette the fast preview's per-triangle filament indices were built against, captured when
|
||||
// the mesh was. Empty when the active layer is not colouring, which is what tells the shader to
|
||||
// fall back to the model's own colour. Held rather than re-read at draw time so the indices baked
|
||||
// into the mesh can never be resolved against a different set of filaments than they were computed
|
||||
// from - loading a filament mid-session would otherwise recolour a stale preview at random.
|
||||
std::vector<PaletteEntry> m_bump_preview_palette;
|
||||
|
||||
// GPU island drag: while an island is dragged in the UV editor, the bump mesh is baked once (with
|
||||
// the dragged island's vertices flagged, v_normal.y = 1) and then moved purely through the shader's
|
||||
@@ -621,6 +706,12 @@ private:
|
||||
const void *m_height_tex_source = nullptr;
|
||||
float m_height_tex_smoothing = -1.f;
|
||||
|
||||
// The same, for the layer's *colour*, which the fast preview quantizes per fragment so it shows
|
||||
// the image at texel resolution rather than at the mesh's. Null for a grayscale texture.
|
||||
std::unique_ptr<GLTexture> m_color_tex;
|
||||
const void *m_color_tex_source = nullptr;
|
||||
float m_color_tex_smoothing = -1.f;
|
||||
|
||||
// Library textures the picker has shown at least once, keyed by file path (see LibraryTexture).
|
||||
std::map<std::string, LibraryTexture> m_library_textures;
|
||||
|
||||
|
||||
@@ -3,10 +3,12 @@
|
||||
#include <algorithm>
|
||||
|
||||
#include "libslic3r/Model.hpp"
|
||||
#include "libslic3r/TriangleSelector.hpp"
|
||||
|
||||
#include "slic3r/GUI/GLCanvas3D.hpp"
|
||||
#include "slic3r/GUI/GUI_App.hpp"
|
||||
#include "slic3r/GUI/GUI_ObjectList.hpp"
|
||||
#include "slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp"
|
||||
#include "slic3r/GUI/I18N.hpp"
|
||||
#include "slic3r/GUI/Plater.hpp"
|
||||
#include "slic3r/Utils/UndoRedo.hpp"
|
||||
@@ -30,6 +32,21 @@ void TextureDisplacementBakeJob::process(Ctl &ctl)
|
||||
// grows a close button once it reaches 100%, so a job that reports 0 and nothing else leaves an
|
||||
// uncloseable notification pinned on screen. It also carries the Cancel button's effect into the
|
||||
// bake, which on a subdivided mesh can run for several seconds.
|
||||
// Colour, when any layer asks for it, is computed in the same pass as the displacement: both need
|
||||
// the same per-layer projection and UV work, and doing it twice would double the expensive part.
|
||||
TextureColorRequest color_request;
|
||||
TextureColorRequest *color = nullptr;
|
||||
if (!m_input.color.empty()) {
|
||||
color_request.quantize = GLGizmoTextureDisplacement::make_palette_quantizer(m_input.color.palette);
|
||||
color_request.resolve = GLGizmoTextureDisplacement::make_mix_resolver(
|
||||
m_input.color.palette, m_input.color.mix_mode, m_input.color.layer_height,
|
||||
m_input.color.dither_cell_mm);
|
||||
color_request.despeckle_passes = m_input.color.despeckle_passes;
|
||||
color_request.out_triangle = &m_triangle_color;
|
||||
if (color_request.quantize)
|
||||
color = &color_request;
|
||||
}
|
||||
|
||||
int last_reported = 1;
|
||||
m_result = TriangleMesh(build_texture_displacement(
|
||||
m_input.base_mesh, m_input.layers, m_input.facets_data, m_input.options,
|
||||
@@ -43,7 +60,8 @@ void TextureDisplacementBakeJob::process(Ctl &ctl)
|
||||
ctl.update_status(percent, status);
|
||||
}
|
||||
return true;
|
||||
}));
|
||||
},
|
||||
color));
|
||||
|
||||
// Always finish at 100: this is what closes the notification. Reported even on cancel, where
|
||||
// build_texture_displacement() returns an empty mesh and finalize() commits nothing.
|
||||
@@ -72,6 +90,23 @@ void TextureDisplacementBakeJob::finalize(bool canceled, std::exception_ptr &ept
|
||||
volume->set_new_unique_id();
|
||||
volume->calculate_convex_hull();
|
||||
|
||||
// Colour lands in mmu_segmentation_facets, merged *over* whatever is already painted there
|
||||
// rather than replacing it: a triangle the texture does not colour keeps its existing filament,
|
||||
// and one the user never painted at all stays at NONE, which already means "the volume's own
|
||||
// filament". That is what confines the effect to the painted area without having to invent a
|
||||
// colour for everything outside it. Safe to index straight onto the new mesh - the bake is
|
||||
// topology-preserving, so triangle i is still triangle i.
|
||||
if (!m_triangle_color.empty() && m_triangle_color.size() == volume->mesh().its.indices.size()) {
|
||||
TriangleSelector selector(volume->mesh());
|
||||
const TriangleSelector::TriangleSplittingData &existing = volume->mmu_segmentation_facets.get_data();
|
||||
if (!existing.bitstream.empty())
|
||||
selector.deserialize(existing, false);
|
||||
for (size_t i = 0; i < m_triangle_color.size(); ++i)
|
||||
if (m_triangle_color[i] > 0)
|
||||
selector.set_facet(int(i), EnforcerBlockerType(m_triangle_color[i]));
|
||||
volume->mmu_segmentation_facets.set(selector);
|
||||
}
|
||||
|
||||
// Clear the paint mask of every layer that was actually baked so a repeat bake (or the paint
|
||||
// overlay) doesn't act on triangles that no longer represent the same unbaked surface. The
|
||||
// texture layer definitions themselves (and paint outside the baked area, if any) are left
|
||||
@@ -107,10 +142,11 @@ void TextureDisplacementBakeJob::finalize(bool canceled, std::exception_ptr &ept
|
||||
}
|
||||
}
|
||||
|
||||
void queue_texture_displacement_bake(const ModelVolume &volume, std::function<void()> on_finished,
|
||||
bool take_snapshot)
|
||||
void queue_texture_displacement_bake(const ModelVolume &volume, const TextureColorSettings &color,
|
||||
std::function<void()> on_finished, bool take_snapshot)
|
||||
{
|
||||
TextureDisplacementBakeInput input;
|
||||
input.color = color;
|
||||
input.take_snapshot = take_snapshot;
|
||||
input.volume_id = volume.id();
|
||||
input.base_mesh = volume.mesh().its;
|
||||
@@ -119,6 +155,7 @@ void queue_texture_displacement_bake(const ModelVolume &volume, std::function<vo
|
||||
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i)
|
||||
input.facets_data[size_t(i)] = volume.texture_displacement_facet(i).get_data();
|
||||
|
||||
|
||||
auto &worker = wxGetApp().plater()->get_ui_job_worker();
|
||||
queue_job(worker, std::make_unique<TextureDisplacementBakeJob>(std::move(input), std::move(on_finished)));
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <functional>
|
||||
#include <vector>
|
||||
|
||||
#include "libslic3r/Color.hpp"
|
||||
#include "libslic3r/ObjectID.hpp"
|
||||
#include "libslic3r/TextureDisplacement.hpp"
|
||||
#include "libslic3r/TriangleMesh.hpp"
|
||||
@@ -22,6 +23,10 @@ struct TextureDisplacementBakeInput
|
||||
std::vector<TextureDisplacementLayer> layers;
|
||||
TextureDisplacementFacetsData facets_data;
|
||||
TextureDisplacementOptions options;
|
||||
// Captured on the main thread. Empty unless some layer is colouring, in which case the bake also
|
||||
// writes the volume's mmu_segmentation_facets - the same per-triangle filament assignment the MMU
|
||||
// paint gizmo writes - alongside the displaced geometry.
|
||||
TextureColorSettings color;
|
||||
// Whether this job pushes its own undo step when it commits. False when the caller has already
|
||||
// taken one that is meant to cover the displacement as well - Standard mode's Bake, which remeshes
|
||||
// and subdivides first and has to undo as a single action.
|
||||
@@ -42,6 +47,9 @@ public:
|
||||
private:
|
||||
TextureDisplacementBakeInput m_input;
|
||||
TriangleMesh m_result;
|
||||
// Per triangle of m_result: the filament to print it in, as an EnforcerBlockerType value
|
||||
// (0 = leave alone). Empty unless a layer asked for colour. See TextureColorRequest.
|
||||
std::vector<uint8_t> m_triangle_color;
|
||||
std::function<void()> m_on_finished;
|
||||
};
|
||||
|
||||
@@ -49,8 +57,8 @@ private:
|
||||
// the app's UI job worker. `on_finished` is always called once the job settles (success, failure,
|
||||
// or cancellation), so the caller can clear its own "bake in progress" UI state. Must be called
|
||||
// from the main thread.
|
||||
void queue_texture_displacement_bake(const ModelVolume &volume, std::function<void()> on_finished,
|
||||
bool take_snapshot = true);
|
||||
void queue_texture_displacement_bake(const ModelVolume &volume, const TextureColorSettings &color,
|
||||
std::function<void()> on_finished, bool take_snapshot = true);
|
||||
|
||||
} // namespace Slic3r::GUI
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ void TextureDisplacementPrepareJob::process(Ctl &ctl)
|
||||
// idle loop.
|
||||
int last_reported = 1;
|
||||
m_result = GLGizmoTextureDisplacement::prepare_mesh(m_input.base_mesh, m_input.masks, m_input.layers,
|
||||
m_input.params,
|
||||
m_input.params, m_input.color.palette,
|
||||
[&ctl, &status, &last_reported](int percent) {
|
||||
if (ctl.was_canceled())
|
||||
return false;
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
#include "Job.hpp"
|
||||
|
||||
#include "libslic3r/Color.hpp"
|
||||
#include "libslic3r/ObjectID.hpp"
|
||||
#include "libslic3r/TextureDisplacement.hpp"
|
||||
#include "libslic3r/TriangleMesh.hpp"
|
||||
@@ -43,6 +44,10 @@ struct TextureDisplacementPrepareInput
|
||||
TextureDisplacementFacetsData masks;
|
||||
std::vector<TextureDisplacementLayer> layers;
|
||||
TextureDisplacementPrepareParams params;
|
||||
// Captured on the main thread. Empty when no layer is colouring, in which case the refinement
|
||||
// skips the colour criterion entirely. Only the *quantizer* is used here: refinement follows
|
||||
// perceived colour, never the interleaving that realises a mix.
|
||||
TextureColorSettings color;
|
||||
// The undo step the commit opens. Standard mode's Bake names it after the bake, because the
|
||||
// displacement job that follows commits into this same step rather than pushing its own.
|
||||
std::string snapshot_name;
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
#include "TextureDisplacementPreviewJob.hpp"
|
||||
|
||||
#include "slic3r/GUI/I18N.hpp"
|
||||
#include "slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp"
|
||||
|
||||
namespace Slic3r::GUI {
|
||||
|
||||
TextureDisplacementPreviewJob::TextureDisplacementPreviewJob(TextureDisplacementPreviewInput &&input, uint64_t generation,
|
||||
std::shared_ptr<const std::atomic<uint64_t>> current_generation,
|
||||
std::function<void(indexed_triangle_set, uint64_t)> on_finished)
|
||||
std::function<void(TextureDisplacementPreviewResult, uint64_t)> on_finished)
|
||||
: m_input(std::move(input)), m_generation(generation), m_current_generation(std::move(current_generation)),
|
||||
m_on_finished(std::move(on_finished))
|
||||
{
|
||||
@@ -19,7 +20,21 @@ void TextureDisplacementPreviewJob::process(Ctl &ctl)
|
||||
|
||||
// Only ever touches m_input (captured by value before this job was queued) and local state -
|
||||
// never the live Model - so this is safe to run concurrently with the UI thread.
|
||||
m_result = build_texture_displacement(m_input.base_mesh, m_input.layers, m_input.facets_data, m_input.options,
|
||||
TextureColorRequest color_request;
|
||||
TextureColorRequest *color = nullptr;
|
||||
if (!m_input.color.empty()) {
|
||||
color_request.quantize = GLGizmoTextureDisplacement::make_palette_quantizer(m_input.color.palette);
|
||||
color_request.resolve = GLGizmoTextureDisplacement::make_mix_resolver(
|
||||
m_input.color.palette, m_input.color.mix_mode, m_input.color.layer_height,
|
||||
m_input.color.dither_cell_mm);
|
||||
color_request.despeckle_passes = m_input.color.despeckle_passes;
|
||||
color_request.out_triangle = &m_result.triangle_color;
|
||||
if (color_request.quantize)
|
||||
color = &color_request;
|
||||
}
|
||||
|
||||
m_result.mesh = build_texture_displacement(m_input.base_mesh, m_input.layers, m_input.facets_data,
|
||||
m_input.options,
|
||||
[this, &ctl](int) {
|
||||
// Bail the moment this preview stops being the current
|
||||
// one; build_texture_displacement() then returns an
|
||||
@@ -27,7 +42,8 @@ void TextureDisplacementPreviewJob::process(Ctl &ctl)
|
||||
return !ctl.was_canceled() &&
|
||||
(!m_current_generation ||
|
||||
m_current_generation->load() == m_generation);
|
||||
});
|
||||
},
|
||||
color);
|
||||
}
|
||||
|
||||
void TextureDisplacementPreviewJob::finalize(bool canceled, std::exception_ptr &eptr)
|
||||
@@ -40,7 +56,7 @@ void TextureDisplacementPreviewJob::finalize(bool canceled, std::exception_ptr &
|
||||
// was ever queued again for the rest of the session. An empty result is the caller's signal that
|
||||
// nothing usable came back; it already handles that.
|
||||
if (canceled || eptr)
|
||||
m_on_finished(indexed_triangle_set{}, m_generation);
|
||||
m_on_finished(TextureDisplacementPreviewResult{}, m_generation);
|
||||
else
|
||||
m_on_finished(std::move(m_result), m_generation);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include "libslic3r/Color.hpp"
|
||||
#include "libslic3r/TextureDisplacement.hpp"
|
||||
#include "libslic3r/TriangleMesh.hpp"
|
||||
|
||||
@@ -22,6 +23,17 @@ struct TextureDisplacementPreviewInput
|
||||
std::vector<TextureDisplacementLayer> layers;
|
||||
TextureDisplacementFacetsData facets_data;
|
||||
TextureDisplacementOptions options;
|
||||
// Empty unless a layer is colouring, in which case the preview reports the filament per triangle
|
||||
// alongside the mesh, so the Normal view shows what the bake will produce - interleaving included.
|
||||
TextureColorSettings color;
|
||||
};
|
||||
|
||||
// A preview result: the displaced mesh, and - when the input carried a palette - one filament index
|
||||
// per triangle (an EnforcerBlockerType value; 0 means "no colour from the texture").
|
||||
struct TextureDisplacementPreviewResult
|
||||
{
|
||||
indexed_triangle_set mesh;
|
||||
std::vector<uint8_t> triangle_color;
|
||||
};
|
||||
|
||||
// Computes the true (unbaked) displaced-mesh preview in the background. With several painted
|
||||
@@ -49,7 +61,7 @@ public:
|
||||
// order: without it, a Bake queued behind a handful of stale previews waits for every one of them.
|
||||
TextureDisplacementPreviewJob(TextureDisplacementPreviewInput &&input, uint64_t generation,
|
||||
std::shared_ptr<const std::atomic<uint64_t>> current_generation,
|
||||
std::function<void(indexed_triangle_set, uint64_t)> on_finished);
|
||||
std::function<void(TextureDisplacementPreviewResult, uint64_t)> on_finished);
|
||||
|
||||
void process(Ctl &ctl) override;
|
||||
void finalize(bool canceled, std::exception_ptr &eptr) override;
|
||||
@@ -58,8 +70,8 @@ private:
|
||||
TextureDisplacementPreviewInput m_input;
|
||||
uint64_t m_generation;
|
||||
std::shared_ptr<const std::atomic<uint64_t>> m_current_generation;
|
||||
indexed_triangle_set m_result;
|
||||
std::function<void(indexed_triangle_set, uint64_t)> m_on_finished;
|
||||
TextureDisplacementPreviewResult m_result;
|
||||
std::function<void(TextureDisplacementPreviewResult, uint64_t)> m_on_finished;
|
||||
};
|
||||
|
||||
} // namespace Slic3r::GUI
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <boost/system/error_code.hpp>
|
||||
|
||||
#include <wx/image.h>
|
||||
#include <wx/mstream.h>
|
||||
|
||||
#include "libslic3r/PNGReadWrite.hpp"
|
||||
#include "libslic3r/Utils.hpp"
|
||||
@@ -47,6 +48,52 @@ void scan_dir(const boost::filesystem::path &dir, bool is_user, std::vector<Text
|
||||
[](const TextureLibraryEntry &a, const TextureLibraryEntry &b) { return a.name < b.name; });
|
||||
}
|
||||
|
||||
// True if the image carries any colour at all, i.e. some pixel's channels are not all equal. A
|
||||
// photo saved as RGB but actually grey should still be stored as a grayscale PNG - it is a third of
|
||||
// the bytes, and the colour feature keys off has_color(), so storing a grey image as RGB would offer
|
||||
// the user a "colour" that is a row of identical greys.
|
||||
bool image_has_color(const wxImage &image)
|
||||
{
|
||||
const unsigned char *rgb = image.GetData();
|
||||
if (rgb == nullptr)
|
||||
return false;
|
||||
const size_t n = size_t(image.GetWidth()) * size_t(image.GetHeight());
|
||||
for (size_t i = 0; i < n; ++i)
|
||||
if (rgb[i * 3] != rgb[i * 3 + 1] || rgb[i * 3 + 1] != rgb[i * 3 + 2])
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Colour images are stored as-is (as an RGB PNG) so the texture can colour the model as well as
|
||||
// displace it; decode_height_texture() takes the height from their luminance, with the same
|
||||
// coefficients ConvertToGreyscale() uses, so the relief is identical either way. wxImage writes the
|
||||
// PNG here rather than Slic3r::png, which only encodes grayscale.
|
||||
bool encode_color_png_bytes(const wxImage &image, std::vector<unsigned char> &out, std::string &error)
|
||||
{
|
||||
if (image.GetWidth() <= 0 || image.GetHeight() <= 0) {
|
||||
error = _u8L("The selected image is empty.");
|
||||
return false;
|
||||
}
|
||||
wxMemoryOutputStream stream;
|
||||
// Alpha would be lost on the way into DecodedHeightTexture anyway, and a partly transparent
|
||||
// texture reading as black relief is worse than reading as its own colour over the background.
|
||||
wxImage opaque = image;
|
||||
if (opaque.HasAlpha())
|
||||
opaque.ClearAlpha();
|
||||
if (!opaque.SaveFile(stream, wxBITMAP_TYPE_PNG)) {
|
||||
error = _u8L("Failed to prepare the texture for use.");
|
||||
return false;
|
||||
}
|
||||
const size_t size = size_t(stream.GetLength());
|
||||
out.resize(size);
|
||||
stream.CopyTo(out.data(), size);
|
||||
if (out.empty()) {
|
||||
error = _u8L("Failed to prepare the texture for use.");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Slic3r::png only writes PNGs to a file, so the encode round-trips through a temp file rather than
|
||||
// staying in memory. It happens once per import / per texture pick, not per frame, so the I/O is
|
||||
// not worth avoiding with a second PNG encoder.
|
||||
@@ -87,14 +134,22 @@ bool encode_gray_png_bytes(const wxImage &image, std::vector<unsigned char> &out
|
||||
return true;
|
||||
}
|
||||
|
||||
// Grayscale in, grayscale out; colour in, colour out.
|
||||
bool encode_png_bytes(const wxImage &image, std::vector<unsigned char> &out, std::string &error)
|
||||
{
|
||||
return image_has_color(image) ? encode_color_png_bytes(image, out, error)
|
||||
: encode_gray_png_bytes(image, out, error);
|
||||
}
|
||||
|
||||
std::vector<unsigned char> read_file_bytes(const std::string &path)
|
||||
{
|
||||
std::ifstream ifs(path, std::ios::binary);
|
||||
return std::vector<unsigned char>(std::istreambuf_iterator<char>(ifs), std::istreambuf_iterator<char>());
|
||||
}
|
||||
|
||||
// True if these bytes are already the 8-bit grayscale PNG libslic3r can decode, i.e. can be stored
|
||||
// on a layer as-is. Mirrors exactly what decode_height_texture() accepts.
|
||||
// True if these bytes are a PNG libslic3r can decode, i.e. can be stored on a layer as-is. Mirrors
|
||||
// exactly what decode_height_texture() accepts: 8-bit grayscale, or colour (whose luminance is the
|
||||
// height and whose RGB is available to colour the model).
|
||||
bool is_supported_height_map(const std::vector<unsigned char> &bytes)
|
||||
{
|
||||
if (bytes.empty())
|
||||
@@ -102,8 +157,12 @@ bool is_supported_height_map(const std::vector<unsigned char> &bytes)
|
||||
const png::ReadBuf rbuf{ bytes.data(), bytes.size() };
|
||||
if (!png::is_png(rbuf))
|
||||
return false;
|
||||
png::ImageGreyscale img;
|
||||
return png::decode_png(rbuf, img) && img.cols > 0 && img.rows > 0;
|
||||
png::ImageGreyscale gray;
|
||||
if (png::decode_png(rbuf, gray) && gray.cols > 0 && gray.rows > 0)
|
||||
return true;
|
||||
png::ImageColorscale color;
|
||||
return png::decode_colored_png(rbuf, color) && color.cols > 0 && color.rows > 0 &&
|
||||
color.bytes_per_pixel >= 3;
|
||||
}
|
||||
|
||||
std::vector<TextureLibraryEntry> g_library;
|
||||
@@ -153,7 +212,7 @@ std::optional<TextureLibraryEntry> import_texture_to_library(const std::string &
|
||||
}
|
||||
|
||||
std::vector<unsigned char> bytes;
|
||||
if (!encode_gray_png_bytes(image, bytes, error))
|
||||
if (!encode_png_bytes(image, bytes, error))
|
||||
return std::nullopt;
|
||||
|
||||
// Never overwrite an existing texture (the user's or, if they picked the same name twice, their
|
||||
@@ -192,15 +251,15 @@ std::shared_ptr<std::vector<unsigned char>> load_texture_image_data(const std::s
|
||||
if (is_supported_height_map(bytes))
|
||||
return std::make_shared<std::vector<unsigned char>>(std::move(bytes));
|
||||
|
||||
// Not an 8-bit grayscale PNG (a colour image somebody copied into the folder by hand, say):
|
||||
// convert it the same way an import would, but leave the file on disk alone.
|
||||
// Not a PNG at all (a jpg somebody copied into the folder by hand, say): convert it the same way
|
||||
// an import would - keeping its colour if it has any - but leave the file on disk alone.
|
||||
wxImage image;
|
||||
if (!image.LoadFile(from_u8(path)) || !image.IsOk()) {
|
||||
error = _u8L("Could not load the selected image.");
|
||||
return nullptr;
|
||||
}
|
||||
std::vector<unsigned char> converted;
|
||||
if (!encode_gray_png_bytes(image, converted, error))
|
||||
if (!encode_png_bytes(image, converted, error))
|
||||
return nullptr;
|
||||
return std::make_shared<std::vector<unsigned char>>(std::move(converted));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user