mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-12 18:31:18 +00:00
feat: a colour count instead of Two colours, and a pickable background
Two colours was the N=2 case of a more useful control, so it is replaced by a Colors slider: 0 for off, then 2 to 12. The picture is reduced to that many colours before the filaments are matched to it, so it prints in at most that many. Reduced by k-means in RGB rather than by posterizing the range, because the useful question is which colours the picture is actually made of. A tartan asks for its own handful of greens and reds; an even split of the range would hand it eight shades of mud. The clustering runs over a coarse 16-per-channel histogram, 4096 weighted points instead of a million pixels, which is what makes it cheap enough to sit on a slider, and it is seeded the k-means++ way so a picture with one dominant colour does not get every centre inside it. This is not the displacement tool's "Mixed colors", which caps how many mixed filament slots a bake may create. Nothing like it existed. Background removal can now be told which colour to remove. The four corners stay the default, because a background that does not reach all four of them is not a background and that makes the common case one click, but the panel shows the colour as a swatch that is also a picker, and a dropper beside it takes the colour from the preview. The dropper reads the decoded image rather than the screen: the preview is scaled, and it already has the corrections applied, so a screen read would key the corrected colour against itself. "Corners" puts it back. Measured on Tartan, a cube face, four filaments: run out tris coloured filaments filament shares image, no correction 22534 14544 4 1:91% 3:8% 4:1% image, 2 colors 18232 10268 2 1:66% 3:34% image, 3 colors 24458 16459 3 1:78% 3:18% 4:5% image, 5 colors 23972 15978 4 1:90% 3:9% 4:1% Asking for 2 gives 2 and asking for 3 gives 3. Asking for 5 gives 4, which is the contract working the right way round: only four filaments are loaded, and "at most" is the guarantee. No run moves the surface.
This commit is contained in:
@@ -305,42 +305,140 @@ void sharpen_rgb(DecodedHeightTexture &tex, float amount)
|
||||
}
|
||||
}
|
||||
|
||||
// Two levels, on luma. For line art and logos, where the useful result is exactly two filaments and
|
||||
// anything in between is speckle.
|
||||
void apply_threshold(DecodedHeightTexture &tex, const TextureDisplacementLayer &layer)
|
||||
// Reduces the picture to at most `count` colours, by k-means in RGB.
|
||||
//
|
||||
// "At most": two of the chosen colours can still match the same filament, in which case the print
|
||||
// uses fewer. The guarantee runs the right way round, which is the one that matters - asking for
|
||||
// three colours cannot produce four.
|
||||
//
|
||||
// k-means rather than a fixed palette or an even split of the luma range, because the useful
|
||||
// question is which colours this picture is actually made of. A tartan asks for its own handful of
|
||||
// greens and reds; posterizing the range would hand it eight shades of mud instead. The clustering
|
||||
// runs over a coarse histogram, not over the pixels: at 1024 x 1024 that is 4096 weighted points
|
||||
// instead of a million, which is what makes it cheap enough to sit on a slider.
|
||||
void reduce_colors(DecodedHeightTexture &tex, int count)
|
||||
{
|
||||
if (!tex.has_color() || !layer.threshold_enabled)
|
||||
if (!tex.has_color() || count < 2 || count > 64)
|
||||
return;
|
||||
const float cut = std::clamp(layer.threshold, 0.f, 1.f) * 255.f;
|
||||
const size_t n = size_t(tex.width) * size_t(tex.height);
|
||||
const size_t n = size_t(tex.width) * size_t(tex.height);
|
||||
|
||||
// 16 levels per channel, each bin keeping the mean of what fell in it, so a colour is not
|
||||
// snapped to a bin corner before the clustering has had a say.
|
||||
struct Bin { double r = 0, g = 0, b = 0; uint32_t n = 0; };
|
||||
std::vector<Bin> bins(size_t(16 * 16 * 16));
|
||||
for (size_t i = 0; i < n; ++i) {
|
||||
const float luma = 0.2126f * float(tex.rgb[i * 3]) + 0.7152f * float(tex.rgb[i * 3 + 1]) +
|
||||
0.0722f * float(tex.rgb[i * 3 + 2]);
|
||||
const uint8_t v = luma >= cut ? 255 : 0;
|
||||
tex.rgb[i * 3] = tex.rgb[i * 3 + 1] = tex.rgb[i * 3 + 2] = v;
|
||||
const uint8_t r = tex.rgb[i * 3], g = tex.rgb[i * 3 + 1], b = tex.rgb[i * 3 + 2];
|
||||
Bin &bin = bins[(size_t(r >> 4) << 8) | (size_t(g >> 4) << 4) | size_t(b >> 4)];
|
||||
bin.r += r; bin.g += g; bin.b += b; ++bin.n;
|
||||
}
|
||||
struct Point { float c[3]; double w; };
|
||||
std::vector<Point> pts;
|
||||
pts.reserve(bins.size());
|
||||
for (const Bin &bin : bins)
|
||||
if (bin.n > 0)
|
||||
pts.push_back({ { float(bin.r / bin.n), float(bin.g / bin.n), float(bin.b / bin.n) }, double(bin.n) });
|
||||
if (pts.size() <= size_t(count))
|
||||
return; // already no more colours than asked for
|
||||
|
||||
// Seeded the k-means++ way, deterministically: the heaviest point first, then each next one is
|
||||
// whichever point is furthest from everything chosen so far, weighted by how much of the image
|
||||
// it is. Picking the N fullest bins instead would put every centre inside the one dominant
|
||||
// colour of a picture that has one.
|
||||
std::vector<std::array<float, 3>> centre;
|
||||
{
|
||||
size_t best = 0;
|
||||
for (size_t i = 1; i < pts.size(); ++i)
|
||||
if (pts[i].w > pts[best].w)
|
||||
best = i;
|
||||
centre.push_back({ pts[best].c[0], pts[best].c[1], pts[best].c[2] });
|
||||
std::vector<double> d2(pts.size(), std::numeric_limits<double>::max());
|
||||
while (int(centre.size()) < count) {
|
||||
const auto &last = centre.back();
|
||||
double top = -1.0;
|
||||
size_t pick = 0;
|
||||
for (size_t i = 0; i < pts.size(); ++i) {
|
||||
const double dr = pts[i].c[0] - last[0], dg = pts[i].c[1] - last[1], db = pts[i].c[2] - last[2];
|
||||
d2[i] = std::min(d2[i], dr * dr + dg * dg + db * db);
|
||||
if (const double score = d2[i] * pts[i].w; score > top) {
|
||||
top = score;
|
||||
pick = i;
|
||||
}
|
||||
}
|
||||
if (top <= 0.0)
|
||||
break; // fewer distinct colours than asked for
|
||||
centre.push_back({ pts[pick].c[0], pts[pick].c[1], pts[pick].c[2] });
|
||||
}
|
||||
}
|
||||
|
||||
// Lloyd's algorithm over the weighted bins. Twelve passes: on a histogram this coarse it has
|
||||
// stopped moving well before that, and a fixed count keeps the slider's cost predictable.
|
||||
const int k = int(centre.size());
|
||||
for (int iter = 0; iter < 12; ++iter) {
|
||||
std::vector<std::array<double, 3>> sum(size_t(k), { 0.0, 0.0, 0.0 });
|
||||
std::vector<double> wsum(size_t(k), 0.0);
|
||||
for (const Point &p : pts) {
|
||||
int best = 0;
|
||||
double bd = std::numeric_limits<double>::max();
|
||||
for (int c = 0; c < k; ++c) {
|
||||
const double dr = p.c[0] - centre[size_t(c)][0], dg = p.c[1] - centre[size_t(c)][1],
|
||||
db = p.c[2] - centre[size_t(c)][2];
|
||||
if (const double d = dr * dr + dg * dg + db * db; d < bd) { bd = d; best = c; }
|
||||
}
|
||||
for (int ch = 0; ch < 3; ++ch)
|
||||
sum[size_t(best)][size_t(ch)] += double(p.c[ch]) * p.w;
|
||||
wsum[size_t(best)] += p.w;
|
||||
}
|
||||
for (int c = 0; c < k; ++c)
|
||||
if (wsum[size_t(c)] > 0.0)
|
||||
for (int ch = 0; ch < 3; ++ch)
|
||||
centre[size_t(c)][size_t(ch)] = float(sum[size_t(c)][size_t(ch)] / wsum[size_t(c)]);
|
||||
}
|
||||
|
||||
// Every texel takes its nearest centre. Parallel by rows, as the tonal pass is.
|
||||
const size_t rows = size_t(tex.height), cols = size_t(tex.width);
|
||||
tbb::parallel_for(tbb::blocked_range<size_t>(0, rows), [&](const tbb::blocked_range<size_t> &range) {
|
||||
for (size_t y = range.begin(); y < range.end(); ++y)
|
||||
for (size_t x = 0; x < cols; ++x) {
|
||||
uint8_t *px = tex.rgb.data() + (y * cols + x) * 3;
|
||||
int best = 0;
|
||||
float bd = std::numeric_limits<float>::max();
|
||||
for (int c = 0; c < k; ++c) {
|
||||
const float dr = float(px[0]) - centre[size_t(c)][0], dg = float(px[1]) - centre[size_t(c)][1],
|
||||
db = float(px[2]) - centre[size_t(c)][2];
|
||||
if (const float d = dr * dr + dg * dg + db * db; d < bd) { bd = d; best = c; }
|
||||
}
|
||||
for (int ch = 0; ch < 3; ++ch)
|
||||
px[size_t(ch)] = uint8_t(std::lround(std::clamp(centre[size_t(best)][size_t(ch)], 0.f, 255.f)));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Keys out the colour the picture's corners share, so a logo on a white card prints as the logo and
|
||||
// the object's own colour shows through the rest.
|
||||
// Keys one colour out, so a logo on a white card prints as the logo and the object's own colour
|
||||
// shows through the rest.
|
||||
//
|
||||
// The corners rather than a colour the user picks: a background that does not reach all four of them
|
||||
// is not a background, and asking for an eyedropper would be a second interaction for the case this
|
||||
// exists to make one click. The median of the four is used so a single stray corner cannot decide it.
|
||||
// The corners are the default because a background that does not reach all four of them is not a
|
||||
// background, and that makes the common case one click; the median of the four is taken so a single
|
||||
// stray corner cannot decide it. The panel's dropper and swatch override it when the picture needs
|
||||
// something else keyed out.
|
||||
void remove_background(DecodedHeightTexture &tex, const TextureDisplacementLayer &layer)
|
||||
{
|
||||
if (!tex.has_color() || !layer.bg_remove || tex.width < 2 || tex.height < 2)
|
||||
return;
|
||||
const size_t n = size_t(tex.width) * size_t(tex.height);
|
||||
const size_t corner[4] = { 0, size_t(tex.width) - 1, (size_t(tex.height) - 1) * size_t(tex.width),
|
||||
n - 1 };
|
||||
float key[3];
|
||||
for (int c = 0; c < 3; ++c) {
|
||||
std::array<float, 4> v{};
|
||||
for (int i = 0; i < 4; ++i)
|
||||
v[size_t(i)] = float(tex.rgb[corner[i] * 3 + size_t(c)]);
|
||||
std::sort(v.begin(), v.end());
|
||||
key[c] = 0.5f * (v[1] + v[2]);
|
||||
if (layer.bg_from_corners) {
|
||||
const size_t corner[4] = { 0, size_t(tex.width) - 1, (size_t(tex.height) - 1) * size_t(tex.width),
|
||||
n - 1 };
|
||||
for (int c = 0; c < 3; ++c) {
|
||||
std::array<float, 4> v{};
|
||||
for (int i = 0; i < 4; ++i)
|
||||
v[size_t(i)] = float(tex.rgb[corner[i] * 3 + size_t(c)]);
|
||||
std::sort(v.begin(), v.end());
|
||||
key[c] = 0.5f * (v[1] + v[2]);
|
||||
}
|
||||
} else {
|
||||
for (int c = 0; c < 3; ++c)
|
||||
key[c] = std::clamp(layer.bg_color[c], 0.f, 1.f) * 255.f;
|
||||
}
|
||||
// Euclidean in 0..255 RGB, against a tolerance scaled to the diagonal of that cube, so the
|
||||
// slider reads as "how different from the background still counts as background".
|
||||
@@ -361,7 +459,7 @@ bool has_color_adjustments(const TextureDisplacementLayer &layer)
|
||||
{
|
||||
return layer.brightness != 0.f || layer.contrast != 0.f || layer.saturation != 1.f ||
|
||||
layer.gamma != 1.f || layer.color_invert || layer.hue != 0.f || layer.sharpness > 0.f ||
|
||||
layer.threshold_enabled || layer.bg_remove;
|
||||
layer.color_count >= 2 || layer.bg_remove;
|
||||
}
|
||||
|
||||
void apply_color_adjustments(DecodedHeightTexture &tex, const TextureDisplacementLayer &layer)
|
||||
@@ -439,7 +537,7 @@ void apply_color_adjustments(DecodedHeightTexture &tex, const TextureDisplacemen
|
||||
// thresholded image would only ring along the one edge it has left, and the key has to read the
|
||||
// colours the user is actually looking at.
|
||||
sharpen_rgb(tex, layer.sharpness);
|
||||
apply_threshold(tex, layer);
|
||||
reduce_colors(tex, layer.color_count);
|
||||
remove_background(tex, layer);
|
||||
}
|
||||
|
||||
|
||||
@@ -301,15 +301,20 @@ struct TextureDisplacementLayer
|
||||
float contrast = 0.f; // -1 .. 1, pivoted on mid grey; -1 flattens to grey, 1 doubles the slope
|
||||
float hue = 0.f; // -180 .. 180 degrees, rotated about the grey axis at constant luma
|
||||
float sharpness = 0.f; // 0 .. 1 unsharp mask amount, 0 is off
|
||||
// Line art and logos: everything lighter than `threshold` becomes white and the rest black, so the
|
||||
// picture matches exactly two filaments instead of speckling across several.
|
||||
bool threshold_enabled = false;
|
||||
float threshold = 0.5f; // 0 .. 1, on Rec. 709 luma
|
||||
// How many colours the picture is allowed to use, 0 for as many as it likes. Reduced by k-means
|
||||
// in RGB before the filaments are matched, so the result uses at most this many of them: 2 for a
|
||||
// logo that should be two flat colours, 4 or 5 for a poster, off for a photograph.
|
||||
int color_count = 0; // 0, or 2 .. 12
|
||||
// Leaves the background of a picture unprinted, so the object's own colour shows through instead
|
||||
// of a slab of white behind a logo. Texels the source image marked transparent always count;
|
||||
// `bg_remove` additionally keys out whatever colour the picture's corners share.
|
||||
// `bg_remove` additionally keys out one colour.
|
||||
bool bg_remove = false;
|
||||
float bg_tolerance = 0.15f; // 0 .. 1, distance from the keyed colour that still counts as background
|
||||
// The colour keyed out. Taken from the median of the picture's four corners while
|
||||
// `bg_from_corners` is set, which is right for nearly every logo on a flat card; the panel's
|
||||
// dropper and swatch clear it and put a colour of the user's own here.
|
||||
bool bg_from_corners = true;
|
||||
float bg_color[3] = { 1.f, 1.f, 1.f };
|
||||
float saturation = 1.f; // 0 .. 2, 1 leaves it alone, 0 is grayscale
|
||||
float gamma = 1.f; // 0.2 .. 5, 1 leaves it alone; above 1 lifts the midtones
|
||||
bool color_invert = false; // negative of the colour, as opposed to `invert` which flips relief
|
||||
@@ -324,8 +329,8 @@ struct TextureDisplacementLayer
|
||||
static_cast<int>(blend_mode), midlevel, island_padding_mm, lscm_seam_edges, view_project_right,
|
||||
view_project_up, smoothing, edge_smoothing, edge_smoothing_amount, auto_connect_islands, island_groups,
|
||||
lscm_uv_overrides, view_project_projective, view_project_matrix, color_enabled, brightness,
|
||||
contrast, saturation, gamma, color_invert, hue, sharpness, threshold_enabled, threshold,
|
||||
bg_remove, bg_tolerance);
|
||||
contrast, saturation, gamma, color_invert, hue, sharpness, color_count, bg_remove,
|
||||
bg_tolerance, bg_from_corners, bg_color[0], bg_color[1], bg_color[2]);
|
||||
}
|
||||
template<class Archive> void load(Archive &ar)
|
||||
{
|
||||
@@ -338,7 +343,7 @@ struct TextureDisplacementLayer
|
||||
island_padding_mm, lscm_seam_edges, view_project_right, view_project_up, smoothing, edge_smoothing,
|
||||
edge_smoothing_amount, auto_connect_islands, island_groups, lscm_uv_overrides, view_project_projective,
|
||||
view_project_matrix, color_enabled, brightness, contrast, saturation, gamma, color_invert, hue, sharpness,
|
||||
threshold_enabled, threshold, bg_remove, bg_tolerance);
|
||||
color_count, bg_remove, bg_tolerance, bg_from_corners, bg_color[0], bg_color[1], bg_color[2]);
|
||||
image_data = blob.empty() ? nullptr : std::make_shared<std::vector<unsigned char>>(blob.begin(), blob.end());
|
||||
tile_method = static_cast<TextureTileMethod>(tile_method_int);
|
||||
projection_method = static_cast<TextureProjectionMethod>(projection_method_int);
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
#include "slic3r/GUI/Jobs/Worker.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <imgui.h>
|
||||
#include <optional>
|
||||
@@ -259,6 +260,17 @@ void GLGizmoTextureDisplacement::render_image_input_window(float x, float y, flo
|
||||
hover_tip(tip_text);
|
||||
return clicked;
|
||||
};
|
||||
// The integer twin of float_row, for the colour count.
|
||||
const auto int_row = [&](const char *id, const wxString &label, int *v, int v_min, int v_max,
|
||||
const char *format) -> bool {
|
||||
const float x0 = ImGui::GetCursorPosX();
|
||||
ImGui::AlignTextToFramePadding();
|
||||
m_imgui->text(label);
|
||||
ImGui::SameLine();
|
||||
ImGui::SetCursorPosX(std::max(ImGui::GetCursorPosX(), x0 + label_w));
|
||||
ImGui::SetNextItemWidth(x0 + panel_w - ImGui::GetCursorPosX());
|
||||
return ImGui::SliderInt(id, v, v_min, v_max, format, ImGuiSliderFlags_AlwaysClamp);
|
||||
};
|
||||
// Framed icon toggle, the displacement panel's one cut down to what this window needs.
|
||||
const auto icon_toggle = [&](int uid, const std::string &iconfile, bool active, const wxString &label,
|
||||
const wxString &tip_text) -> bool {
|
||||
@@ -356,14 +368,41 @@ void GLGizmoTextureDisplacement::render_image_input_window(float x, float y, flo
|
||||
const float aspect = std::max(0.05f, layer_texture_aspect(*layer));
|
||||
const float box_h = std::clamp(std::round(panel_w / aspect), std::round(panel_w * 0.35f),
|
||||
std::round(panel_w * 1.1f));
|
||||
const ImVec2 box_min = ImGui::GetCursorScreenPos();
|
||||
bool box_hit = false;
|
||||
if (shown != nullptr) {
|
||||
if (ImGui::ImageButton((ImTextureID) (intptr_t) shown->get_id(), ImVec2(panel_w, box_h),
|
||||
ImVec2(0.f, 0.f), ImVec2(1.f, 1.f), 0))
|
||||
box_hit = ImGui::ImageButton((ImTextureID) (intptr_t) shown->get_id(), ImVec2(panel_w, box_h),
|
||||
ImVec2(0.f, 0.f), ImVec2(1.f, 1.f), 0);
|
||||
} else {
|
||||
box_hit = ImGui::Button("?##thumb", ImVec2(panel_w, box_h));
|
||||
}
|
||||
hover_tip(m_image_bg_picking ?
|
||||
_u8L("Click the part of the picture that is its background.") :
|
||||
_u8L("Choose a different picture. Where you placed it and how it is corrected stay as "
|
||||
"they are."));
|
||||
if (box_hit) {
|
||||
if (!m_image_bg_picking) {
|
||||
open_library(layer->slot);
|
||||
} else if (ImGui::Button("?##thumb", ImVec2(panel_w, box_h))) {
|
||||
open_library(layer->slot);
|
||||
} else {
|
||||
// Read straight out of the decoded image rather than off the screen: the preview is
|
||||
// scaled, and it already has the corrections applied, so a screen read would key the
|
||||
// corrected colour against itself.
|
||||
const ImVec2 rel((ImGui::GetIO().MousePos.x - box_min.x) / panel_w,
|
||||
(ImGui::GetIO().MousePos.y - box_min.y) / box_h);
|
||||
const DecodedHeightTexture &tex = decode_height_texture(*layer);
|
||||
if (tex.has_color()) {
|
||||
const int px = std::clamp(int(rel.x * float(tex.width)), 0, tex.width - 1);
|
||||
const int py = std::clamp(int(rel.y * float(tex.height)), 0, tex.height - 1);
|
||||
const size_t i = (size_t(py) * size_t(tex.width) + size_t(px)) * 3;
|
||||
for (int c = 0; c < 3; ++c)
|
||||
layer->bg_color[c] = float(tex.rgb[i + size_t(c)]) / 255.f;
|
||||
layer->bg_from_corners = false;
|
||||
layer->bg_remove = true;
|
||||
m_preview_params_dirty = true;
|
||||
}
|
||||
m_image_bg_picking = false;
|
||||
}
|
||||
}
|
||||
hover_tip(_u8L("Choose a different picture. Where you placed it and how it is corrected stay as they are."));
|
||||
const std::string name = layer->name.empty() ? _u8L("Image") : layer->name;
|
||||
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + panel_w);
|
||||
ImGui::TextDisabled("%s", name.c_str());
|
||||
@@ -534,27 +573,79 @@ void GLGizmoTextureDisplacement::render_image_input_window(float x, float y, flo
|
||||
"than in a photo: a soft edge between two colors quantizes into a ragged band of "
|
||||
"both, and sharpening first makes it land in one place."));
|
||||
|
||||
// Two filaments, on purpose. The case this exists for is a logo or line art, where anything
|
||||
// in between the two reads as speckle rather than as shading.
|
||||
if (m_imgui->bbl_checkbox(_L("Two colors"), layer->threshold_enabled))
|
||||
m_preview_params_dirty = true;
|
||||
hover_tip(_u8L("Forces the picture to light and dark only, so it prints in exactly two filaments. "
|
||||
"For a logo or line art, where the in-between grays come out as speckle."));
|
||||
if (layer->threshold_enabled) {
|
||||
m_preview_params_dirty |= float_row("##threshold", _L("Split at"), &layer->threshold, 0.05f, 0.95f, "%.2f", false);
|
||||
hover_tip(_u8L("Where the split falls. Lower keeps more of the picture on the light side, higher "
|
||||
"on the dark one."));
|
||||
// How many colours the picture may use. 2 is a logo in two flat colours, 4 or 5 a poster,
|
||||
// off a photograph. This is not the same as the displacement tool's "Mixed colors", which
|
||||
// caps how many mixed filament slots a bake may create rather than what the picture uses.
|
||||
{
|
||||
int count = layer->color_count;
|
||||
if (int_row("##colors", _L("Colors"), &count, 0, 12, count == 0 ? _u8L("Off").c_str() : "%d")) {
|
||||
// 1 is not a picture, so the slider steps over it on the way out of Off.
|
||||
layer->color_count = count == 1 ? 2 : count;
|
||||
m_preview_params_dirty = true;
|
||||
}
|
||||
hover_tip(_u8L("Reduces the picture to this many colors before your filaments are matched to "
|
||||
"it, so it prints in at most that many. The colors are the ones the picture is "
|
||||
"actually made of, not an even split of the range. Off leaves it alone."));
|
||||
}
|
||||
|
||||
if (m_imgui->bbl_checkbox(_L("Remove background"), layer->bg_remove))
|
||||
m_preview_params_dirty = true;
|
||||
hover_tip(_u8L("Leaves the background unprinted, so the object's own color shows through instead of "
|
||||
"a slab of white behind a logo. The color is taken from the picture's four corners. "
|
||||
"A picture that is already transparent needs none of this: those parts are left out "
|
||||
"either way."));
|
||||
"a slab of white behind a logo. A picture that is already transparent needs none of "
|
||||
"this: those parts are left out either way."));
|
||||
if (layer->bg_remove) {
|
||||
// Which colour is being removed, shown rather than described: the swatch is also a
|
||||
// picker, and the dropper beside it takes the colour from the preview above.
|
||||
const float x0 = ImGui::GetCursorPosX();
|
||||
ImGui::AlignTextToFramePadding();
|
||||
m_imgui->text(_L("Color"));
|
||||
ImGui::SameLine();
|
||||
ImGui::SetCursorPosX(std::max(ImGui::GetCursorPosX(), x0 + label_w));
|
||||
float shown_bg[3] = { layer->bg_color[0], layer->bg_color[1], layer->bg_color[2] };
|
||||
if (layer->bg_from_corners) {
|
||||
// Nothing has been picked, so show what the corners actually gave: the same median
|
||||
// the bake will key on, not a placeholder white.
|
||||
const DecodedHeightTexture &tex = decode_height_texture(*layer);
|
||||
if (tex.has_color() && tex.width > 1 && tex.height > 1) {
|
||||
const size_t n = size_t(tex.width) * size_t(tex.height);
|
||||
const size_t corner[4] = { 0, size_t(tex.width) - 1,
|
||||
(size_t(tex.height) - 1) * size_t(tex.width), n - 1 };
|
||||
for (int c = 0; c < 3; ++c) {
|
||||
std::array<float, 4> v{};
|
||||
for (int i = 0; i < 4; ++i)
|
||||
v[size_t(i)] = float(tex.rgb[corner[i] * 3 + size_t(c)]);
|
||||
std::sort(v.begin(), v.end());
|
||||
shown_bg[c] = 0.5f * (v[1] + v[2]) / 255.f;
|
||||
}
|
||||
}
|
||||
}
|
||||
ImGui::SetNextItemWidth(x0 + panel_w - ImGui::GetCursorPosX() - icon_sm - gap_s);
|
||||
if (ImGui::ColorEdit3("##bg_color", shown_bg,
|
||||
ImGuiColorEditFlags_NoInputs | ImGuiColorEditFlags_NoLabel)) {
|
||||
for (int c = 0; c < 3; ++c)
|
||||
layer->bg_color[c] = shown_bg[c];
|
||||
layer->bg_from_corners = false;
|
||||
m_preview_params_dirty = true;
|
||||
}
|
||||
hover_tip(layer->bg_from_corners ?
|
||||
_u8L("Taken from the four corners of the picture, which is right for a logo on a "
|
||||
"plain card. Click to choose a color yourself instead.") :
|
||||
_u8L("The color being removed. Click to change it."));
|
||||
ImGui::SameLine(0.f, gap_s);
|
||||
if (icon_toggle(930, "texture_displacement_adjust.svg", m_image_bg_picking, _L("Pick from the picture"),
|
||||
_L("Pick from the picture - then click the background in the preview above.")))
|
||||
m_image_bg_picking = !m_image_bg_picking;
|
||||
if (!layer->bg_from_corners) {
|
||||
ImGui::SameLine(0.f, gap_s);
|
||||
if (m_imgui->button(_u8L("Corners"))) {
|
||||
layer->bg_from_corners = true;
|
||||
m_preview_params_dirty = true;
|
||||
}
|
||||
hover_tip(_u8L("Back to taking the color from the picture's four corners."));
|
||||
}
|
||||
|
||||
m_preview_params_dirty |= float_row("##bg_tol", _L("Tolerance"), &layer->bg_tolerance, 0.01f, 0.6f, "%.2f", false);
|
||||
hover_tip(_u8L("How different from the corners still counts as background. Raise it when a "
|
||||
hover_tip(_u8L("How different from that color still counts as background. Raise it when a "
|
||||
"shadow or a gradient is left behind; lower it when it starts eating the picture."));
|
||||
}
|
||||
|
||||
@@ -569,10 +660,12 @@ void GLGizmoTextureDisplacement::render_image_input_window(float x, float y, flo
|
||||
layer->hue = 0.f;
|
||||
layer->gamma = 1.f;
|
||||
layer->sharpness = 0.f;
|
||||
layer->threshold_enabled = false;
|
||||
layer->threshold = 0.5f;
|
||||
layer->color_count = 0;
|
||||
layer->bg_remove = false;
|
||||
layer->bg_tolerance = 0.15f;
|
||||
layer->bg_from_corners = true;
|
||||
layer->bg_color[0] = layer->bg_color[1] = layer->bg_color[2] = 1.f;
|
||||
m_image_bg_picking = false;
|
||||
layer->color_invert = false;
|
||||
m_preview_params_dirty = true;
|
||||
}
|
||||
|
||||
@@ -160,21 +160,24 @@ public:
|
||||
struct ColorAdjustKey
|
||||
{
|
||||
float brightness = 0.f, contrast = 0.f, saturation = 1.f, gamma = 1.f, hue = 0.f;
|
||||
float sharpness = 0.f, threshold = 0.5f, bg_tolerance = 0.15f;
|
||||
bool invert = false, threshold_on = false, bg_remove = false;
|
||||
float sharpness = 0.f, bg_tolerance = 0.15f;
|
||||
float bg_r = 1.f, bg_g = 1.f, bg_b = 1.f;
|
||||
int color_count = 0;
|
||||
bool invert = false, bg_remove = false, bg_from_corners = true;
|
||||
bool operator==(const ColorAdjustKey &o) const
|
||||
{
|
||||
return brightness == o.brightness && contrast == o.contrast && saturation == o.saturation &&
|
||||
gamma == o.gamma && hue == o.hue && sharpness == o.sharpness &&
|
||||
threshold == o.threshold && bg_tolerance == o.bg_tolerance && invert == o.invert &&
|
||||
threshold_on == o.threshold_on && bg_remove == o.bg_remove;
|
||||
bg_tolerance == o.bg_tolerance && bg_r == o.bg_r && bg_g == o.bg_g && bg_b == o.bg_b &&
|
||||
color_count == o.color_count && invert == o.invert && bg_remove == o.bg_remove &&
|
||||
bg_from_corners == o.bg_from_corners;
|
||||
}
|
||||
bool operator!=(const ColorAdjustKey &o) const { return !(*this == o); }
|
||||
static ColorAdjustKey of(const TextureDisplacementLayer &l)
|
||||
{
|
||||
return { l.brightness, l.contrast, l.saturation, l.gamma, l.hue,
|
||||
l.sharpness, l.threshold, l.bg_tolerance,
|
||||
l.color_invert, l.threshold_enabled, l.bg_remove };
|
||||
return { l.brightness, l.contrast, l.saturation, l.gamma, l.hue,
|
||||
l.sharpness, l.bg_tolerance, l.bg_color[0], l.bg_color[1], l.bg_color[2],
|
||||
l.color_count, l.color_invert, l.bg_remove, l.bg_from_corners };
|
||||
}
|
||||
};
|
||||
std::vector<ColorAdjustKey> m_palette_adjusts; // alongside m_palette_images, one per entry
|
||||
@@ -324,6 +327,9 @@ private:
|
||||
// render_image_input_window() draws over the canvas.
|
||||
bool m_file_drag_over = false;
|
||||
void render_image_drop_hint();
|
||||
// Set while the panel's background dropper is armed: the next click on the picture preview takes
|
||||
// the colour under it instead of opening the library.
|
||||
bool m_image_bg_picking = false;
|
||||
// Standard mode's Bake: remesh to an even density, refine where the texture bends, then displace.
|
||||
// The order matters and is the whole reason this is one button - a height map can only move
|
||||
// existing vertices, so the mesh has to be prepared first, and remeshing after painting would drop
|
||||
|
||||
Reference in New Issue
Block a user