diff --git a/resources/images/delete_all_filaments.svg b/resources/images/delete_all_filaments.svg
new file mode 100644
index 0000000000..e2f3641300
--- /dev/null
+++ b/resources/images/delete_all_filaments.svg
@@ -0,0 +1 @@
+
diff --git a/resources/shaders/110/texture_displacement_shaded.fs b/resources/shaders/110/texture_displacement_shaded.fs
index 4a2980cd64..e2050137c9 100644
--- a/resources/shaders/110/texture_displacement_shaded.fs
+++ b/resources/shaders/110/texture_displacement_shaded.fs
@@ -35,13 +35,8 @@ uniform bool pure_only; // match against single filaments only (flat-c
// per triangle on the CPU, so the preview shows the pattern the bake will print.
uniform int palette_a[64];
uniform int palette_b[64];
-uniform int palette_num[64];
-uniform int palette_den[64];
uniform vec3 filament_rgb[16];
uniform int filament_count;
-uniform int mix_mode; // ColorMixMode: 0 Z bands, 1 XY dither, 2 auto
-uniform float layer_height; // mm; one Z band per print layer
-uniform float dither_cell; // mm; one XY dither cell
uniform sampler2D color_tex; // the layer's colour image, sampled at the same uv as the height
uniform bool has_color_tex;
uniform bool volume_mirrored;
@@ -229,63 +224,16 @@ int nearest_palette_entry(vec3 rgb)
}
// One 2x2 Bayer cell, {0, 2; 3, 1}, for x and y in {0, 1}.
-float bayer2(float x, float y) { return 2.0 * x + 3.0 * y - 4.0 * x * y; }
-// The colour the printer lays down at world point `pos` for palette entry `index`: its filament, or
-// for a mix whichever of its two filaments this point falls on. Mirrors make_mix_resolver() on the
-// CPU, floors on the band/cell size included. All the modular arithmetic is done in floats with
-// mod(), which wraps negative coordinates the way the CPU's ((v % n) + n) % n does and needs no
-// integer % (not available on every GLSL 1.10 target).
-vec3 printed_color(int index, vec3 pos, vec3 normal, vec3 footprint)
+// The colour the printer lays down at world point `pos` for palette entry `index`. Every entry names a
+// single filament: a mix is given its own mixed filament slot, whose components the slicer alternates
+// per print layer, so there is nothing left to interleave here.
+vec3 printed_color(int index)
{
int a = palette_a[index];
- int b = palette_b[index];
- if (a < 0 || a >= filament_count || b < 0 || b >= filament_count)
+ if (a < 0 || a >= filament_count)
return palette_rgb[index]; // no filament to resolve to: the entry's own colour
- if (a == b)
- return filament_rgb[a];
- float num = float(palette_num[index]);
- float den = float(palette_den[index]);
- // Auto: bands where the surface is steeper than ~45 degrees, the dominant filament elsewhere.
- if (mix_mode == 2 && abs(normal.z) >= 0.7)
- return filament_rgb[(num * 2.0 >= den) ? a : b];
-
- // Pre-filter. The interleave is an ordered dither the eye is meant to blend away, and no dither
- // blends when it is drawn at less than a few pixels per period - it aliases, which is what turned
- // every upright wall into horizontal streaks: the Z band cycle is den * layer_height (around a
- // millimetre), and every pixel of a row on a vertical wall shares one z, so each row came out as a
- // 1-bit threshold of the image at that row's phase. `footprint` is mm of world position per pixel,
- // so this is zoom- and resolution-correct rather than a tuned constant: where the print's own
- // pattern is finer than this view can resolve, show what the print looks like from here, which is
- // the entry's perceptual average. The Normal view remains where the per-facet truth lives.
- float period = (mix_mode == 1) ? 2.0 * max(dither_cell, 0.01) : den * max(layer_height, 0.01);
- float px = (mix_mode == 1) ? max(footprint.x, footprint.y) : footprint.z;
- float sharp = clamp(period / max(4.0 * px, 1e-6) - 0.5, 0.0, 1.0);
- if (sharp <= 0.0)
- return palette_rgb[index];
-
- vec3 picked;
- if (mix_mode == 1) {
- // Ordered 4x4 Bayer over floor(x / cell), floor(y / cell). The CPU's table
- // 0 8 2 10
- // 12 4 14 6
- // 3 11 1 9
- // 15 7 13 5
- // is 4 * bayer2(x % 2, y % 2) + bayer2(x / 2, y / 2), which needs no array (GLSL 1.10 has
- // no constant arrays).
- float cell = max(dither_cell, 0.01);
- float gx = mod(floor(pos.x / cell), 4.0);
- float gy = mod(floor(pos.y / cell), 4.0);
- float bayer = 4.0 * bayer2(mod(gx, 2.0), mod(gy, 2.0)) + bayer2(floor(gx / 2.0), floor(gy / 2.0));
- picked = filament_rgb[(num / den > (bayer + 0.5) / 16.0) ? a : b];
- } else {
- // Z bands: one per band height, the band's phase in the a/b cycle picks the filament. Both
- // operands are integer-valued, so the half keeps "phase < num" exact under float rounding.
- float slot = floor(pos.z / max(layer_height, 0.01));
- float phase = mod(slot, den);
- picked = filament_rgb[(phase < num - 0.5) ? a : b];
- }
- return mix(palette_rgb[index], picked, sharp);
+ return filament_rgb[a];
}
void main()
@@ -296,9 +244,6 @@ void main()
// World millimetres throughout, like the bake - see the 140 variant.
vec3 triangle_normal = normalize(cross(dFdx(world_pos.xyz), dFdy(world_pos.xyz)));
vec3 tex_pos = world_pos.xyz - tex_anchor; // the frame the texture is projected in, as the bake does
- // World mm per pixel, for pre-filtering the interleave in printed_color(). Taken here because the
- // albedo branch at the end of main() is non-uniform control flow, where derivatives are undefined.
- vec3 pos_fwidth = fwidth(world_pos.xyz);
if (volume_mirrored)
triangle_normal = -triangle_normal;
@@ -420,6 +365,6 @@ void main()
// orientation and scale about the volume's origin, see texture_displacement_bake_frame()), so
// measuring z from the bed instead shifted the band phase by the volume origin's height - a
// different filament in the same place than the bake produces.
- albedo = printed_color(nearest_palette_entry(texture2D(color_tex, color_uv).rgb), tex_pos, triangle_normal, pos_fwidth);
+ albedo = printed_color(nearest_palette_entry(texture2D(color_tex, color_uv).rgb));
gl_FragColor = vec4(vec3(intensity.y) + albedo * intensity.x, uniform_color.a);
}
diff --git a/resources/shaders/140/texture_displacement_shaded.fs b/resources/shaders/140/texture_displacement_shaded.fs
index 1d8bee10fa..4a2f2581a3 100644
--- a/resources/shaders/140/texture_displacement_shaded.fs
+++ b/resources/shaders/140/texture_displacement_shaded.fs
@@ -94,13 +94,8 @@ uniform bool pure_only; // match against single filaments only (flat-c
// per triangle on the CPU, so the preview shows the pattern the bake will print.
uniform int palette_a[64];
uniform int palette_b[64];
-uniform int palette_num[64];
-uniform int palette_den[64];
uniform vec3 filament_rgb[16];
uniform int filament_count;
-uniform int mix_mode; // ColorMixMode: 0 Z bands, 1 XY dither, 2 auto
-uniform float layer_height; // mm; one Z band per print layer
-uniform float dither_cell; // mm; one XY dither cell
uniform sampler2D color_tex; // the layer's colour image, sampled at the same uv as the height
uniform bool has_color_tex;
uniform bool volume_mirrored;
@@ -295,63 +290,16 @@ int nearest_palette_entry(vec3 rgb)
}
// One 2x2 Bayer cell, {0, 2; 3, 1}, for x and y in {0, 1}.
-float bayer2(float x, float y) { return 2.0 * x + 3.0 * y - 4.0 * x * y; }
-// The colour the printer lays down at world point `pos` for palette entry `index`: its filament, or
-// for a mix whichever of its two filaments this point falls on. Mirrors make_mix_resolver() on the
-// CPU, floors on the band/cell size included. All the modular arithmetic is done in floats with
-// mod(), which wraps negative coordinates the way the CPU's ((v % n) + n) % n does and needs no
-// integer % (not available on every GLSL 1.10 target).
-vec3 printed_color(int index, vec3 pos, vec3 normal, vec3 footprint)
+// The colour the printer lays down at world point `pos` for palette entry `index`. Every entry names a
+// single filament: a mix is given its own mixed filament slot, whose components the slicer alternates
+// per print layer, so there is nothing left to interleave here.
+vec3 printed_color(int index)
{
int a = palette_a[index];
- int b = palette_b[index];
- if (a < 0 || a >= filament_count || b < 0 || b >= filament_count)
+ if (a < 0 || a >= filament_count)
return palette_rgb[index]; // no filament to resolve to: the entry's own colour
- if (a == b)
- return filament_rgb[a];
- float num = float(palette_num[index]);
- float den = float(palette_den[index]);
- // Auto: bands where the surface is steeper than ~45 degrees, the dominant filament elsewhere.
- if (mix_mode == 2 && abs(normal.z) >= 0.7)
- return filament_rgb[(num * 2.0 >= den) ? a : b];
-
- // Pre-filter. The interleave is an ordered dither the eye is meant to blend away, and no dither
- // blends when it is drawn at less than a few pixels per period - it aliases, which is what turned
- // every upright wall into horizontal streaks: the Z band cycle is den * layer_height (around a
- // millimetre), and every pixel of a row on a vertical wall shares one z, so each row came out as a
- // 1-bit threshold of the image at that row's phase. `footprint` is mm of world position per pixel,
- // so this is zoom- and resolution-correct rather than a tuned constant: where the print's own
- // pattern is finer than this view can resolve, show what the print looks like from here, which is
- // the entry's perceptual average. The Normal view remains where the per-facet truth lives.
- float period = (mix_mode == 1) ? 2.0 * max(dither_cell, 0.01) : den * max(layer_height, 0.01);
- float px = (mix_mode == 1) ? max(footprint.x, footprint.y) : footprint.z;
- float sharp = clamp(period / max(4.0 * px, 1e-6) - 0.5, 0.0, 1.0);
- if (sharp <= 0.0)
- return palette_rgb[index];
-
- vec3 picked;
- if (mix_mode == 1) {
- // Ordered 4x4 Bayer over floor(x / cell), floor(y / cell). The CPU's table
- // 0 8 2 10
- // 12 4 14 6
- // 3 11 1 9
- // 15 7 13 5
- // is 4 * bayer2(x % 2, y % 2) + bayer2(x / 2, y / 2), which needs no array (GLSL 1.10 has
- // no constant arrays).
- float cell = max(dither_cell, 0.01);
- float gx = mod(floor(pos.x / cell), 4.0);
- float gy = mod(floor(pos.y / cell), 4.0);
- float bayer = 4.0 * bayer2(mod(gx, 2.0), mod(gy, 2.0)) + bayer2(floor(gx / 2.0), floor(gy / 2.0));
- picked = filament_rgb[(num / den > (bayer + 0.5) / 16.0) ? a : b];
- } else {
- // Z bands: one per band height, the band's phase in the a/b cycle picks the filament. Both
- // operands are integer-valued, so the half keeps "phase < num" exact under float rounding.
- float slot = floor(pos.z / max(layer_height, 0.01));
- float phase = mod(slot, den);
- picked = filament_rgb[(phase < num - 0.5) ? a : b];
- }
- return mix(palette_rgb[index], picked, sharp);
+ return filament_rgb[a];
}
void main()
@@ -364,9 +312,6 @@ void main()
// world position and perturb the world normal.
vec3 triangle_normal = normalize(cross(dFdx(world_pos.xyz), dFdy(world_pos.xyz)));
vec3 tex_pos = world_pos.xyz - tex_anchor; // the frame the texture is projected in, as the bake does
- // World mm per pixel, for pre-filtering the interleave in printed_color(). Taken here because the
- // albedo branch at the end of main() is non-uniform control flow, where derivatives are undefined.
- vec3 pos_fwidth = fwidth(world_pos.xyz);
if (volume_mirrored)
triangle_normal = -triangle_normal;
@@ -508,6 +453,6 @@ void main()
// orientation and scale about the volume's origin, see texture_displacement_bake_frame()), so
// measuring z from the bed instead shifted the band phase by the volume origin's height - a
// different filament in the same place than the bake produces.
- albedo = printed_color(nearest_palette_entry(texture(color_tex, color_uv).rgb), tex_pos, triangle_normal, pos_fwidth);
+ albedo = printed_color(nearest_palette_entry(texture(color_tex, color_uv).rgb));
out_color = vec4(vec3(intensity.y) + albedo * intensity.x, uniform_color.a);
}
diff --git a/src/libslic3r/TextureDisplacement.cpp b/src/libslic3r/TextureDisplacement.cpp
index 46a7cb18b4..fdd23d0a08 100644
--- a/src/libslic3r/TextureDisplacement.cpp
+++ b/src/libslic3r/TextureDisplacement.cpp
@@ -2283,12 +2283,11 @@ indexed_triangle_set build_texture_displacement_v2(const indexed_triangle_set
//
// The *palette* index, not the printed filament. The decimation treats any edge whose two faces
// differ as a crease (TextureBakeDecimate.cpp), so it must only ever see where the **perceived**
- // colour changes - which is exactly what ColorResolveFn's own contract says the interleaving may
- // never be fed into. Handing it the resolved filament made every Z band boundary a crease: on an
- // upright wall that is one crease per band, so the collapse ran along those lines and left a stack
- // of horizontal slivers, each printing in a single filament. Those were the horizontal colour
- // lines in the baked result, and they also spent the triangle budget drawing a pattern the eye is
- // meant to blend away. Faces the paint excludes are skipped by the pipeline itself.
+ // colour changes. A mix is one perceived colour however its components are laid down, which is why
+ // it has to be the palette index here: back when this was handed a per-triangle interleave instead,
+ // every band boundary read as a crease, the collapse ran along those lines and left a stack of
+ // horizontal slivers, and the triangle budget went on drawing a pattern the eye is meant to blend
+ // away. Faces the paint excludes are skipped by the pipeline itself.
const TextureBake::ColorSampleFn color_sample =
color_sampler ? TextureBake::ColorSampleFn([&color_sampler](const Vec3f &p, const Vec3f &n) {
return color_sampler(p, n);
@@ -2373,7 +2372,7 @@ indexed_triangle_set build_texture_displacement_v2(const indexed_triangle_set
Vec3f normal = (b - a).cross(c - a);
const float nl = normal.norm();
normal = (nl > 0.f) ? Vec3f(normal / nl) : Vec3f::UnitZ();
- const int filament = color->resolve ? color->resolve(palette[i], centroid, normal) : palette[i];
+ const int filament = palette[i];
if (filament >= 0)
out_color[i] = uint8_t(std::min(filament + 1, 255));
}
@@ -2830,8 +2829,7 @@ static indexed_triangle_set build_texture_displacement_in_place(
Vec3f normal = (b - a).cross(c - a);
const float nl = normal.norm();
normal = (nl > 0.f) ? Vec3f(normal / nl) : Vec3f::UnitZ();
- const int filament = color->resolve ? color->resolve(triangle_palette[i], centroid, normal)
- : triangle_palette[i];
+ const int filament = triangle_palette[i];
if (filament >= 0)
out_color[i] = uint8_t(std::min(filament + 1, 255));
}
diff --git a/src/libslic3r/TextureDisplacement.hpp b/src/libslic3r/TextureDisplacement.hpp
index afe6a42911..7ec4d6f1e5 100644
--- a/src/libslic3r/TextureDisplacement.hpp
+++ b/src/libslic3r/TextureDisplacement.hpp
@@ -322,25 +322,6 @@ struct TextureDisplacementLayer
}
};
-// How a *mixed* palette entry - one that names two filaments rather than one - is turned into real
-// per-facet paint. An MMU extrudes one filament at a time, so an intermediate colour exists only by
-// interleaving two of them finely enough that the eye does the blending.
-enum class ColorMixMode : int
-{
- // Horizontal bands: which of the two filaments a point takes depends on its height, so
- // consecutive print layers alternate. This is how filament-blend prints actually work, and on a
- // vertical-ish surface it reads as a genuinely smooth colour. On a near-horizontal surface a whole
- // layer is one band, so the blend disappears - that is what XYDither is for.
- ZBands = 0,
- // An ordered (Bayer) checkerboard across the surface, at any orientation. Independent of layer
- // height, but its cell is around the size of one facet, so a fine mix can read as texture rather
- // than as a clean blend.
- XYDither = 1,
- // Per triangle, by its orientation: bands where the surface is upright enough for consecutive
- // layers to alternate, the checkerboard where it faces up or down and a layer would be one band.
- // The default - a flat-topped part with a mix on top gets no blend at all from bands alone.
- Auto = 2,
-};
// Settings that apply to the whole layer stack rather than to one layer, held per ModelVolume next
// to texture_displacement_layers and consumed by build_texture_displacement().
@@ -407,7 +388,6 @@ struct TextureDisplacementOptions
// image (TextureDetail::flat_colors): a texture of flat colours prints in single filaments, a
// photograph or gradient in mixes. Off forces single filaments everywhere.
bool color_mix_enabled = true;
- ColorMixMode color_mix_mode = ColorMixMode::Auto;
// Majority-filter passes over the assigned colours. See TextureColorRequest::despeckle_passes -
// this is the control for it, and 2 is enough to clear the salt-and-pepper an image with detail
// finer than the mesh leaves behind, without eating features that are genuinely a facet wide.
@@ -415,11 +395,9 @@ struct TextureDisplacementOptions
template void serialize(Archive &ar)
{
- int mix_mode = int(color_mix_mode);
ar(displace_border, smooth_enabled, smooth_strength, smooth_iterations, smooth_skip_border,
pipeline_v2, v2_refine_mm, v2_regularize, v2_max_triangles_k,
- v2_relocate, color_mix_enabled, mix_mode, color_despeckle);
- color_mix_mode = ColorMixMode(mix_mode);
+ v2_relocate, color_mix_enabled, color_despeckle);
}
};
@@ -512,17 +490,9 @@ DecodedHeightTexture decode_height_texture(const TextureDisplacementLayer &layer
// is. See GLGizmoTextureDisplacement::make_palette_quantizer().
using ColorQuantizeFn = std::function;
-// Resolves a palette index plus a surface position to the filament index that position should print
-// in. A pure entry ignores the position; a mixed one interleaves its two filaments per ColorMixMode.
-//
-// Deliberately separate from ColorQuantizeFn, and deliberately *not* used by the subdivision's colour
-// criterion: that criterion asks where the **perceived** colour changes, and must not see the
-// interleaving. Refining on every band or dither-cell boundary would spend the whole triangle budget
-// drawing a pattern the eye is supposed to blend away.
-using ColorResolveFn = std::function;
// One printable colour: either a loaded filament on its own, or a blend of two of them realised by
-// interleaving (see ColorMixMode). Plain data, so it can be captured into a background job.
+// interleaving, which the slicer does per print layer. Plain data, so it can be captured into a job.
struct PrintableColor
{
Vec3f rgb = Vec3f::Zero(); // what it looks like; for a mix, the perceptual average of the two
@@ -539,9 +509,6 @@ struct TextureColorSettings
{
std::vector palette;
std::vector palette_pure; // the filaments alone, for flat-colour images
- ColorMixMode mix_mode = ColorMixMode::ZBands;
- float layer_height = 0.2f; // sizes the Z bands
- float dither_cell_mm = 0.4f; // sizes the XY dither cells
int despeckle_passes = 2;
bool empty() const { return palette.empty(); }
@@ -781,9 +748,6 @@ struct TextureColorRequest
// made of flat colours (TextureDetail::flat_colors) is matched with this one, so a tile or a logo
// prints in single filaments while a photograph on another layer may still use mixes.
ColorQuantizeFn quantize_pure;
- // Palette index + position -> filament. Optional: without it a palette index is taken to be a
- // filament index directly, which is the no-mixing case.
- ColorResolveFn resolve;
// Majority-filter passes over the *perceived* colour, before any interleaving is resolved.
//
// Sampling a detailed image once per triangle leaves salt-and-pepper wherever the image's own
diff --git a/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.cpp b/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.cpp
index fe1368b69e..89775f800b 100644
--- a/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.cpp
+++ b/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.cpp
@@ -1462,11 +1462,8 @@ void GLGizmoTextureDisplacement::render_shaded_preview_mesh()
// How the entry prints: its filament, or for a mix the two it interleaves and in what ratio.
shader->set_uniform(("palette_a" + idx).c_str(), e.a);
shader->set_uniform(("palette_b" + idx).c_str(), e.b);
- shader->set_uniform(("palette_num" + idx).c_str(), e.num);
- shader->set_uniform(("palette_den" + idx).c_str(), e.den);
}
// The filaments those indices refer to, and the interleave the shader resolves a mix with - the
- // same inputs make_mix_resolver() gets, so the preview shows the pattern that prints rather than
// the mix's smooth average colour. m_palette_filaments is what m_shaded_preview_palette was built from.
const int filament_count =
(palette_count > 0) ? int(std::min(m_palette_filaments.size(), size_t(PALETTE_MAX_FILAMENTS))) : 0;
@@ -1475,9 +1472,6 @@ void GLGizmoTextureDisplacement::render_shaded_preview_mesh()
const ColorRGBA &c = m_palette_filaments[size_t(i)];
shader->set_uniform(("filament_rgb[" + std::to_string(i) + "]").c_str(), Vec3f(c.r(), c.g(), c.b()));
}
- shader->set_uniform("mix_mode", int(mv->texture_displacement_options.color_mix_mode));
- shader->set_uniform("layer_height", color_band_mm(*mv)); // as color_settings_for()
- shader->set_uniform("dither_cell", std::max(m_subdivide_color_mm, 0.05f) * 2.f); // as color_settings_for()
if (color_tex != nullptr) {
shader->set_uniform("color_tex", 1);
glsafe(::glActiveTexture(GL_TEXTURE1));
@@ -2022,7 +2016,13 @@ void GLGizmoTextureDisplacement::queue_preview_job()
// 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 filaments = m_palette_filaments;
+ // Every extruder, not the palette's physical-only list: the bake writes the filament it resolved
+ // to, and a mix resolves to a *mixed filament slot*, which is an extruder past the physical ones.
+ // Grouping against the shorter list dropped every triangle carrying such a slot out of the mesh
+ // entirely - the relief vanished and left only the few triangles that happened to print in a plain
+ // filament. The palette still has to be built from physical filaments alone (see filament_palette()),
+ // which is why these two are not the same list.
+ const std::vector filaments = wxGetApp().plater()->get_extruders_colors();
m_preview_job_running = true;
auto &worker = wxGetApp().plater()->get_ui_job_worker();
@@ -4219,19 +4219,44 @@ bool GLGizmoTextureDisplacement::any_layer_colors(const ModelVolume &mv)
return false;
}
+void GLGizmoTextureDisplacement::bind_mixes_to_filament_slots(std::vector &palette)
+{
+ Sidebar *sidebar = &wxGetApp().plater()->sidebar();
+ if (sidebar == nullptr)
+ return;
+ for (PaletteEntry &e : palette) {
+ if (!e.is_mix())
+ continue;
+ // Components are 1-based in the config; the ratios are percentages summing to 100, which is the
+ // form create_mixed_filament_from_result() normalises from.
+ const int a_pct = int(std::lround(100.0 * double(e.num) / double(e.den)));
+ const int slot = sidebar->ensure_mixed_filament({ unsigned(e.a + 1), unsigned(e.b + 1) },
+ { a_pct, 100 - a_pct });
+ if (slot >= 0) {
+ e.a = e.b = slot;
+ e.num = e.den = 1;
+ } else {
+ // No room for another slot. Collapse to the component that dominates the blend, which is what
+ // the old per-triangle path did on a surface it could not band anyway.
+ const int dominant = (e.num * 2 >= e.den) ? e.a : e.b;
+ e.a = e.b = dominant;
+ e.num = e.den = 1;
+ }
+ }
+}
+
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.palette_pure = make_palette(m_palette_filaments, /* mixing */ false);
- out.mix_mode = mv.texture_displacement_options.color_mix_mode;
+ out.palette_pure = make_palette(m_palette_filaments, /* mixing */ false, PALETTE_MAX_ENTRIES);
+ // Done here rather than in cached_palette(): this runs when a preview or a bake is queued, off a
+ // user action, while that one is also touched from the render path - and creating filament slots
+ // there would mutate the project mid-frame.
+ bind_mixes_to_filament_slots(out.palette);
out.despeckle_passes = mv.texture_displacement_options.color_despeckle;
- out.layer_height = color_band_mm(mv);
- // 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;
}
@@ -4244,10 +4269,15 @@ const std::vector &GLGizmoTextureDispl
const ModelVolume *mv = texture_volume();
const bool mixing = mv != nullptr && mv->texture_displacement_options.color_mix_enabled;
std::vector filaments = filament_palette();
- if (m_palette_cache.empty() || filaments != m_palette_filaments || mixing != m_palette_mixing) {
+ // Every mix costs a filament slot once they are bound to one, and the mask can name only so many
+ // states, so the palette has to leave room beside the physical filaments it already counts.
+ const int cap = int(EnforcerBlockerType::ExtruderMax);
+ if (m_palette_cache.empty() || filaments != m_palette_filaments || mixing != m_palette_mixing ||
+ cap != m_palette_cap) {
m_palette_filaments = std::move(filaments);
m_palette_mixing = mixing;
- m_palette_cache = make_palette(m_palette_filaments, mixing);
+ m_palette_cap = cap;
+ m_palette_cache = make_palette(m_palette_filaments, mixing, cap);
m_palette_quantizer = make_palette_quantizer(m_palette_cache);
}
return m_palette_cache;
@@ -4255,40 +4285,29 @@ const std::vector &GLGizmoTextureDispl
std::vector GLGizmoTextureDisplacement::filament_palette()
{
- std::vector palette = wxGetApp().plater()->get_extruders_colors();
- // mmu_segmentation_facets encodes the filament in a 6-bit prefix code and stops at Extruder16.
+ std::vector all = wxGetApp().plater()->get_extruders_colors();
+
+ // Physical filaments only. The mixes this palette produces each become a mixed filament slot of
+ // their own (see bind_mixes_to_filament_slots()), and those slots are extruders too - so taking the
+ // list as it comes meant the next rebuild mixed *them* again, and handed components naming a
+ // virtual slot to a blend that can only name physical ones. That is what left entries reading
+ // "filament 1 plus nothing" and raised "Mixed filament has invalid or mismatched components".
+ const auto *is_mixed = wxGetApp().preset_bundle->project_config.option("filament_is_mixed");
+ std::vector palette;
+ palette.reserve(all.size());
+ for (size_t i = 0; i < all.size(); ++i)
+ if (is_mixed == nullptr || i >= is_mixed->values.size() || !is_mixed->values[i])
+ palette.push_back(all[i]);
+
+ // A paint mask can only name so many states, and every mix spends one beside these.
if (palette.size() > size_t(EnforcerBlockerType::ExtruderMax))
palette.resize(size_t(EnforcerBlockerType::ExtruderMax));
return palette;
}
-float GLGizmoTextureDisplacement::color_band_mm(const ModelVolume &mv)
-{
- const float lh = print_layer_height();
- const float edge = (mv.texture_displacement_options.v2_refine_mm > 0.f) ? mv.texture_displacement_options.v2_refine_mm
- : v2_recommendation(mv).edge_mm;
- if (edge <= 0.f || lh <= 0.f)
- return lh;
- // A refined triangle of edge e stacks in rows about 0.87 * e apart (an equilateral triangle's
- // height), and a dither needs at least two rows per period to be a dither at all.
- constexpr float ROW_PER_EDGE = 0.87f;
- return lh * std::max(1.f, std::ceil(2.f * ROW_PER_EDGE * edge / lh));
-}
-
-float GLGizmoTextureDisplacement::print_layer_height()
-{
- try {
- const DynamicPrintConfig &cfg = wxGetApp().preset_bundle->prints.get_edited_preset().config;
- if (const ConfigOptionFloat *opt = cfg.option("layer_height"); opt != nullptr)
- if (opt->value > 1e-3)
- return float(opt->value);
- } catch (...) {
- }
- return 0.2f;
-}
std::vector GLGizmoTextureDisplacement::make_palette(
- const std::vector &filaments, bool mixing)
+ const std::vector &filaments, bool mixing, int max_entries)
{
std::vector out;
const int n = int(filaments.size());
@@ -4305,7 +4324,7 @@ std::vector GLGizmoTextureDisplacement
const int pairs = n * (n - 1) / 2;
int steps = 0;
for (int s = 5; s >= 1; --s)
- if (n + pairs * s <= PALETTE_MAX_ENTRIES) {
+ if (n + pairs * s <= max_entries) {
steps = s;
break;
}
@@ -4327,51 +4346,6 @@ std::vector GLGizmoTextureDisplacement
return out;
}
-ColorResolveFn GLGizmoTextureDisplacement::make_mix_resolver(const std::vector &palette,
- ColorMixMode mode, float layer_height,
- float cell_mm)
-{
- if (palette.empty())
- return nullptr;
- auto entries = std::make_shared>(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, const Vec3f &normal) -> 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.
- // Auto: bands wherever the surface is steeper than ~45 degrees - consecutive layers alternate
- // there, which is how a blend prints and reads. On a flat-facing surface a layer is one band
- // and the only way to interleave is a checkerboard across the surface, which at print scale
- // reads as a pattern rather than a colour; there the mix falls back to its dominant filament.
- const bool upright = std::abs(normal.z()) < 0.7f;
- if (mode == ColorMixMode::Auto && !upright)
- return e.num * 2 >= e.den ? e.a : e.b;
- const bool bands = mode == ColorMixMode::ZBands || mode == ColorMixMode::Auto;
- if (bands) {
- // 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 &palette)
{
if (palette.empty())
@@ -4408,10 +4382,15 @@ ColorQuantizeFn GLGizmoTextureDisplacement::make_palette_quantizer(const std::ve
best_pure = int(i);
}
}
- // A mix is an interleave that only reads as its colour from a distance; up close
- // it is stripes. Spend it only where it buys a clearly better match than the nearest
- // single filament: ten Delta E is a visible step, less is not worth the stripes.
- constexpr float PREFER_PURE_DE = 10.f;
+ // A mix is an interleave that only reads as its colour from a distance; up close it is
+ // stripes. So it is spent only where it buys a better match than the nearest single
+ // filament - but "better" was set at ten Delta E, which is not a visible step, it is a
+ // different colour. Measured over the whole cube that threshold turned 94% of the
+ // lookups that wanted a mix back into a pure filament, leaving 38%; along a greyscale
+ // ramp, the shape a height texture actually traces, it cut 80% to 66%. Two Delta E is
+ // about where a side-by-side difference stops being arguable, which is the right place
+ // to start paying for stripes.
+ constexpr float PREFER_PURE_DE = 2.f;
if (best_pure >= 0 && palette[size_t(best)].is_mix() && best_pure_d - best_d < PREFER_PURE_DE)
best = best_pure;
(*lut)[(size_t(r) * E + size_t(g)) * E + size_t(b)] = uint8_t(best);
@@ -4511,7 +4490,7 @@ TextureDisplacementPrepareResult GLGizmoTextureDisplacement::prepare_mesh(
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).
+ // colour, never the interleaving that realises a mix - the slicer does that per layer.
// "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.
@@ -6069,24 +6048,6 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
"of filaments can cover a photo or a gradient. An image of flat colours "
"prints the same either way. Off uses one filament per area."));
if (opts.color_mix_enabled) {
- slider_label(_L("Mix by"));
- const std::string mix_z = _u8L("Layers");
- const std::string mix_xy = _u8L("Surface");
- const std::string mix_auto = _u8L("Automatic");
- const char *mix_items[] = { mix_z.c_str(), mix_xy.c_str(), mix_auto.c_str() };
- int mix_mode = int(opts.color_mix_mode);
- ImGui::SetNextItemWidth(-card_pad);
- 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;
- }
- hover_tip(_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.\n"
- "Automatic: layers on upright faces; flat-facing faces take the nearer "
- "single filament, since a checkerboard there shows as a pattern."));
ImGui::TextDisabled("%s", Slic3r::format(_u8L("%1% printable colours from %2% filaments"),
int(cached_palette().size()), int(m_palette_filaments.size())).c_str());
}
diff --git a/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp b/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp
index 6685f8d713..c368107a53 100644
--- a/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp
+++ b/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp
@@ -81,7 +81,11 @@ public:
// 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 make_palette(const std::vector &filaments, bool mixing);
+ // `max_entries` bounds the whole palette. It is normally the quantizer's own limit, but when the
+ // mixes become filament slots it has to be the paint mask's instead: a mask can name only
+ // EnforcerBlockerType::ExtruderMax states, and every mix now occupies one of them.
+ static std::vector make_palette(const std::vector &filaments, bool mixing,
+ int max_entries);
// 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
@@ -95,9 +99,6 @@ public:
// 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 &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.
@@ -106,16 +107,20 @@ public:
// 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 &cached_palette();
+ // Turns every mix in `palette` into a mixed filament slot and rewrites the entry to name that slot
+ // as a plain filament, so nothing downstream has to know a mix is involved: is_mix() goes false and
+ // the resolver simply returns it. The per-layer interleaving then happens in the slicer, where it is
+ // not limited by how fine the mesh is. Entries whose slot could not be created (the paint-state cap)
+ // fall back to the nearer of the two components.
+ void bind_mixes_to_filament_slots(std::vector &palette);
std::vector m_palette_cache;
std::vector m_palette_filaments;
+ int m_palette_cap = 0; // the max_entries m_palette_cache was built with
bool m_palette_mixing = false;
ColorQuantizeFn m_palette_quantizer;
// The loaded filaments, clamped to the sixteen mmu_segmentation_facets can address.
static std::vector 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 Z band height, in mm. One print layer is the ideal, but the interleave is realised per
// *facet*: a band thinner than the mesh can resolve does not dither, it beats against the triangle
// grid and comes out as broad horizontal stripes - and since MMU segmentation reads facet colour,
@@ -123,7 +128,6 @@ public:
// diagonal and knows nothing about the layer height, so the band is rounded up to a whole number of
// layers at least two facet rows tall: still exact on the printer, and representable by the mesh
// that has to carry it. Used by both the bake settings and the preview shader, so the two agree.
- float color_band_mm(const ModelVolume &mv);
// 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
diff --git a/src/slic3r/GUI/Jobs/TextureDisplacementBakeJob.cpp b/src/slic3r/GUI/Jobs/TextureDisplacementBakeJob.cpp
index 03b4b14fc1..8cf780bf50 100644
--- a/src/slic3r/GUI/Jobs/TextureDisplacementBakeJob.cpp
+++ b/src/slic3r/GUI/Jobs/TextureDisplacementBakeJob.cpp
@@ -51,9 +51,6 @@ void TextureDisplacementBakeJob::process(Ctl &ctl)
color_request.quantize = GLGizmoTextureDisplacement::make_palette_quantizer(m_input.color.palette);
if (!m_input.color.palette_pure.empty())
color_request.quantize_pure = GLGizmoTextureDisplacement::make_palette_quantizer(m_input.color.palette_pure);
- 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)
diff --git a/src/slic3r/GUI/Jobs/TextureDisplacementPreviewJob.cpp b/src/slic3r/GUI/Jobs/TextureDisplacementPreviewJob.cpp
index e8c7dfca01..82ba357381 100644
--- a/src/slic3r/GUI/Jobs/TextureDisplacementPreviewJob.cpp
+++ b/src/slic3r/GUI/Jobs/TextureDisplacementPreviewJob.cpp
@@ -33,9 +33,6 @@ void TextureDisplacementPreviewJob::process(Ctl &ctl)
color_request.quantize = GLGizmoTextureDisplacement::make_palette_quantizer(m_input.color.palette);
if (!m_input.color.palette_pure.empty())
color_request.quantize_pure = GLGizmoTextureDisplacement::make_palette_quantizer(m_input.color.palette_pure);
- 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)
diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp
index a069e89609..b70e9ec348 100644
--- a/src/slic3r/GUI/Plater.cpp
+++ b/src/slic3r/GUI/Plater.cpp
@@ -938,6 +938,7 @@ struct Sidebar::priv
wxStaticText* m_text_mixed_title{nullptr};
ScalableButton* m_btn_mixed_add{nullptr};
ScalableButton* m_btn_mixed_del{nullptr};
+ ScalableButton* m_btn_mixed_del_all{nullptr};
wxScrolledWindow* m_mixed_scroll_area{nullptr}; // independent scrollbar for mixed rows
wxPanel* m_panel_mixed_content{nullptr};
wxBoxSizer* m_sizer_mixed_filaments{nullptr}; // two-column, mirrors sizer_filaments
@@ -3451,6 +3452,11 @@ Sidebar::Sidebar(Plater *parent)
});
title_sizer->Add(p->m_btn_mixed_del, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(SidebarProps::IconSpacing()));
+ p->m_btn_mixed_del_all = new ScalableButton(p->m_panel_mixed_title, wxID_ANY, "delete_all_filaments");
+ p->m_btn_mixed_del_all->SetToolTip(_L("Remove all mixed filaments"));
+ p->m_btn_mixed_del_all->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { remove_all_mixed_filaments(); });
+ title_sizer->Add(p->m_btn_mixed_del_all, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(SidebarProps::IconSpacing()));
+
p->m_btn_mixed_add = new ScalableButton(p->m_panel_mixed_title, wxID_ANY, "add_filament");
p->m_btn_mixed_add->SetToolTip(_L("Add mixed filament"));
p->m_btn_mixed_add->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { add_mixed_filament(); });
@@ -5120,6 +5126,58 @@ static bool create_mixed_filament_from_result(
return true;
}
+int Sidebar::ensure_mixed_filament(const std::vector &components, const std::vector &ratios)
+{
+ if (components.size() < 2 || components.size() != ratios.size())
+ return -1;
+ if (p->combos_filament.size() < 2)
+ return -1;
+
+ // Normalise the way create_mixed_filament_from_result() stores them, so the comparison below sees
+ // the same text the config holds rather than two spellings of one blend.
+ int ratio_sum = 0;
+ for (const int r : ratios)
+ ratio_sum += r;
+ if (ratio_sum <= 0)
+ return -1;
+
+ std::string comp_str, ratio_str;
+ {
+ CNumericLocalesSetter c_locale_setter;
+ for (size_t i = 0; i < components.size(); ++i) {
+ if (i > 0) { comp_str += ","; ratio_str += ","; }
+ comp_str += std::to_string(components[i]);
+ char buf[32];
+ std::snprintf(buf, sizeof(buf), "%.4f", float(ratios[i]) / float(ratio_sum));
+ ratio_str += buf;
+ }
+ }
+
+ const auto &project_config = wxGetApp().preset_bundle->project_config;
+ const auto *is_mixed_opt = project_config.option("filament_is_mixed");
+ const auto *comp_opt = project_config.option("filament_mixed_components");
+ const auto *ratios_opt = project_config.option("filament_mixed_sublayer_ratios");
+ if (is_mixed_opt != nullptr && comp_opt != nullptr && ratios_opt != nullptr)
+ for (size_t i = 0; i < is_mixed_opt->values.size(); ++i)
+ if (is_mixed_opt->values[i] && i < comp_opt->values.size() && i < ratios_opt->values.size() &&
+ comp_opt->values[i] == comp_str && ratios_opt->values[i] == ratio_str)
+ return int(i);
+
+ if (wxGetApp().preset_bundle->filament_presets.size() >= size_t(EnforcerBlockerType::ExtruderMax))
+ return -1;
+
+ std::vector color_strs, names, types;
+ collect_physical_filament_info(color_strs, names, types);
+
+ MixedFilamentResult result;
+ result.components = components;
+ result.ratios = ratios;
+ const size_t created_at = wxGetApp().preset_bundle->filament_presets.size();
+ if (!create_mixed_filament_from_result(this, result, color_strs))
+ return -1;
+ return int(created_at);
+}
+
void Sidebar::add_mixed_filament()
{
auto* plater = dynamic_cast(GetParent());
@@ -5297,6 +5355,28 @@ void Sidebar::edit_mixed_filament(size_t panel_idx)
}
}
+void Sidebar::remove_all_mixed_filaments()
+{
+ auto *plater = dynamic_cast(GetParent());
+ if (plater == nullptr)
+ return;
+ const size_t count = plater->mixed_filament_config_indices().size();
+ if (count == 0)
+ return;
+
+ // Worth a confirmation: this drops filament slots the model may be painted with, and anything
+ // painted in one falls back to a plain filament.
+ MessageDialog dlg(this, format_wxstr(_L("Remove all %1% mixed filaments?"), count), _L("Mixed Filament"),
+ wxYES_NO | wxNO_DEFAULT | wxICON_QUESTION);
+ if (dlg.ShowModal() != wxID_YES)
+ return;
+
+ // Back to front: delete_mixed_filament_at() indexes the list as it stands, so removing from the end
+ // leaves the indices of everything still to go untouched.
+ for (size_t i = count; i-- > 0;)
+ delete_mixed_filament_at(i);
+}
+
void Sidebar::delete_mixed_filament_at(size_t panel_idx)
{
auto* plater = dynamic_cast(GetParent());
diff --git a/src/slic3r/GUI/Plater.hpp b/src/slic3r/GUI/Plater.hpp
index 21fda05246..4f7c30ad2a 100644
--- a/src/slic3r/GUI/Plater.hpp
+++ b/src/slic3r/GUI/Plater.hpp
@@ -291,8 +291,19 @@ public:
// Mixed-color filament sidebar section
void add_mixed_filament();
+ // The filament slot that blends `components` (1-based physical filament indices) in `ratios`
+ // (percentages), creating it when no existing mixed slot already describes that blend. Returns the
+ // 0-based filament index, or -1 when the paint-state cap leaves no room for another one.
+ //
+ // Exists so a feature that needs a blend can ask for one without going through the modal dialog:
+ // the texture displacement gizmo turns each mix in its palette into a slot, which is what moves the
+ // interleaving from its own paint mask to the slicer, where it happens per layer.
+ int ensure_mixed_filament(const std::vector &components, const std::vector &ratios);
void edit_mixed_filament(size_t idx);
void delete_mixed_filament_at(size_t idx);
+ // Drops every mixed filament at once, after confirming. The texture displacement gizmo can create
+ // one slot per colour in its palette, so clearing them one at a time is tedious.
+ void remove_all_mixed_filaments();
void decompose_filament_color(int filament_idx);
void recalc_filament_scroll_sizes();
void update_mixed_filament_list();