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