mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-27 02:41:17 +00:00
Texture displacement: per-layer bake, UV pane redesign, unwrap and layer view fixes
- Bake: each layer is sampled only on its own painted area; analytic projections used to stack every layer over every painted region, so the top layer's texture showed on all of them (colour sampler too) - Auto resolution follows the texture's texel size and sharpness again - Unwrap: charts cut by each face's own normal (a cube gives 6 islands, not 12 triangles); non-disk charts (tubes, closed shells) are split until they flatten; connected nets test real triangle overlap, grow from the largest chart and are packed side by side - UV edits are stored per unwrapped copy, so dragging a seam vertex no longer moves its copies in neighbouring islands - UV pane: tool strip with unwrap settings moved in from the panel, sharp HiDPI icons, clearer island/edge/selection drawing with hover, texture picker from the thumbnail, texture no longer lost on reopen (GL state from the 3D view, background upload retries) - Panel: whole-model select/erase as icons in the tools row; inactive layers' paint shown muted; colour textures shown in colour in the picker - Built-in displacement texture library - Tests for unwrap segmentation, connected nets, UV edits and per-layer sampling
This commit is contained in:
@@ -2943,6 +2943,11 @@ void ModelVolume::assign_new_unique_ids_recursive()
|
||||
seam_facets.set_new_unique_id();
|
||||
mmu_segmentation_facets.set_new_unique_id();
|
||||
fuzzy_skin_facets.set_new_unique_id();
|
||||
// As set_new_unique_id() already does: the undo/redo stack stores FacetsAnnotation contents keyed
|
||||
// by ObjectID, so a clone left sharing these ids with its source can be handed the source's mask
|
||||
// on an undo - after which a paint mask and the mesh it was recorded against no longer match.
|
||||
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i)
|
||||
texture_displacement_facet(i).set_new_unique_id();
|
||||
}
|
||||
|
||||
void ModelVolume::rotate(double angle, Axis axis)
|
||||
|
||||
@@ -98,7 +98,7 @@ struct HeapEntry
|
||||
|
||||
DecimateResult decimate(const TriSoup &geometry, size_t target_triangles, bool harvest_flat,
|
||||
double harvest_tol, const std::vector<uint8_t> &locked_faces,
|
||||
const DecimateProgressFn &on_progress)
|
||||
const DecimateProgressFn &on_progress, const std::vector<int> &face_color)
|
||||
{
|
||||
DecimateResult result;
|
||||
const size_t n = geometry.pos.size();
|
||||
@@ -211,8 +211,10 @@ DecimateResult decimate(const TriSoup &geometry, size_t target_triangles, bool h
|
||||
faces[size_t(er.f0) * 3 + 2]);
|
||||
const Vec3d n1 = face_normal_unit(pos, faces[size_t(er.f1) * 3], faces[size_t(er.f1) * 3 + 1],
|
||||
faces[size_t(er.f1) * 3 + 2]);
|
||||
if (n0.dot(n1) >= DECIMATE_CREASE_COS)
|
||||
continue; // smooth enough to be no crease
|
||||
const bool color_edge = face_color.size() > std::max(size_t(er.f0), size_t(er.f1)) &&
|
||||
face_color[size_t(er.f0)] != face_color[size_t(er.f1)];
|
||||
if (!color_edge && n0.dot(n1) >= DECIMATE_CREASE_COS)
|
||||
continue; // smooth enough to be no crease, and no colour changes across it
|
||||
|
||||
const Vec3d e = pos[size_t(er.vb)] - pos[size_t(er.va)];
|
||||
const double elen = e.norm();
|
||||
|
||||
@@ -47,10 +47,14 @@ struct DecimateResult
|
||||
|
||||
// `locked_faces`: one entry per input triangle; a vertex touching one may neither move nor be
|
||||
// removed, which also pins the ring between the two regions.
|
||||
// `face_color`: optional, one entry per input triangle. An edge between two faces of different
|
||||
// colour is treated as a crease, so the simplified triangles never span a colour boundary and the
|
||||
// boundary keeps its place - a per-triangle colour read off the result then has nothing to smear.
|
||||
DecimateResult decimate(const TriSoup &geometry, size_t target_triangles, bool harvest_flat = true,
|
||||
double harvest_tol = DECIMATE_DEFAULT_HARVEST_TOL,
|
||||
const std::vector<uint8_t> &locked_faces = {},
|
||||
const DecimateProgressFn &on_progress = {});
|
||||
const DecimateProgressFn &on_progress = {},
|
||||
const std::vector<int> &face_color = {});
|
||||
|
||||
} // namespace TextureBake
|
||||
} // namespace Slic3r
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
#include "TextureBakePipeline.hpp"
|
||||
|
||||
#include <tbb/blocked_range.h>
|
||||
#include <tbb/parallel_for.h>
|
||||
|
||||
#include "TextureBakeDebug.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
@@ -111,7 +114,8 @@ size_t snap_bottom_to_flat(TriSoup &geometry, float bottom_z, double tol)
|
||||
PipelineResult run_pipeline(const TriSoup &input, const HeightSampleFn &sample,
|
||||
const PipelineSettings &settings, const DisplaceBounds &bounds,
|
||||
PipelineMode mode, const std::vector<uint8_t> &face_excluded,
|
||||
const PipelineProgressFn &on_progress, BakeStageRecorder *debug)
|
||||
const PipelineProgressFn &on_progress, BakeStageRecorder *debug,
|
||||
const ColorSampleFn &color_sample)
|
||||
{
|
||||
PipelineResult result;
|
||||
const auto report = [&](const char *stage, double f) {
|
||||
@@ -193,6 +197,46 @@ PipelineResult run_pipeline(const TriSoup &input, const HeightSampleFn &sample,
|
||||
}
|
||||
}
|
||||
|
||||
// 2b. Paint finer than the input triangles. The caller includes a source triangle when any part of
|
||||
// it is painted; now that the faces are small, ask once more per face and switch the unpainted
|
||||
// ones off. They are pinned like the excluded region from here on (their own corners at weight
|
||||
// 1, and the displacement's boundary sealing pins the stroke's rim on the painted side), but they
|
||||
// are refined pieces of painted triangles, not original geometry, so `soft_excluded` keeps them
|
||||
// out of the decimation lock below. Every stage between here and the decimation rewrites faces in
|
||||
// place, so the per-face flag stays valid by index.
|
||||
std::vector<uint8_t> soft_excluded;
|
||||
if (settings.painted) {
|
||||
const size_t nf = sub.geometry.triangle_count();
|
||||
const bool have_w = !sub.geometry.exclude_weight.empty();
|
||||
std::vector<uint8_t> unpainted(nf, 0);
|
||||
tbb::parallel_for(tbb::blocked_range<size_t>(0, nf), [&](const tbb::blocked_range<size_t> &r) {
|
||||
for (size_t t = r.begin(); t < r.end(); ++t) {
|
||||
if (have_w && sub.geometry.exclude_weight[t * 3] > 0.99f)
|
||||
continue; // excluded from the start, never asked
|
||||
const Vec3f &a = sub.geometry.pos[t * 3], &b = sub.geometry.pos[t * 3 + 1], &c = sub.geometry.pos[t * 3 + 2];
|
||||
if (!settings.painted((a + b + c) / 3.f))
|
||||
unpainted[t] = 1;
|
||||
}
|
||||
});
|
||||
size_t switched = 0;
|
||||
for (size_t t = 0; t < nf; ++t)
|
||||
switched += unpainted[t];
|
||||
if (switched > 0) {
|
||||
if (sub.geometry.exclude_weight.empty())
|
||||
sub.geometry.exclude_weight.assign(sub.geometry.pos.size(), 0.f);
|
||||
for (size_t t = 0; t < nf; ++t)
|
||||
if (unpainted[t])
|
||||
sub.geometry.exclude_weight[t * 3] = sub.geometry.exclude_weight[t * 3 + 1] =
|
||||
sub.geometry.exclude_weight[t * 3 + 2] = 1.f;
|
||||
soft_excluded = std::move(unpainted);
|
||||
}
|
||||
lap("paint", sub.geometry, std::to_string(switched) + " faces switched off");
|
||||
if (!report("paint", 1.0)) {
|
||||
result.canceled = true;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Align the mesh to the height field's edges, then displace.
|
||||
if (settings.relocate) {
|
||||
std::vector<uint8_t> locked;
|
||||
@@ -247,12 +291,40 @@ PipelineResult run_pipeline(const TriSoup &input, const HeightSampleFn &sample,
|
||||
std::vector<uint8_t> locked;
|
||||
if (settings.preserve_untextured && !displaced.exclude_weight.empty()) {
|
||||
locked.assign(displaced.triangle_count(), 0);
|
||||
// The corner average, as the displacement stage judges it: after the flip stage's
|
||||
// per-vertex merge an included face touching the excluded region carries one corner
|
||||
// at weight 1, and must stay free to collapse and to take colour.
|
||||
for (size_t t = 0; t < locked.size(); ++t)
|
||||
locked[t] = displaced.exclude_weight[t * 3] > 0.99f ? 1 : 0;
|
||||
locked[t] = (displaced.exclude_weight[t * 3] + displaced.exclude_weight[t * 3 + 1] +
|
||||
displaced.exclude_weight[t * 3 + 2]) / 3.f > 0.99f ? 1 : 0;
|
||||
// Faces the paint test switched off carry weight 1 too, but are refined pieces of
|
||||
// painted triangles rather than original geometry: locking them would keep a partly
|
||||
// painted source triangle at full refinement. Face indices survived relocate, flip
|
||||
// and displace unchanged, so the flag still lines up.
|
||||
for (size_t t = 0; t < locked.size() && t < soft_excluded.size(); ++t)
|
||||
if (soft_excluded[t])
|
||||
locked[t] = 0;
|
||||
}
|
||||
// Colour per face on the fine mesh, so colour boundaries become creases the collapse
|
||||
// respects. Excluded (unpainted) faces take no colour.
|
||||
std::vector<int> face_color;
|
||||
if (color_sample) {
|
||||
const size_t nf = displaced.triangle_count();
|
||||
face_color.assign(nf, -1);
|
||||
const bool have_w = !displaced.exclude_weight.empty();
|
||||
tbb::parallel_for(tbb::blocked_range<size_t>(0, nf), [&](const tbb::blocked_range<size_t> &r) {
|
||||
for (size_t t = r.begin(); t < r.end(); ++t) {
|
||||
if (have_w && (displaced.exclude_weight[t * 3] + displaced.exclude_weight[t * 3 + 1] +
|
||||
displaced.exclude_weight[t * 3 + 2]) / 3.f > 0.99f)
|
||||
continue;
|
||||
const Vec3f &a = displaced.pos[t * 3], &b = displaced.pos[t * 3 + 1], &c = displaced.pos[t * 3 + 2];
|
||||
face_color[t] = color_sample((a + b + c) / 3.f, displaced.nrm[t * 3]);
|
||||
}
|
||||
});
|
||||
}
|
||||
DecimateResult dec = decimate(displaced, settings.max_triangles, settings.harvest_flat,
|
||||
settings.harvest_tol, locked,
|
||||
[&](double f) { return report("decimate", f); });
|
||||
[&](double f) { return report("decimate", f); }, face_color);
|
||||
result.locked_over_budget = dec.locked_over_budget;
|
||||
displaced = std::move(dec.geometry);
|
||||
lap("decimate", displaced, "over budget, simplified");
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
// The bake pipeline:
|
||||
//
|
||||
// subdivide -> [regularize -> re-subdivide] -> [relocate] -> displace -> [decimate]
|
||||
// -> bottom clamp -> bottom snap -> [resolve T-junctions]
|
||||
// subdivide -> [regularize -> re-subdivide] -> [paint test] -> [relocate] -> [flip edges]
|
||||
// -> displace -> [decimate] -> bottom clamp -> bottom snap -> [resolve T-junctions]
|
||||
//
|
||||
// Regularization sits between two subdivisions on purpose: it dissolves the slivers refinement
|
||||
// inherited, which lengthens some edges past the target, and the second pass brings those back.
|
||||
@@ -66,6 +66,13 @@ struct PipelineSettings
|
||||
|
||||
DisplaceSettings displace;
|
||||
|
||||
// Optional. Asked once per refined face (its centroid, in the soup's coordinates) after the
|
||||
// refinement stages and before displacement, for faces whose source triangle was included:
|
||||
// false marks the face as unpainted (no displacement), so paint finer than the input triangles
|
||||
// is honoured. Faces excluded from the start are never asked. Called from several threads at
|
||||
// once, so it must be safe to call concurrently.
|
||||
std::function<bool(const Vec3f ¢roid)> painted;
|
||||
|
||||
// Export mode only.
|
||||
size_t max_triangles = 750'000;
|
||||
// Keep removing zero-cost flat faces past the target. Only applies when decimation runs, i.e. when
|
||||
@@ -87,6 +94,9 @@ struct PipelineSettings
|
||||
|
||||
// Stage name and a fraction within it. Returning false cancels the run.
|
||||
using PipelineProgressFn = std::function<bool(const char *stage, double fraction)>;
|
||||
// Colour class of a point of the surface (a palette index, -1 for none), for the decimation's
|
||||
// colour-boundary creases. Only consulted when the mesh is over budget.
|
||||
using ColorSampleFn = std::function<int(const Vec3f ¢roid, const Vec3f &normal)>;
|
||||
|
||||
struct PipelineResult
|
||||
{
|
||||
@@ -105,7 +115,7 @@ PipelineResult run_pipeline(const TriSoup &input, const HeightSampleFn &sample,
|
||||
const PipelineSettings &settings, const DisplaceBounds &bounds,
|
||||
PipelineMode mode, const std::vector<uint8_t> &face_excluded = {},
|
||||
const PipelineProgressFn &on_progress = {},
|
||||
BakeStageRecorder *debug = nullptr);
|
||||
BakeStageRecorder *debug = nullptr, const ColorSampleFn &color_sample = {});
|
||||
|
||||
// Snap anything that ended below the model's original bottom back up to it.
|
||||
void clamp_below_bottom(TriSoup &geometry, float bottom_z);
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <array>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <functional>
|
||||
#include <cassert>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
@@ -277,18 +278,26 @@ DecodedHeightTexture decode_height_texture(const TextureDisplacementLayer &layer
|
||||
col.bytes_per_pixel < 3)
|
||||
return result;
|
||||
|
||||
const size_t n = size_t(col.cols) * size_t(col.rows);
|
||||
const size_t bpp = size_t(col.bytes_per_pixel);
|
||||
result.width = int(col.cols);
|
||||
result.height = int(col.rows);
|
||||
const size_t cols = size_t(col.cols), rows = size_t(col.rows), n = cols * rows;
|
||||
const size_t bpp = size_t(col.bytes_per_pixel);
|
||||
const size_t stride = col.buf.size() / rows;
|
||||
result.width = int(cols);
|
||||
result.height = int(rows);
|
||||
result.pixels.resize(n);
|
||||
result.rgb.resize(n * 3);
|
||||
for (size_t i = 0; i < n; ++i) {
|
||||
const uint8_t r = col.buf[i * bpp], g = col.buf[i * bpp + 1], b = col.buf[i * bpp + 2];
|
||||
result.rgb[i * 3] = r;
|
||||
result.rgb[i * 3 + 1] = g;
|
||||
result.rgb[i * 3 + 2] = b;
|
||||
result.pixels[i] = uint8_t(std::lround(0.299 * r + 0.587 * g + 0.114 * b));
|
||||
// decode_colored_png() fills its buffer bottom-up (its other callers hand the rows to
|
||||
// OpenGL, which wants them that way); a height map is top-down, like decode_png()'s grey
|
||||
// output, so a colour image has to read the same way up as a grey copy of itself.
|
||||
for (size_t y = 0; y < rows; ++y) {
|
||||
const uint8_t *src = col.buf.data() + (rows - 1 - y) * stride;
|
||||
for (size_t x = 0; x < cols; ++x) {
|
||||
const size_t i = y * cols + x;
|
||||
const uint8_t r = src[x * bpp], g = src[x * bpp + 1], b = src[x * bpp + 2];
|
||||
result.rgb[i * 3] = r;
|
||||
result.rgb[i * 3 + 1] = g;
|
||||
result.rgb[i * 3 + 2] = b;
|
||||
result.pixels[i] = uint8_t(std::lround(0.299 * r + 0.587 * g + 0.114 * b));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -386,6 +395,24 @@ TextureDetail analyze_texture_detail(const TextureDisplacementLayer &layer)
|
||||
else if (out.sharp_fraction > 0.05f || out.mean_gradient > 20.f) out.pixels_per_edge = 1.5f;
|
||||
else if (out.mean_gradient > 8.f) out.pixels_per_edge = 2.5f;
|
||||
else out.pixels_per_edge = 4.f;
|
||||
|
||||
// Colour spread: a coarse histogram (8 levels per channel, 64 levels for a grey image) and
|
||||
// the share of the eight fullest bins. Tiles, logos and camouflage put nearly everything in a
|
||||
// handful of bins even with some texture noise; a photograph spreads across hundreds.
|
||||
std::vector<uint32_t> bins(size_t(8 * 8 * 8), 0);
|
||||
const size_t npx = size_t(w) * size_t(h);
|
||||
if (tex.has_color())
|
||||
for (size_t i = 0; i < npx; ++i)
|
||||
++bins[size_t(tex.rgb[i * 3] >> 5) * 64 + size_t(tex.rgb[i * 3 + 1] >> 5) * 8 + size_t(tex.rgb[i * 3 + 2] >> 5)];
|
||||
else
|
||||
for (size_t i = 0; i < npx; ++i)
|
||||
++bins[size_t(tex.pixels[i] >> 2) * 8]; // 64 grey levels, spread over distinct bins
|
||||
std::partial_sort(bins.begin(), bins.begin() + 8, bins.end(), std::greater<uint32_t>());
|
||||
uint64_t top = 0;
|
||||
for (int i = 0; i < 8; ++i)
|
||||
top += bins[size_t(i)];
|
||||
out.flat_share = float(double(top) / double(npx));
|
||||
out.flat_colors = out.flat_share >= 0.85f;
|
||||
}
|
||||
std::lock_guard<std::mutex> lock(g_texture_detail_cache.mutex);
|
||||
auto &entries = g_texture_detail_cache.entries;
|
||||
@@ -399,60 +426,40 @@ V2Resolution recommend_v2_resolution(const indexed_triangle_set
|
||||
const std::vector<TextureDisplacementLayer> &layers,
|
||||
const Transform3d &volume_to_world)
|
||||
{
|
||||
// BumpMesh's smart resolution, the numbers included: equilateral-cover triangle density, a 16 M
|
||||
// triangle refinement cap taken at 75 %, a 0.5 mm reference relief for the budget.
|
||||
constexpr double TRIS_PER_AREA = 2.309, CAP_TRIANGLES = 16e6 * 0.75;
|
||||
constexpr double EDGE_MIN = 0.05, EDGE_MAX = 5.0;
|
||||
constexpr double BUDGET_MIN = 10e3, BUDGET_MAX = 2000e3, REF_DEPTH = 0.5, MIN_DEPTH = 0.1;
|
||||
// bumpmesh.com's defaults on model load: edge = diagonal / 250 in [0.05, 5] mm, budget 750 k. A
|
||||
// texture-driven variant (BumpMesh's smart resolution) was measured to give better walls on step
|
||||
// textures at 2-10x the bake time and up to 2 M output triangles; the user preferred the site's
|
||||
// defaults. The texel size and sharpness are still reported for the panel.
|
||||
constexpr double EDGE_MIN = 0.05, EDGE_MAX = 5.0, DIAG_DIVISOR = 250.0;
|
||||
constexpr int BUDGET_K = 750;
|
||||
|
||||
V2Resolution out;
|
||||
// The finest layer decides: the smallest detail edge (texel x pixels per edge) across the layers.
|
||||
double detail_edge = std::numeric_limits<double>::max(), depth = 0.0;
|
||||
if (mesh.vertices.empty())
|
||||
return out;
|
||||
for (const TextureDisplacementLayer &layer : layers) {
|
||||
if (layer.empty() || layer.tiling_scale <= 0.f)
|
||||
continue;
|
||||
const DecodedHeightTexture &tex = decode_height_texture(layer);
|
||||
if (tex.width <= 0)
|
||||
continue;
|
||||
const TextureDetail detail = analyze_texture_detail(layer);
|
||||
const double texel = double(layer.tiling_scale) / double(tex.width);
|
||||
const double edge = texel * double(detail.pixels_per_edge);
|
||||
if (edge < detail_edge) {
|
||||
detail_edge = edge;
|
||||
out.texel_mm = float(texel);
|
||||
out.pixels_per_edge = detail.pixels_per_edge;
|
||||
depth = std::abs(double(layer.depth_mm));
|
||||
const float texel = layer.tiling_scale / float(tex.width);
|
||||
if (out.texel_mm <= 0.f || texel < out.texel_mm) {
|
||||
out.texel_mm = texel;
|
||||
out.pixels_per_edge = analyze_texture_detail(layer).pixels_per_edge;
|
||||
}
|
||||
}
|
||||
if (out.texel_mm <= 0.f || mesh.vertices.empty())
|
||||
return out;
|
||||
|
||||
// Surface area and diagonal in world mm: the tile is in world mm and the pipeline refines there.
|
||||
double area = 0.0;
|
||||
Vec3d bmin = Vec3d::Constant(std::numeric_limits<double>::max()), bmax = -bmin;
|
||||
std::vector<Vec3d> world(mesh.vertices.size());
|
||||
for (size_t i = 0; i < world.size(); ++i) {
|
||||
world[i] = volume_to_world * mesh.vertices[i].cast<double>();
|
||||
bmin = bmin.cwiseMin(world[i]);
|
||||
bmax = bmax.cwiseMax(world[i]);
|
||||
Vec3d bmin = Vec3d::Constant(std::numeric_limits<double>::max()), bmax = -bmin;
|
||||
for (const Vec3f &v : mesh.vertices) {
|
||||
const Vec3d w = volume_to_world * v.cast<double>();
|
||||
bmin = bmin.cwiseMin(w);
|
||||
bmax = bmax.cwiseMax(w);
|
||||
}
|
||||
for (const stl_triangle_vertex_indices &t : mesh.indices)
|
||||
area += 0.5 * (world[size_t(t[1])] - world[size_t(t[0])]).cross(world[size_t(t[2])] - world[size_t(t[0])]).norm();
|
||||
const double diag = (bmax - bmin).norm();
|
||||
|
||||
const double budget_edge = std::sqrt(TRIS_PER_AREA * area / CAP_TRIANGLES);
|
||||
double edge = std::max(detail_edge, budget_edge);
|
||||
out.budget_bound = budget_edge > detail_edge;
|
||||
const double hi = std::max(EDGE_MIN, std::min(EDGE_MAX, diag / 50.0));
|
||||
edge = std::clamp(edge, EDGE_MIN, hi);
|
||||
edge = std::max(EDGE_MIN, std::ceil(edge * 100.0) / 100.0); // up, so the cap holds
|
||||
out.edge_mm = float(edge);
|
||||
|
||||
const double depth_scale = std::sqrt(REF_DEPTH / std::max(depth, MIN_DEPTH));
|
||||
const double target_edge = double(out.pixels_per_edge) * double(out.texel_mm) * depth_scale;
|
||||
const double raw = TRIS_PER_AREA * area / (target_edge * target_edge);
|
||||
const double stepped = std::round(raw / 10e3) * 10e3;
|
||||
out.budget_k = int(std::clamp(stepped, BUDGET_MIN, BUDGET_MAX) / 1000.0);
|
||||
double edge = std::clamp(diag / DIAG_DIVISOR, EDGE_MIN, EDGE_MAX);
|
||||
edge = std::max(EDGE_MIN, std::ceil(edge * 100.0) / 100.0);
|
||||
out.edge_mm = float(edge);
|
||||
out.budget_k = BUDGET_K;
|
||||
out.budget_bound = false;
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -917,6 +924,9 @@ PatchUnwrap compute_patch_unwrap(const indexed_triangle_set &patch, float seam_a
|
||||
std::vector<Vec2f> uvs;
|
||||
std::vector<int> to_patch; // chart vertex -> patch vertex
|
||||
std::vector<stl_triangle_vertex_indices> indices; // chart-local
|
||||
// Parallel to `indices`: the patch triangle each one came from. compact_patch_with_map() keeps
|
||||
// the patch's triangle count *and* order, so a compact face index is already a patch face index.
|
||||
std::vector<int> faces;
|
||||
Vec2f min = Vec2f::Zero();
|
||||
Vec2f size = Vec2f::Zero();
|
||||
};
|
||||
@@ -950,6 +960,7 @@ PatchUnwrap compute_patch_unwrap(const indexed_triangle_set &patch, float seam_a
|
||||
local_tri[i] = compact_to_local[size_t(cv)];
|
||||
}
|
||||
chart_mesh.indices.push_back(local_tri);
|
||||
chart.faces.push_back(int(f));
|
||||
}
|
||||
if (chart_mesh.indices.empty())
|
||||
continue;
|
||||
@@ -1043,6 +1054,7 @@ PatchUnwrap compute_patch_unwrap(const indexed_triangle_set &patch, float seam_a
|
||||
result.vertex_chart.insert(result.vertex_chart.end(), chart.uvs.size(), c);
|
||||
for (const stl_triangle_vertex_indices &tri : chart.indices)
|
||||
result.indices.emplace_back(tri[0] + base, tri[1] + base, tri[2] + base);
|
||||
result.source_face.insert(result.source_face.end(), chart.faces.begin(), chart.faces.end());
|
||||
|
||||
if (!chart.uvs.empty()) {
|
||||
Vec2f sum = Vec2f::Zero();
|
||||
@@ -1509,6 +1521,41 @@ std::vector<Vec2f> compute_lscm_uvs(const indexed_triangle_set &patch, const Tex
|
||||
return per_vertex;
|
||||
}
|
||||
|
||||
std::vector<Vec2f> compute_lscm_corner_uvs(const indexed_triangle_set &patch, const TextureDisplacementLayer &layer)
|
||||
{
|
||||
// Padding 0 and the layer's own seam angle/edges, exactly as compute_lscm_uvs() does - the two must
|
||||
// unwrap identically or a hand placement would land in one place on screen and another in the bake.
|
||||
const PatchUnwrap unwrap = compute_patch_unwrap(patch, layer.lscm_seam_angle_deg, 0.f, layer.lscm_seam_edges);
|
||||
if (unwrap.empty() || unwrap.source_face.size() != unwrap.indices.size())
|
||||
return {};
|
||||
|
||||
PatchUnwrap edited_unwrap = unwrap;
|
||||
apply_lscm_uv_overrides(edited_unwrap, layer.lscm_uv_overrides);
|
||||
|
||||
// No first-copy-wins collapse here: the unwrap's triangles are already per chart, so each corner
|
||||
// simply takes its own chart's copy. A triangle the unwrap dropped (a sliver a chart rejected) keeps
|
||||
// the zero it was initialised with; the callers treat that as "no placement" the same way they treat
|
||||
// an empty result.
|
||||
std::vector<Vec2f> corner(patch.indices.size() * 3, Vec2f::Zero());
|
||||
for (size_t t = 0; t < edited_unwrap.indices.size(); ++t) {
|
||||
const int f = edited_unwrap.source_face[t];
|
||||
if (f < 0 || size_t(f) >= patch.indices.size())
|
||||
continue;
|
||||
const stl_triangle_vertex_indices &tri = edited_unwrap.indices[t];
|
||||
for (int k = 0; k < 3; ++k) {
|
||||
const int uvi = tri[k];
|
||||
if (uvi < 0 || size_t(uvi) >= edited_unwrap.uvs.size())
|
||||
continue;
|
||||
// The island transform is taken against the *unedited* unwrap, whose chart_centroid is the
|
||||
// pivot the UV editor rotates about - same as compute_lscm_uvs().
|
||||
corner[size_t(f) * 3 + size_t(k)] = apply_island_transform(edited_unwrap.uvs[size_t(uvi)],
|
||||
edited_unwrap.vertex_chart[size_t(uvi)],
|
||||
unwrap, layer.islands);
|
||||
}
|
||||
}
|
||||
return corner;
|
||||
}
|
||||
|
||||
namespace {
|
||||
// apply_uv_transform()'s per-layer constants, worked out once. Triplanar sampling runs the transform
|
||||
// three times per point, and recomputing the rotation's cos/sin and the tiling reciprocal on every one
|
||||
@@ -1820,7 +1867,7 @@ bool compute_layer_paint_anchor(const indexed_triangle_set &b
|
||||
// these once, up front, and every layer both projects and displaces along them - so a vertex
|
||||
// covered by several layers is pushed along one single, well-defined direction rather than along
|
||||
// whatever direction the surface happened to be pointing partway through the stack.
|
||||
static std::vector<Vec3f> texture_displacement_vertex_normals(const indexed_triangle_set &its)
|
||||
std::vector<Vec3f> texture_displacement_vertex_normals(const indexed_triangle_set &its)
|
||||
{
|
||||
std::vector<Vec3f> normals(its.vertices.size(), Vec3f::Zero());
|
||||
for (const stl_triangle_vertex_indices &tri : its.indices) {
|
||||
@@ -1943,6 +1990,98 @@ void despeckle_triangle_colors(const indexed_triangle_set &mesh, std::vector<int
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void merge_small_color_regions(const indexed_triangle_set &mesh, std::vector<int> &color, float min_area_mm2)
|
||||
{
|
||||
const size_t n = mesh.indices.size();
|
||||
if (min_area_mm2 <= 0.f || color.size() != n)
|
||||
return;
|
||||
const std::vector<Vec3i32> neighbors = its_face_neighbors(mesh);
|
||||
if (neighbors.size() != n)
|
||||
return;
|
||||
|
||||
const auto edge_length = [&mesh](size_t f, int e) {
|
||||
const stl_triangle_vertex_indices &t = mesh.indices[f];
|
||||
return (mesh.vertices[size_t(t[(e + 1) % 3])] - mesh.vertices[size_t(t[e])]).norm();
|
||||
};
|
||||
|
||||
// Connected components of equal colour: `faces` lists every coloured face, component by
|
||||
// component, `start` delimits them. Uncoloured faces (-1) belong to no component and block the
|
||||
// flood, so a region never grows across the paint's border.
|
||||
std::vector<int> component(n, -1);
|
||||
std::vector<int> faces;
|
||||
std::vector<size_t> start;
|
||||
std::vector<float> area;
|
||||
std::vector<int> stack;
|
||||
faces.reserve(n);
|
||||
for (size_t seed = 0; seed < n; ++seed) {
|
||||
if (color[seed] < 0 || component[seed] >= 0)
|
||||
continue;
|
||||
const int c = color[seed];
|
||||
const int id = int(area.size());
|
||||
start.push_back(faces.size());
|
||||
area.push_back(0.f);
|
||||
component[seed] = id;
|
||||
stack.push_back(int(seed));
|
||||
while (!stack.empty()) {
|
||||
const size_t f = size_t(stack.back());
|
||||
stack.pop_back();
|
||||
faces.push_back(int(f));
|
||||
const stl_triangle_vertex_indices &t = mesh.indices[f];
|
||||
const Vec3f &a = mesh.vertices[size_t(t[0])], &b = mesh.vertices[size_t(t[1])], &cv = mesh.vertices[size_t(t[2])];
|
||||
area[size_t(id)] += 0.5f * (b - a).cross(cv - a).norm();
|
||||
for (int e = 0; e < 3; ++e) {
|
||||
const int nb = neighbors[f][e];
|
||||
if (nb < 0 || size_t(nb) >= n || component[size_t(nb)] >= 0 || color[size_t(nb)] != c)
|
||||
continue;
|
||||
component[size_t(nb)] = id;
|
||||
stack.push_back(nb);
|
||||
}
|
||||
}
|
||||
}
|
||||
start.push_back(faces.size());
|
||||
|
||||
// Smallest first, so that when a small island borders a slightly larger one the larger one has
|
||||
// not yet moved and the small one joins whatever the two of them sit in; the larger one then
|
||||
// reads that colour in turn.
|
||||
std::vector<int> order;
|
||||
for (int id = 0; id < int(area.size()); ++id)
|
||||
if (area[size_t(id)] < min_area_mm2)
|
||||
order.push_back(id);
|
||||
std::sort(order.begin(), order.end(), [&area](int l, int r) { return area[size_t(l)] < area[size_t(r)]; });
|
||||
|
||||
std::vector<std::pair<int, float>> weights; // neighbouring colour -> shared edge length
|
||||
for (const int id : order) {
|
||||
const size_t begin = start[size_t(id)], end = start[size_t(id) + 1];
|
||||
const int own = color[size_t(faces[begin])];
|
||||
weights.clear();
|
||||
for (size_t k = begin; k < end; ++k) {
|
||||
const size_t f = size_t(faces[k]);
|
||||
for (int e = 0; e < 3; ++e) {
|
||||
const int nb = neighbors[f][e];
|
||||
if (nb < 0 || size_t(nb) >= n)
|
||||
continue;
|
||||
const int c = color[size_t(nb)]; // read now: an earlier merge may have recoloured it
|
||||
if (c < 0 || c == own)
|
||||
continue;
|
||||
const float len = edge_length(f, e);
|
||||
auto it = std::find_if(weights.begin(), weights.end(), [c](const std::pair<int, float> &w) { return w.first == c; });
|
||||
if (it == weights.end())
|
||||
weights.emplace_back(c, len);
|
||||
else
|
||||
it->second += len;
|
||||
}
|
||||
}
|
||||
if (weights.empty())
|
||||
continue; // bordered only by uncoloured faces (or nothing): stays
|
||||
const int target = std::max_element(weights.begin(), weights.end(),
|
||||
[](const std::pair<int, float> &l, const std::pair<int, float> &r) {
|
||||
return l.second < r.second;
|
||||
})->first;
|
||||
for (size_t k = begin; k < end; ++k)
|
||||
color[size_t(faces[k])] = target;
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// Wired to the same layer stack via make_combined_displacement_sampler(), so layers, blend modes and
|
||||
@@ -1960,8 +2099,13 @@ indexed_triangle_set build_texture_displacement_v2(const indexed_triangle_set
|
||||
if (!combined)
|
||||
return mesh; // nothing decodable to displace with
|
||||
|
||||
// Unpainted triangles are excluded, keeping them out of refinement and pinned thereafter.
|
||||
// Unpainted triangles are excluded, keeping them out of refinement and pinned thereafter. The
|
||||
// paint is finer than that, though: a brush stroke splits a source triangle into pieces, and only
|
||||
// some of them are painted. `painted_pieces` keeps every layer's painted pieces (they lie in the
|
||||
// source surface) so the refined faces can be tested against the paint itself, not against the
|
||||
// source triangle they came from.
|
||||
std::vector<uint8_t> excluded(mesh.indices.size(), 1);
|
||||
indexed_triangle_set painted_pieces;
|
||||
{
|
||||
const TriangleMesh selector_mesh(mesh);
|
||||
TriangleSelector selector(selector_mesh);
|
||||
@@ -1977,11 +2121,49 @@ indexed_triangle_set build_texture_displacement_v2(const indexed_triangle_set
|
||||
for (const int src : piece_src)
|
||||
if (src >= 0 && size_t(src) < excluded.size())
|
||||
excluded[size_t(src)] = 0;
|
||||
// `patch` carries the whole mesh's vertex array (see compact_patch_with_map()); append
|
||||
// only what its pieces reference.
|
||||
std::vector<int> unused;
|
||||
const indexed_triangle_set compact = compact_patch_with_map(patch, unused);
|
||||
const int offset = int(painted_pieces.vertices.size());
|
||||
painted_pieces.vertices.insert(painted_pieces.vertices.end(), compact.vertices.begin(), compact.vertices.end());
|
||||
for (const stl_triangle_vertex_indices &t : compact.indices)
|
||||
painted_pieces.indices.emplace_back(t[0] + offset, t[1] + offset, t[2] + offset);
|
||||
}
|
||||
}
|
||||
if (std::all_of(excluded.begin(), excluded.end(), [](uint8_t e) { return e != 0; }))
|
||||
if (std::all_of(excluded.begin(), excluded.end(), [](uint8_t e) { return e != 0; }) || painted_pieces.indices.empty())
|
||||
return mesh; // nothing painted
|
||||
|
||||
// Distance to the nearest painted piece. Built once here; the tree is read-only afterwards, so
|
||||
// the parallel stages below share it freely.
|
||||
const AABBTreeIndirect::Tree3f painted_tree =
|
||||
AABBTreeIndirect::build_aabb_tree_over_indexed_triangle_set(painted_pieces.vertices, painted_pieces.indices);
|
||||
// `foot`/`normal`, when asked for, are the closest point on the painted pieces and that piece's
|
||||
// normal. The pieces lie in the *undisplaced* surface, so for a displaced point those two are the
|
||||
// base position and normal underneath it - the frame colour has to be projected in (see below).
|
||||
const auto painted_closest = [&painted_pieces, &painted_tree](const Vec3f &p, Vec3f *foot, Vec3f *normal) {
|
||||
size_t hit = 0;
|
||||
Vec3f hit_point;
|
||||
const float d2 = AABBTreeIndirect::squared_distance_to_indexed_triangle_set(
|
||||
painted_pieces.vertices, painted_pieces.indices, painted_tree, p, hit, hit_point);
|
||||
if (foot != nullptr)
|
||||
*foot = hit_point;
|
||||
if (normal != nullptr && hit < painted_pieces.indices.size()) {
|
||||
const stl_triangle_vertex_indices &t = painted_pieces.indices[hit];
|
||||
const Vec3f &a = painted_pieces.vertices[size_t(t[0])], &b = painted_pieces.vertices[size_t(t[1])],
|
||||
&c = painted_pieces.vertices[size_t(t[2])];
|
||||
Vec3f n = (b - a).cross(c - a);
|
||||
const float l = n.norm();
|
||||
*normal = (l > 0.f) ? Vec3f(n / l) : Vec3f::UnitZ();
|
||||
}
|
||||
return d2;
|
||||
};
|
||||
const auto painted_dist2 = [&painted_closest](const Vec3f &p) { return painted_closest(p, nullptr, nullptr); };
|
||||
// Before displacement the queried centroids lie in the same surface as the pieces, so anything
|
||||
// beyond a hair is genuinely outside the paint.
|
||||
constexpr float paint_tol = 0.05f;
|
||||
const auto painted_at = [&painted_dist2](const Vec3f &p) { return painted_dist2(p) < paint_tol * paint_tol; };
|
||||
|
||||
// "Auto" resolution and budget (0 and -1) resolve here, from the texture and the model - the mesh
|
||||
// is already in world mm at this point, so no transform is needed.
|
||||
const bool auto_edge = options.v2_refine_mm <= 0.f, auto_budget = options.v2_max_triangles_k < 0;
|
||||
@@ -2012,6 +2194,30 @@ indexed_triangle_set build_texture_displacement_v2(const indexed_triangle_set
|
||||
// of vertices that samples both faces' patterns half and half otherwise comes out as a row of
|
||||
// notches, since it matches neither face.
|
||||
settings.displace.blend_normal_smoothing = 32;
|
||||
// Refined faces are asked against the paint itself, so a stroke narrower than a source triangle
|
||||
// moves only what it covers.
|
||||
// Only when some included source triangle is painted in part: the pieces then cover less area
|
||||
// than the triangles they came from. Whole-triangle paint (the usual case, and every bench) has
|
||||
// nothing to gain from a query per refined face.
|
||||
{
|
||||
const auto area_of = [](const indexed_triangle_set &its) {
|
||||
double a = 0.0;
|
||||
for (const stl_triangle_vertex_indices &t : its.indices)
|
||||
a += 0.5 * double((its.vertices[size_t(t[1])] - its.vertices[size_t(t[0])])
|
||||
.cross(its.vertices[size_t(t[2])] - its.vertices[size_t(t[0])]).norm());
|
||||
return a;
|
||||
};
|
||||
double included_area = 0.0;
|
||||
for (size_t t = 0; t < mesh.indices.size(); ++t)
|
||||
if (excluded[t] == 0) {
|
||||
const stl_triangle_vertex_indices &f = mesh.indices[t];
|
||||
included_area += 0.5 * double((mesh.vertices[size_t(f[1])] - mesh.vertices[size_t(f[0])])
|
||||
.cross(mesh.vertices[size_t(f[2])] - mesh.vertices[size_t(f[0])]).norm());
|
||||
}
|
||||
const double pieces_area = area_of(painted_pieces);
|
||||
if (pieces_area < included_area * (1.0 - 1e-4))
|
||||
settings.painted = painted_at;
|
||||
}
|
||||
|
||||
TextureBake::DisplaceBounds bounds;
|
||||
bounds.min = bounds.max = mesh.vertices.empty() ? Vec3f::Zero() : mesh.vertices.front();
|
||||
@@ -2039,6 +2245,26 @@ indexed_triangle_set build_texture_displacement_v2(const indexed_triangle_set
|
||||
// 0 means no simplification, i.e. Bake mode.
|
||||
const TextureBake::PipelineMode mode = settings.max_triangles > 0 ? TextureBake::PipelineMode::Export
|
||||
: TextureBake::PipelineMode::Bake;
|
||||
// Colour, when asked for. The sampler is built now so the simplification can see the colour
|
||||
// boundaries: a simplified triangle must not span two colours, or its one colour is wrong over
|
||||
// part of it (half a tile in the neighbour's colour, a tile edge that wanders).
|
||||
const bool want_color = color != nullptr && color->out_triangle != nullptr && bool(color->quantize);
|
||||
const ColorFieldSampler color_sampler =
|
||||
want_color ? make_combined_color_sampler(mesh, layers, facets_data, color->quantize, color->quantize_pure) : ColorFieldSampler{};
|
||||
//
|
||||
// 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.
|
||||
const TextureBake::ColorSampleFn color_sample =
|
||||
color_sampler ? TextureBake::ColorSampleFn([&color_sampler](const Vec3f &p, const Vec3f &n) {
|
||||
return color_sampler(p, n);
|
||||
})
|
||||
: TextureBake::ColorSampleFn{};
|
||||
// The pipeline works on `oriented`, whose winding was reversed above for a mirrored placement, so
|
||||
// the stages it records are wound the same way. Note where they start and turn the whole range
|
||||
// back afterwards, exactly as the result itself is turned back below.
|
||||
@@ -2048,7 +2274,7 @@ indexed_triangle_set build_texture_displacement_v2(const indexed_triangle_set
|
||||
[&progress](const char *, double f) {
|
||||
return !progress || progress(std::clamp(int(f * 100.0), 0, 99));
|
||||
},
|
||||
debug);
|
||||
debug, color_sample);
|
||||
if (debug != nullptr && flip_normals)
|
||||
debug->rebase(debug_mark, nullptr, /* flip_winding */ true);
|
||||
if (result.canceled || result.geometry.empty())
|
||||
@@ -2063,44 +2289,56 @@ indexed_triangle_set build_texture_displacement_v2(const indexed_triangle_set
|
||||
|
||||
// Colour, per output triangle. The topology is new, so unlike the classic path there is no base
|
||||
// triangle to inherit a colour from: each output triangle samples the colour stack at its own
|
||||
// centroid, and takes colour only where the base surface under it is painted - found by the nearest
|
||||
// base triangle, which is never more than the relief depth away. Then the same despeckle and
|
||||
// centroid, and takes colour only where the paint is - measured against the painted pieces, which
|
||||
// an output centroid is never further from than the relief depth. Then the same despeckle and
|
||||
// filament resolution as the classic path.
|
||||
if (color != nullptr && color->out_triangle != nullptr && bool(color->quantize)) {
|
||||
std::vector<uint8_t> out_color(out.indices.size(), 0);
|
||||
const ColorFieldSampler sampler = make_combined_color_sampler(mesh, layers, facets_data, color->quantize);
|
||||
if (want_color) {
|
||||
std::vector<uint8_t> out_color(out.indices.size(), 0);
|
||||
const ColorFieldSampler &sampler = color_sampler;
|
||||
if (sampler) {
|
||||
const bool all_painted = std::none_of(excluded.begin(), excluded.end(), [](uint8_t e) { return e != 0; });
|
||||
AABBTreeIndirect::Tree3f tree;
|
||||
if (!all_painted)
|
||||
tree = AABBTreeIndirect::build_aabb_tree_over_indexed_triangle_set(mesh.vertices, mesh.indices);
|
||||
float max_depth = 0.f;
|
||||
for (const TextureDisplacementLayer &layer : layers)
|
||||
max_depth = std::max(max_depth, std::abs(layer.depth_mm));
|
||||
const float relief_tol = max_depth + paint_tol;
|
||||
std::vector<int> palette(out.indices.size(), -1);
|
||||
tbb::parallel_for(tbb::blocked_range<size_t>(0, out.indices.size()), [&](const tbb::blocked_range<size_t> &r) {
|
||||
for (size_t i = r.begin(); i < r.end(); ++i) {
|
||||
const stl_triangle_vertex_indices &t = out.indices[i];
|
||||
const Vec3f &a = out.vertices[size_t(t[0])], &b = out.vertices[size_t(t[1])], &c = out.vertices[size_t(t[2])];
|
||||
const Vec3f centroid = (a + b + c) / 3.f;
|
||||
if (!all_painted) {
|
||||
size_t hit = 0;
|
||||
Vec3f hit_point;
|
||||
AABBTreeIndirect::squared_distance_to_indexed_triangle_set(mesh.vertices, mesh.indices, tree,
|
||||
centroid, hit, hit_point);
|
||||
if (hit >= excluded.size() || excluded[hit] != 0)
|
||||
continue;
|
||||
}
|
||||
Vec3f n = (b - a).cross(c - a);
|
||||
const float l = n.norm();
|
||||
n = (l > 0.f) ? Vec3f(n / l) : Vec3f::UnitZ();
|
||||
palette[i] = sampler(centroid, n);
|
||||
// Sample on the *base* surface under this face, not on the relief. The projection
|
||||
// is a function of position and normal, and the displacement has moved both: the
|
||||
// triplanar blend weights three axis planes by |n|^4, so a face tilted ~45 degrees
|
||||
// away from its base normal reads the image half through an unrelated plane. The
|
||||
// patch border is a ring of exactly such faces - the relief ramps to zero there -
|
||||
// which is the coloured fringe around the border, and the steep interior slopes
|
||||
// streak for the same reason. The classic path samples the base patch for this very
|
||||
// reason; this path was the inconsistent one.
|
||||
Vec3f foot = centroid, base_n = Vec3f::UnitZ();
|
||||
const float d2 = painted_closest(centroid, &foot, &base_n);
|
||||
if (!all_painted && d2 >= relief_tol * relief_tol)
|
||||
continue;
|
||||
palette[i] = sampler(foot, base_n);
|
||||
}
|
||||
});
|
||||
despeckle_triangle_colors(out, palette, color->despeckle_passes);
|
||||
// The despeckle filter is for a fine, uniform mesh, where one facet flipping colour is
|
||||
// noise. A simplified mesh is neither: its triangles are as large as the colour regions
|
||||
// themselves and already end on the colour boundaries, so a majority vote among three
|
||||
// neighbours would repaint whole features. Bake mode (no simplification) keeps it.
|
||||
const bool simplified = result.face_parent_id.empty();
|
||||
despeckle_triangle_colors(out, palette, simplified ? 0 : color->despeckle_passes);
|
||||
merge_small_color_regions(out, palette, color->min_color_region_mm2);
|
||||
for (size_t i = 0; i < out.indices.size(); ++i) {
|
||||
if (palette[i] < 0)
|
||||
continue;
|
||||
const stl_triangle_vertex_indices &t = out.indices[i];
|
||||
const Vec3f centroid = (out.vertices[size_t(t[0])] + out.vertices[size_t(t[1])] + out.vertices[size_t(t[2])]) / 3.f;
|
||||
const int filament = color->resolve ? color->resolve(palette[i], centroid) : palette[i];
|
||||
const Vec3f &a = out.vertices[size_t(t[0])], &b = out.vertices[size_t(t[1])], &c = out.vertices[size_t(t[2])];
|
||||
const Vec3f centroid = (a + b + c) / 3.f;
|
||||
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];
|
||||
if (filament >= 0)
|
||||
out_color[i] = uint8_t(std::min(filament + 1, 255));
|
||||
}
|
||||
@@ -2278,6 +2516,10 @@ static indexed_triangle_set build_texture_displacement_in_place(
|
||||
selector_dirty = true;
|
||||
|
||||
const bool color_this_layer = want_color && layer->color_enabled;
|
||||
// A flat-colour image is matched against the filaments alone (see TextureColorRequest).
|
||||
const ColorQuantizeFn &layer_quantize =
|
||||
(color_this_layer && color->quantize_pure && analyze_texture_detail(*layer).flat_colors) ? color->quantize_pure
|
||||
: color->quantize;
|
||||
std::vector<int> patch_source; // sub-triangle -> base mesh triangle, only built when colouring
|
||||
const indexed_triangle_set patch =
|
||||
selector.get_facets_strict(EnforcerBlockerType::ENFORCER, color_this_layer ? &patch_source : nullptr);
|
||||
@@ -2302,34 +2544,11 @@ static indexed_triangle_set build_texture_displacement_in_place(
|
||||
}
|
||||
const bool pin_boundary = !options.displace_border;
|
||||
|
||||
// Only the Cylindrical/Spherical methods need these; Triplanar blends each vertex's own
|
||||
// normal and LSCM solves the patch globally.
|
||||
Vec3f average_normal = Vec3f::Zero();
|
||||
Vec3f patch_centroid = Vec3f::Zero();
|
||||
int patch_vertex_count = 0;
|
||||
for (const stl_triangle_vertex_indices &tri : patch.indices)
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
const int vi = tri[i];
|
||||
patch_centroid += patch.vertices[size_t(vi)];
|
||||
++patch_vertex_count;
|
||||
// A brush stroke that split a triangle appends new vertices past the base mesh's own
|
||||
// (see the get_facets_strict() note above); vertex_normals is sized to the base mesh,
|
||||
// so those split indices must be skipped here or this reads out of bounds. The main
|
||||
// displacement loop below guards the same way.
|
||||
if (vi < int(vertex_normals.size()))
|
||||
average_normal += vertex_normals[size_t(vi)];
|
||||
}
|
||||
average_normal = (average_normal.norm() > 1e-8f) ? Vec3f(average_normal.normalized()) : Vec3f::UnitZ();
|
||||
patch_centroid = (patch_vertex_count > 0) ? Vec3f(patch_centroid / float(patch_vertex_count)) : Vec3f::Zero();
|
||||
|
||||
// Cylinder axis auto-picked as the world axis *least* aligned with the average normal
|
||||
// (perpendicular to the outward radial normal, as a cylinder's own axis would be).
|
||||
Vec3f patch_axis = Vec3f::UnitZ();
|
||||
const Vec3f an = average_normal.cwiseAbs();
|
||||
if (an.x() <= an.y() && an.x() <= an.z())
|
||||
patch_axis = Vec3f::UnitX();
|
||||
else if (an.y() <= an.x() && an.y() <= an.z())
|
||||
patch_axis = Vec3f::UnitY();
|
||||
// Only the Cylindrical/Spherical methods need the centroid and axis; Triplanar blends each
|
||||
// vertex's own normal and LSCM solves the patch globally. average_normal is also the fallback
|
||||
// normal the colour pass below uses for a degenerate triangle.
|
||||
Vec3f average_normal, patch_centroid, patch_axis;
|
||||
texture_displacement_patch_frame(patch, vertex_normals, patch_centroid, patch_axis, average_normal);
|
||||
|
||||
// A real unwrap of the whole patch, computed once here rather than per vertex - it is a
|
||||
// per-chart solve over the whole patch, not a per-point formula. Cached, so repeating this
|
||||
@@ -2337,6 +2556,15 @@ static indexed_triangle_set build_texture_displacement_in_place(
|
||||
const std::vector<Vec2f> lscm_uvs = (layer->projection_method == TextureProjectionMethod::LSCM) ?
|
||||
compute_lscm_uvs(patch, *layer) :
|
||||
std::vector<Vec2f>{};
|
||||
// The colour pass below samples per *triangle*, so it takes the per-corner unwrap instead: the
|
||||
// per-vertex collapse above would hand a triangle at a seam the island layout did not join its
|
||||
// neighbour's placement, painting one triangle per face from the wrong part of the texture.
|
||||
// (The displacement itself stays on lscm_uvs - a vertex has one position, so one height.)
|
||||
const std::vector<Vec2f> lscm_corner_uvs = (layer->projection_method == TextureProjectionMethod::LSCM) ?
|
||||
compute_lscm_corner_uvs(patch, *layer) :
|
||||
std::vector<Vec2f>{};
|
||||
const bool corner_uv_ok = !lscm_corner_uvs.empty() &&
|
||||
lscm_corner_uvs.size() == patch.indices.size() * 3;
|
||||
|
||||
// Colour, if this layer carries any. Area-weighted over each base triangle's *painted* part,
|
||||
// so a triangle the brush only clipped a corner off takes the colour of that corner rather
|
||||
@@ -2371,7 +2599,11 @@ static indexed_triangle_set build_texture_displacement_in_place(
|
||||
const int vi = t[k];
|
||||
if (vi < int(vertex_normals.size()))
|
||||
n += vertex_normals[size_t(vi)];
|
||||
if (have_uv && size_t(vi) < lscm_uvs.size())
|
||||
if (!have_uv)
|
||||
continue;
|
||||
if (corner_uv_ok)
|
||||
uv += lscm_corner_uvs[j * 3 + size_t(k)];
|
||||
else if (size_t(vi) < lscm_uvs.size())
|
||||
uv += lscm_uvs[size_t(vi)];
|
||||
else
|
||||
have_uv = false;
|
||||
@@ -2388,7 +2620,7 @@ static indexed_triangle_set build_texture_displacement_in_place(
|
||||
}
|
||||
for (size_t i = 0; i < mesh.indices.size(); ++i)
|
||||
if (sum_area[i] > 0.f) {
|
||||
const int idx = color->quantize(sum[i] / sum_area[i]);
|
||||
const int idx = layer_quantize(sum[i] / sum_area[i]);
|
||||
// A quantizer that declines this colour leaves whatever a lower layer put
|
||||
// there, rather than punching a hole in it.
|
||||
if (idx >= 0)
|
||||
@@ -2548,15 +2780,19 @@ static indexed_triangle_set build_texture_displacement_in_place(
|
||||
// to keep, and interleaving before the filter would have the filter treat two halves of one
|
||||
// blended colour as a disagreement.
|
||||
despeckle_triangle_colors(mesh, triangle_palette, color->despeckle_passes);
|
||||
merge_small_color_regions(mesh, triangle_palette, color->min_color_region_mm2);
|
||||
|
||||
std::vector<uint8_t> out_color(mesh.indices.size(), 0);
|
||||
for (size_t i = 0; i < mesh.indices.size(); ++i) {
|
||||
if (triangle_palette[i] < 0)
|
||||
continue;
|
||||
const stl_triangle_vertex_indices &t = mesh.indices[i];
|
||||
const Vec3f centroid = (mesh.vertices[size_t(t[0])] + mesh.vertices[size_t(t[1])] +
|
||||
mesh.vertices[size_t(t[2])]) / 3.f;
|
||||
const int filament = color->resolve ? color->resolve(triangle_palette[i], centroid)
|
||||
const Vec3f &a = mesh.vertices[size_t(t[0])], &b = mesh.vertices[size_t(t[1])], &c = mesh.vertices[size_t(t[2])];
|
||||
const Vec3f centroid = (a + b + c) / 3.f;
|
||||
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];
|
||||
if (filament >= 0)
|
||||
out_color[i] = uint8_t(std::min(filament + 1, 255));
|
||||
@@ -2615,6 +2851,32 @@ indexed_triangle_set build_texture_displacement(const indexed_triangle_set
|
||||
return out;
|
||||
}
|
||||
|
||||
void texture_displacement_patch_frame(const indexed_triangle_set &patch, const std::vector<Vec3f> &vertex_normals,
|
||||
Vec3f ¢er, Vec3f &axis, Vec3f &average_normal)
|
||||
{
|
||||
Vec3f normal_sum = Vec3f::Zero();
|
||||
Vec3f centroid_sum = Vec3f::Zero();
|
||||
int count = 0;
|
||||
for (const stl_triangle_vertex_indices &tri : patch.indices)
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
const int vi = tri[i];
|
||||
centroid_sum += patch.vertices[size_t(vi)];
|
||||
++count;
|
||||
// A brush stroke that split a triangle appends new vertices past the base mesh's own, and
|
||||
// vertex_normals is sized to the base mesh, so those indices must be skipped here.
|
||||
if (vi < int(vertex_normals.size()))
|
||||
normal_sum += vertex_normals[size_t(vi)];
|
||||
}
|
||||
average_normal = (normal_sum.norm() > 1e-8f) ? Vec3f(normal_sum.normalized()) : Vec3f::UnitZ();
|
||||
center = (count > 0) ? Vec3f(centroid_sum / float(count)) : Vec3f::Zero();
|
||||
|
||||
// The world axis least aligned with the average normal - perpendicular to the outward radial
|
||||
// normal, as a cylinder's own axis would be.
|
||||
const Vec3f an = average_normal.cwiseAbs();
|
||||
axis = (an.x() <= an.y() && an.x() <= an.z()) ? Vec3f::UnitX() :
|
||||
(an.y() <= an.x() && an.y() <= an.z()) ? Vec3f::UnitY() : Vec3f::UnitZ();
|
||||
}
|
||||
|
||||
Transform3d texture_displacement_bake_frame(const Transform3d &volume_to_world)
|
||||
{
|
||||
// World orientation and scale, but the origin moved to where the volume's own origin sits: the
|
||||
@@ -2704,11 +2966,89 @@ void smooth_mesh_vertices(indexed_triangle_set &mesh, const std::vector<uint8_t>
|
||||
namespace {
|
||||
// One decoded texture + placement per sampleable layer, in blend (slot) order. Held by shared_ptr so
|
||||
// the returned closure owns it for as long as the subdivider keeps calling back.
|
||||
// An unwrap turned into something a *point* sampler can use. LSCM has no formula from position to
|
||||
// uv - it is a per-triangle map - so a point is placed on the painted patch (the nearest patch
|
||||
// triangle, and its barycentric coordinates there) and the uv is interpolated from that triangle's own
|
||||
// per-corner uvs. Exact for a point on the base surface, which is where both samplers are queried: the
|
||||
// displacement samples refined positions before moving them, and the colour pass samples the foot
|
||||
// point on the painted pieces.
|
||||
struct LscmLookup {
|
||||
indexed_triangle_set patch; // the layer's painted patch, as the unwrap was solved on
|
||||
AABBTreeIndirect::Tree3f tree;
|
||||
std::vector<Vec2f> corner; // compute_lscm_corner_uvs(patch, layer)
|
||||
|
||||
// False when `pos` is not on this layer's patch (farther than `tol`): there is no uv there, so the
|
||||
// layer contributes nothing - the same as a non-tiled texture outside its placement.
|
||||
bool uv_at(const Vec3f &pos, float tol, Vec2f &uv) const
|
||||
{
|
||||
size_t hit = 0;
|
||||
Vec3f foot;
|
||||
const float d2 = AABBTreeIndirect::squared_distance_to_indexed_triangle_set(patch.vertices, patch.indices, tree,
|
||||
pos, hit, foot);
|
||||
if (d2 < 0.f || d2 > tol * tol || hit >= patch.indices.size())
|
||||
return false;
|
||||
const stl_triangle_vertex_indices &t = patch.indices[hit];
|
||||
const Vec3f &a = patch.vertices[size_t(t[0])], &b = patch.vertices[size_t(t[1])], &c = patch.vertices[size_t(t[2])];
|
||||
const Vec3f e0 = b - a, e1 = c - a, ep = foot - a;
|
||||
const float d00 = e0.dot(e0), d01 = e0.dot(e1), d11 = e1.dot(e1), dp0 = ep.dot(e0), dp1 = ep.dot(e1);
|
||||
const float den = d00 * d11 - d01 * d01;
|
||||
float w1 = 1.f / 3.f, w2 = 1.f / 3.f; // a degenerate triangle takes its centroid's uv
|
||||
if (std::abs(den) > 1e-20f) {
|
||||
w1 = (d11 * dp0 - d01 * dp1) / den;
|
||||
w2 = (d00 * dp1 - d01 * dp0) / den;
|
||||
}
|
||||
const Vec2f *c3 = &corner[hit * 3];
|
||||
uv = (1.f - w1 - w2) * c3[0] + w1 * c3[1] + w2 * c3[2];
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
// A layer's painted patch as a point-in-region test. Every layer is sampled on its own paint only - the
|
||||
// analytic projections included: unlike an unwrap they are defined everywhere, so without this every layer's
|
||||
// relief was stacked over every other layer's painted area, and the top layer's texture showed on all of them.
|
||||
struct PatchRegion {
|
||||
indexed_triangle_set patch;
|
||||
AABBTreeIndirect::Tree3f tree;
|
||||
|
||||
bool contains(const Vec3f &pos, float tol) const
|
||||
{
|
||||
size_t hit = 0;
|
||||
Vec3f foot;
|
||||
const float d2 = AABBTreeIndirect::squared_distance_to_indexed_triangle_set(patch.vertices, patch.indices, tree,
|
||||
pos, hit, foot);
|
||||
return d2 >= 0.f && d2 <= tol * tol;
|
||||
}
|
||||
};
|
||||
|
||||
struct PreparedLayer {
|
||||
DecodedHeightTexture tex;
|
||||
TextureDisplacementLayer layer; // a copy of the params (depth/tiling/rotation/offset/blend/...)
|
||||
Vec3f center; // patch centroid, for Cylindrical/Spherical
|
||||
Vec3f axis; // cylinder axis, for Cylindrical
|
||||
// Unwrap layers only; null when the unwrap failed, in which case sampling falls through to the
|
||||
// layer's analytic fallback exactly as build_texture_displacement()'s classic path does.
|
||||
std::shared_ptr<const LscmLookup> lscm;
|
||||
// The painted patch of a layer without an unwrap lookup (the unwrap's own lookup already stops at its patch).
|
||||
// Null when the paint covers the whole mesh, where every point is on it.
|
||||
std::shared_ptr<const PatchRegion> region;
|
||||
|
||||
// The uv to hand sample_layer_height()/sample_layer_color(): nullptr for every analytic projection
|
||||
// (they project `pos` themselves). False means `pos` is off this layer's paint and the layer must be
|
||||
// skipped.
|
||||
bool lscm_uv(const Vec3f &pos, Vec2f &uv, const Vec2f *&out) const
|
||||
{
|
||||
out = nullptr;
|
||||
// Queries lie on the base surface, so anything beyond a hair is off this layer's patch.
|
||||
constexpr float ON_PATCH_TOL = 0.05f;
|
||||
if (region && !region->contains(pos, ON_PATCH_TOL))
|
||||
return false;
|
||||
if (!lscm)
|
||||
return true;
|
||||
if (!lscm->uv_at(pos, ON_PATCH_TOL, uv))
|
||||
return false;
|
||||
out = &uv;
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
// Shared by both point samplers, so the height field and the colour field can never disagree about
|
||||
@@ -2731,10 +3071,12 @@ std::shared_ptr<std::vector<PreparedLayer>> prepare_sampleable_layers(
|
||||
|
||||
const std::vector<Vec3f> vertex_normals = texture_displacement_vertex_normals(base_mesh);
|
||||
const TriangleMesh selector_mesh(base_mesh);
|
||||
const float mesh_area = area_3d(base_mesh);
|
||||
|
||||
for (const TextureDisplacementLayer *layer : ordered) {
|
||||
if (layer->projection_method == TextureProjectionMethod::LSCM)
|
||||
continue; // no per-point UV -> not sampleable here (caller falls back to uniform for these)
|
||||
// Unwrap layers used to be skipped here ("no per-point UV"). That made the default pipeline
|
||||
// bake an unwrap layer as nothing at all - and since the job then clears the baked layers'
|
||||
// paint, the painted region simply vanished. They get an LscmLookup below instead.
|
||||
if (need_color && !layer->color_enabled)
|
||||
continue;
|
||||
const TriangleSelector::TriangleSplittingData &data = facets_data[size_t(layer->slot)];
|
||||
@@ -2750,30 +3092,34 @@ std::shared_ptr<std::vector<PreparedLayer>> prepare_sampleable_layers(
|
||||
if (patch.indices.empty())
|
||||
continue;
|
||||
|
||||
// Patch centroid + cylinder axis, computed exactly as build_texture_displacement() does, so a
|
||||
// Patch centroid + cylinder axis, shared with build_texture_displacement() so a
|
||||
// Cylindrical/Spherical layer's detach criterion matches the geometry the bake will produce.
|
||||
Vec3f average_normal = Vec3f::Zero();
|
||||
Vec3f centroid = Vec3f::Zero();
|
||||
int count = 0;
|
||||
for (const stl_triangle_vertex_indices &tri : patch.indices)
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
const int vi = tri[i];
|
||||
centroid += patch.vertices[size_t(vi)];
|
||||
++count;
|
||||
if (vi < int(vertex_normals.size()))
|
||||
average_normal += vertex_normals[size_t(vi)];
|
||||
Vec3f centroid, axis, average_normal;
|
||||
texture_displacement_patch_frame(patch, vertex_normals, centroid, axis, average_normal);
|
||||
|
||||
std::shared_ptr<const LscmLookup> lscm;
|
||||
if (layer->projection_method == TextureProjectionMethod::LSCM) {
|
||||
// Solved on the very patch the classic path and the GUI solve it on (same geometry, seam
|
||||
// angle and edges), so it hits the unwrap cache and lands exactly where the UV editor
|
||||
// shows it, hand-placed islands and UV edits included.
|
||||
auto l = std::make_shared<LscmLookup>();
|
||||
l->corner = compute_lscm_corner_uvs(patch, *layer);
|
||||
if (l->corner.size() == patch.indices.size() * 3) {
|
||||
l->patch = patch;
|
||||
l->tree = AABBTreeIndirect::build_aabb_tree_over_indexed_triangle_set(l->patch.vertices, l->patch.indices);
|
||||
lscm = std::move(l);
|
||||
}
|
||||
average_normal = (average_normal.norm() > 1e-8f) ? Vec3f(average_normal.normalized()) : Vec3f::UnitZ();
|
||||
centroid = (count > 0) ? Vec3f(centroid / float(count)) : Vec3f::Zero();
|
||||
}
|
||||
|
||||
Vec3f axis = Vec3f::UnitZ();
|
||||
const Vec3f an = average_normal.cwiseAbs();
|
||||
if (an.x() <= an.y() && an.x() <= an.z())
|
||||
axis = Vec3f::UnitX();
|
||||
else if (an.y() <= an.x() && an.y() <= an.z())
|
||||
axis = Vec3f::UnitY();
|
||||
std::shared_ptr<const PatchRegion> region;
|
||||
if (!lscm && area_3d(patch) < 0.9999f * mesh_area) {
|
||||
auto r = std::make_shared<PatchRegion>();
|
||||
r->patch = patch;
|
||||
r->tree = AABBTreeIndirect::build_aabb_tree_over_indexed_triangle_set(r->patch.vertices, r->patch.indices);
|
||||
region = std::move(r);
|
||||
}
|
||||
|
||||
prepared->push_back({ tex, *layer, centroid, axis });
|
||||
prepared->push_back({ tex, *layer, centroid, axis, std::move(lscm), std::move(region) });
|
||||
}
|
||||
return prepared;
|
||||
}
|
||||
@@ -2782,22 +3128,34 @@ std::shared_ptr<std::vector<PreparedLayer>> prepare_sampleable_layers(
|
||||
ColorFieldSampler make_combined_color_sampler(const indexed_triangle_set &base_mesh,
|
||||
const std::vector<TextureDisplacementLayer> &layers,
|
||||
const TextureDisplacementFacetsData &facets_data,
|
||||
ColorQuantizeFn quantize)
|
||||
ColorQuantizeFn quantize,
|
||||
ColorQuantizeFn quantize_pure)
|
||||
{
|
||||
if (!quantize)
|
||||
return nullptr;
|
||||
auto prepared = prepare_sampleable_layers(base_mesh, layers, facets_data, /* need_color */ true);
|
||||
if (prepared->empty())
|
||||
return nullptr;
|
||||
// Per layer: a flat-colour image is matched against the filaments alone, when that quantizer
|
||||
// was supplied; anything else may use the mixes. Decided once here, not per sample.
|
||||
auto pure = std::make_shared<std::vector<uint8_t>>(prepared->size(), 0);
|
||||
if (quantize_pure)
|
||||
for (size_t i = 0; i < prepared->size(); ++i)
|
||||
(*pure)[i] = analyze_texture_detail((*prepared)[i].layer).flat_colors ? 1 : 0;
|
||||
|
||||
return [prepared, quantize = std::move(quantize)](const Vec3f &pos, const Vec3f &normal) -> int {
|
||||
return [prepared, pure, quantize = std::move(quantize), quantize_pure = std::move(quantize_pure)](const Vec3f &pos, const Vec3f &normal) -> int {
|
||||
// Last one wins: `prepared` is in ascending slot order and the bake lets a higher layer
|
||||
// overwrite a lower one's colour, so the sampler has to resolve overlaps the same way.
|
||||
int result = -1;
|
||||
for (const PreparedLayer &p : *prepared) {
|
||||
for (size_t i = 0; i < prepared->size(); ++i) {
|
||||
const PreparedLayer &p = (*prepared)[i];
|
||||
Vec2f uv;
|
||||
const Vec2f *lscm_uv = nullptr;
|
||||
if (!p.lscm_uv(pos, uv, lscm_uv))
|
||||
continue;
|
||||
Vec3f rgb;
|
||||
if (sample_layer_color(p.tex, p.layer, pos, normal, rgb, p.center, p.axis, nullptr))
|
||||
if (const int idx = quantize(rgb); idx >= 0)
|
||||
if (sample_layer_color(p.tex, p.layer, pos, normal, rgb, p.center, p.axis, lscm_uv))
|
||||
if (const int idx = ((*pure)[i] ? quantize_pure : quantize)(rgb); idx >= 0)
|
||||
result = idx;
|
||||
}
|
||||
return result;
|
||||
@@ -2816,7 +3174,11 @@ HeightFieldSampler make_combined_displacement_sampler(const indexed_triangle_set
|
||||
float total = 0.f;
|
||||
bool any = false;
|
||||
for (const PreparedLayer &p : *prepared) {
|
||||
const float h = sample_layer_height(p.tex, p.layer, pos, normal, p.center, p.axis, nullptr);
|
||||
Vec2f uv;
|
||||
const Vec2f *lscm_uv = nullptr;
|
||||
if (!p.lscm_uv(pos, uv, lscm_uv))
|
||||
continue; // off this unwrap layer's patch: no uv, so no contribution
|
||||
const float h = sample_layer_height(p.tex, p.layer, pos, normal, p.center, p.axis, lscm_uv);
|
||||
const float sign = p.layer.invert ? -1.f : 1.f;
|
||||
const float signed_h = (h - p.layer.midlevel) * p.layer.depth_mm * sign;
|
||||
// The first (lowest) sampleable layer folds additively; the rest use their own blend mode -
|
||||
@@ -3264,10 +3626,10 @@ indexed_triangle_set subdivide_mesh_adaptive(const indexed_triangle_set &mesh,
|
||||
p = std::max(p, ll / color_sq);
|
||||
}
|
||||
// The band straddling the paint's edge, refined by plain edge length. Deliberately *not* run
|
||||
// through detail_error(): outside the paint the sampler still reports full relief (it has no
|
||||
// per-point paint test), so the chord test there would chase texture detail on a surface the
|
||||
// bake is going to leave flat. Length alone is what this band needs - the error it is fixing
|
||||
// is the size of the triangles spanning the displacement step, not the curvature of anything.
|
||||
// through detail_error(): the sampler reports no relief off the paint, so across its edge the
|
||||
// chord test sees a step and would chase it down to the length floor. Length alone is what this
|
||||
// band needs - the error it is fixing is the size of the triangles spanning the displacement
|
||||
// step, not the curvature of anything.
|
||||
if ((flags & REFINE_BORDER) && border_sq > 0.f)
|
||||
p = std::max(p, ll / border_sq);
|
||||
return p;
|
||||
|
||||
@@ -333,6 +333,10 @@ enum class ColorMixMode : int
|
||||
// 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
|
||||
@@ -396,9 +400,11 @@ struct TextureDisplacementOptions
|
||||
// printer will realise the colours, not about which image they came from.
|
||||
|
||||
// Interleave pairs of filaments to get colours between them - so four loaded filaments offer far
|
||||
// more than four colours. Off means every triangle takes one of the loaded filaments exactly.
|
||||
// more than four colours. Whether a given layer's colours actually use mixes is decided from its
|
||||
// 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::ZBands;
|
||||
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.
|
||||
@@ -423,17 +429,18 @@ struct TextureDetail
|
||||
float mean_gradient = 0.f;
|
||||
float sharp_fraction = 0.f;
|
||||
float pixels_per_edge = 4.f;
|
||||
// How much of the image its eight most common colours cover (8 levels per channel), and the
|
||||
// verdict: a "flat-colour" image (tiles, logos, camouflage) whose colours should each print in a
|
||||
// single filament, versus a photograph or gradient where interleaved filament mixes pay off.
|
||||
float flat_share = 0.f;
|
||||
bool flat_colors = false;
|
||||
};
|
||||
TextureDetail analyze_texture_detail(const TextureDisplacementLayer &layer);
|
||||
|
||||
// The default pipeline's automatic resolution: the refinement edge and the simplification budget the
|
||||
// texture and the model call for, when the options leave them at "auto".
|
||||
// - edge = texel size (tile / image width, in world mm, over the finest layer) x pixels per edge,
|
||||
// but no finer than keeps the refinement under a 12 M triangle cap for this surface area, clamped
|
||||
// to [0.05 mm, min(5 mm, diagonal / 50)] and rounded up to 0.01 mm;
|
||||
// - budget = the triangle count an edge of that texel size needs over the surface, scaled by the
|
||||
// relief depth (a gentle relief needs fewer), stepped to 10 k and clamped to [10 k, 2000 k].
|
||||
// `edge_mm` is 0 when no layer has a usable texture.
|
||||
// The default pipeline's automatic resolution and budget, when the options leave them at "auto":
|
||||
// bumpmesh.com's defaults - edge = the model's world-space diagonal / 250, clamped to [0.05, 5] mm and
|
||||
// rounded up to 0.01; budget 750 k. The texel size of the finest layer and its sharpness class are
|
||||
// reported alongside for the panel. `edge_mm` is 0 for an empty mesh.
|
||||
struct V2Resolution
|
||||
{
|
||||
float edge_mm = 0.f;
|
||||
@@ -510,7 +517,7 @@ using ColorQuantizeFn = std::function<int(const Vec3f &)>;
|
||||
// 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<int(int palette_index, const Vec3f &pos)>;
|
||||
using ColorResolveFn = std::function<int(int palette_index, const Vec3f &pos, const Vec3f &normal)>;
|
||||
|
||||
// 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.
|
||||
@@ -529,6 +536,7 @@ struct PrintableColor
|
||||
struct TextureColorSettings
|
||||
{
|
||||
std::vector<PrintableColor> palette;
|
||||
std::vector<PrintableColor> 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
|
||||
@@ -623,7 +631,12 @@ struct PatchUnwrap
|
||||
std::vector<Vec2f> uvs; // one per unwrapped vertex, in mm
|
||||
std::vector<int> source_vertex; // unwrapped vertex -> index into patch.vertices
|
||||
std::vector<int> vertex_chart; // unwrapped vertex -> chart (island) id
|
||||
std::vector<stl_triangle_vertex_indices> indices; // patch triangles, re-indexed into `uvs`
|
||||
// The patch's triangles re-indexed into `uvs` - but *grouped by chart*, not left in the patch's
|
||||
// own order: the charts are flattened one at a time and then concatenated. `source_face` is the
|
||||
// map back, so anything that needs UVs per triangle corner (as opposed to per vertex) can place
|
||||
// them against its own triangle list. See compute_lscm_corner_uvs().
|
||||
std::vector<stl_triangle_vertex_indices> indices;
|
||||
std::vector<int> source_face; // unwrapped triangle -> index into patch.indices
|
||||
// Per chart, the centroid of its uvs - the point a TextureIsland's rotation turns about.
|
||||
std::vector<Vec2f> chart_centroid;
|
||||
// Edges belonging to exactly one triangle: the outline of each island. Indices into `uvs`. This
|
||||
@@ -675,17 +688,30 @@ bool join_chart_placement(const PatchUnwrap &unwrap, const std::vector<TextureIs
|
||||
PatchUnwrap compute_patch_unwrap(const indexed_triangle_set &patch, float seam_angle_deg = LSCM_DEFAULT_SEAM_ANGLE_DEG,
|
||||
float padding_mm = -1.f, const std::vector<std::pair<int, int>> &seam_edges = {});
|
||||
|
||||
// One UV per patch vertex, for displacement. Displacement is inherently per-vertex - a vertex has
|
||||
// exactly one position, so it can only be pushed out by one height - which means a seam vertex has
|
||||
// to settle on a single one of its charts' UVs (the first, arbitrarily). That is not a compromise
|
||||
// in the result: the surface stays watertight either way, since neighbouring vertices each move
|
||||
// along their own normals and nothing depends on the UVs agreeing across the seam. It is only the
|
||||
// *display* in the UV editor that needs the duplicated-vertex form above.
|
||||
// One UV per patch vertex, **for displacement only**. Displacement is inherently per-vertex - a
|
||||
// vertex has exactly one position, so it can only be pushed out by one height - which means a seam
|
||||
// vertex has to settle on a single one of its charts' UVs (the first, arbitrarily). That is not a
|
||||
// compromise in the result: the surface stays watertight either way, since neighbouring vertices
|
||||
// each move along their own normals and nothing depends on the UVs agreeing across the seam.
|
||||
//
|
||||
// Anything that samples or draws per *triangle* must use compute_lscm_corner_uvs() instead. This
|
||||
// collapse is wrong for those: a triangle at a seam that the island layout did not join gets handed
|
||||
// a neighbouring island's placement, which showed up as a single skewed triangle per face and as
|
||||
// every island's texture following the lowest-numbered island when it was dragged.
|
||||
//
|
||||
// Returns an empty vector if the patch has no triangles. Takes the whole layer because it applies
|
||||
// both the layer's seam angle and its hand-placed islands.
|
||||
std::vector<Vec2f> compute_lscm_uvs(const indexed_triangle_set &patch, const TextureDisplacementLayer &layer);
|
||||
|
||||
// Three UVs per patch triangle (corner 0, 1, 2 of triangle i at index 3i..3i+2), in the patch's own
|
||||
// triangle order. Unlike compute_lscm_uvs() this keeps a seam vertex's separate per-chart copies: a
|
||||
// triangle belongs to exactly one chart and is given that chart's UVs, which is what every consumer
|
||||
// that works per triangle rather than per vertex needs - the fast preview's flat mesh, the checker
|
||||
// overlay and the bake's per-facet colour.
|
||||
//
|
||||
// Returns an empty vector if the patch has no triangles or the unwrap carries no source_face map.
|
||||
std::vector<Vec2f> compute_lscm_corner_uvs(const indexed_triangle_set &patch, const TextureDisplacementLayer &layer);
|
||||
|
||||
// The TextureDisplacementLayer::lscm_uv_overrides key for one unwrapped vertex (an index into PatchUnwrap::uvs).
|
||||
inline int lscm_uv_override_key(int unwrapped_vertex) { return -(unwrapped_vertex + 1); }
|
||||
|
||||
@@ -749,6 +775,10 @@ struct TextureColorRequest
|
||||
// RGB -> palette index. Supplied by the GUI, which owns both the perceptual matching and the list
|
||||
// of filaments actually loaded (see ColorQuantizeFn).
|
||||
ColorQuantizeFn quantize;
|
||||
// The same over the loaded filaments alone, no mixes. Optional; when given, a layer whose image is
|
||||
// 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;
|
||||
@@ -760,6 +790,11 @@ struct TextureColorRequest
|
||||
// its edge neighbours removes exactly that, and leaves any feature wider than a facet alone. 0
|
||||
// turns it off.
|
||||
int despeckle_passes = 0;
|
||||
// After the despeckle: connected patches of one colour smaller than this (mm^2) are recoloured
|
||||
// to whatever borders them most - see merge_small_color_regions(). The despeckle only reaches
|
||||
// single facets; an image detail a few facets wide still leaves thousands of pinhead islands
|
||||
// that the slicer's multi-material segmentation cannot digest. 0 turns it off.
|
||||
float min_color_region_mm2 = 0.5f;
|
||||
// Filled per *base mesh* triangle (the bake is topology-preserving, so this indexes the returned
|
||||
// mesh too): the quantize callback's index plus one, or 0 for "this triangle takes no colour from
|
||||
// the texture". The +1 is not arbitrary - it lines up with EnforcerBlockerType, where 0 is NONE
|
||||
@@ -767,6 +802,15 @@ struct TextureColorRequest
|
||||
// straight to a TriangleSelector without a second mapping table.
|
||||
std::vector<uint8_t> *out_triangle = nullptr;
|
||||
};
|
||||
|
||||
// Recolours connected patches of one colour whose area is under `min_area_mm2` to the colour that
|
||||
// borders them most (by shared edge length). Colour is per triangle, -1 = none (never merged into,
|
||||
// never merged away). Removes the confetti a detailed image leaves on a fine mesh - thousands of
|
||||
// one-facet zones, which the slicer's multi-material segmentation cannot digest. Patches are
|
||||
// processed smallest-first, reading their neighbours' current colour, so a chain of tiny islands
|
||||
// collapses into its surroundings rather than into each other.
|
||||
void merge_small_color_regions(const indexed_triangle_set &mesh, std::vector<int> &color, float min_area_mm2);
|
||||
|
||||
// Where the volume sits on the plate: its instance transform times its own volume transform, i.e.
|
||||
// mesh coordinates -> world millimetres.
|
||||
//
|
||||
@@ -796,6 +840,22 @@ Transform3d texture_displacement_volume_to_world(const ModelVolume &volume);
|
||||
// removed, i.e. world orientation and scale about the volume's own origin. See build_texture_displacement().
|
||||
Transform3d texture_displacement_bake_frame(const Transform3d &volume_to_world);
|
||||
|
||||
// Area-weighted vertex normals of `its` - the directions the bake both projects and displaces along.
|
||||
std::vector<Vec3f> texture_displacement_vertex_normals(const indexed_triangle_set &its);
|
||||
|
||||
// The frame the Cylindrical and Spherical projections wrap around: `patch`'s triangle-corner centroid,
|
||||
// the world axis *least* aligned with the average of `vertex_normals` over those corners (a cylinder's
|
||||
// own axis is perpendicular to its outward radial normal), and that average normal itself.
|
||||
//
|
||||
// Results come out in whatever frame `patch` is given in. The bake calls this with the patch already in
|
||||
// the bake frame (see texture_displacement_bake_frame()), so a preview that wants to reproduce the
|
||||
// bake's projection must too, or it wraps the texture around a different centre. Corners past the end
|
||||
// of `vertex_normals` - the ones a brush stroke appended - contribute to the centroid but carry no
|
||||
// normal, exactly as the bake's own loops skip them.
|
||||
void texture_displacement_patch_frame(const indexed_triangle_set &patch,
|
||||
const std::vector<Vec3f> &vertex_normals,
|
||||
Vec3f ¢er, Vec3f &axis, Vec3f &average_normal);
|
||||
|
||||
// Convenience overload for main-thread callers: extracts the mesh/layers/paint data/options from
|
||||
// `volume` and forwards to the overload above.
|
||||
indexed_triangle_set build_texture_displacement(const ModelVolume &volume);
|
||||
@@ -844,7 +904,8 @@ using ColorFieldSampler = std::function<int(const Vec3f &pos, const Vec3f &norma
|
||||
ColorFieldSampler make_combined_color_sampler(const indexed_triangle_set &base_mesh,
|
||||
const std::vector<TextureDisplacementLayer> &layers,
|
||||
const TextureDisplacementFacetsData &facets_data,
|
||||
ColorQuantizeFn quantize);
|
||||
ColorQuantizeFn quantize,
|
||||
ColorQuantizeFn quantize_pure = nullptr);
|
||||
|
||||
HeightFieldSampler make_combined_displacement_sampler(const indexed_triangle_set &base_mesh,
|
||||
const std::vector<TextureDisplacementLayer> &layers,
|
||||
|
||||
@@ -224,6 +224,9 @@ constexpr int PALETTE_LUT_EDGE = 24;
|
||||
|
||||
// Ceiling on the printable palette, which bounds that fill cost (and the shader's uniform array).
|
||||
constexpr int PALETTE_MAX_ENTRIES = 64;
|
||||
// Ceiling on the filaments the palette's entries can refer to (the bump shader's filament_rgb[]);
|
||||
// mmu segmentation stops at Extruder16 anyway.
|
||||
constexpr int PALETTE_MAX_FILAMENTS = 16;
|
||||
|
||||
// sRGB (0..1) <-> CIELAB, D65. Exactly what the bump shader's srgb_to_lab() computes, so the CPU
|
||||
// quantizer, the mixed-palette entries and the per-fragment preview all match in the same space.
|
||||
@@ -408,7 +411,7 @@ void GLGizmoTextureDisplacement::on_shutdown()
|
||||
m_subdivide_preview_tris = -1;
|
||||
m_subdivide_preview_glmodel.reset();
|
||||
m_bump_active_chart = -1;
|
||||
m_bump_active_vertex.clear();
|
||||
m_bump_active_face.clear();
|
||||
m_bump_island_delta = Eigen::Matrix<float, 2, 3>::Identity();
|
||||
m_island_drag_active = false;
|
||||
m_island_move_set.clear();
|
||||
@@ -481,6 +484,7 @@ void GLGizmoTextureDisplacement::render_painter_gizmo()
|
||||
rebuild_paint_overlay();
|
||||
m_paint_overlay_dirty = false;
|
||||
}
|
||||
rebuild_other_paint_overlay(); // a no-op unless another layer's paint, the active layer or the preview changed
|
||||
// is_initialized() alone is not enough: render_bump_preview_mesh() also needs an active layer
|
||||
// with a decoded texture and a compiled shader, and bails silently without them. Hiding the real
|
||||
// volume for a bump pass that then draws nothing is what made the model vanish - most obviously
|
||||
@@ -518,12 +522,33 @@ void GLGizmoTextureDisplacement::render_painter_gizmo()
|
||||
render_triangles(selection);
|
||||
}
|
||||
|
||||
// Every other layer's paint, in muted grey, so all layers stay visible while one of them is edited. Drawn
|
||||
// before the active layer's tint so that one reads on top where the two overlap.
|
||||
if (show_paint_overlay)
|
||||
render_paint_overlay(m_other_paint_glmodel);
|
||||
|
||||
// The translucent paint tint. Needed in the bump view because the opaque highlight above is
|
||||
// skipped there, and in the true-displacement view because the displaced surface rises *above*
|
||||
// the undisplaced overlay geometry and hides it exactly where the relief is strongest - in both
|
||||
// cases leaving an erase stroke with no visible effect until the next full preview rebuild.
|
||||
if (show_paint_overlay && (use_bump || use_true_preview))
|
||||
render_paint_overlay();
|
||||
render_paint_overlay(m_paint_overlay_glmodel);
|
||||
|
||||
// The UV editor's island selection, shown on the model. Polled here rather than pushed: the pane
|
||||
// changes its selection in its own mouse handling, and a compare of a few ints per frame is free.
|
||||
{
|
||||
const TextureDisplacementLayer *al = active_layer();
|
||||
const UVEditorCanvas *uv_canvas = wxGetApp().plater()->get_uv_editor_canvas();
|
||||
if (m_show_uv_editor && al != nullptr && al->projection_method == TextureProjectionMethod::LSCM &&
|
||||
uv_canvas != nullptr && !m_uv_editor_unwrap.empty()) {
|
||||
if (uv_canvas->selected_islands() != m_island_overlay_selection)
|
||||
rebuild_island_overlay(uv_canvas->selected_islands());
|
||||
render_island_overlay();
|
||||
} else if (m_island_overlay_glmodel.is_initialized()) {
|
||||
m_island_overlay_glmodel.reset();
|
||||
m_island_overlay_selection.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// Diagnostic overlays, drawn on top of whatever preview is active (both pull toward the camera
|
||||
// with a polygon offset so they win the depth test against the coincident surface).
|
||||
@@ -1055,6 +1080,49 @@ std::vector<Vec2f> GLGizmoTextureDisplacement::compute_layer_vertex_uvs(const in
|
||||
return {}; // Triplanar / Cylindrical / Spherical: the shader projects on its own
|
||||
}
|
||||
|
||||
int GLGizmoTextureDisplacement::layer_projection_frame(const indexed_triangle_set &local_patch,
|
||||
const TextureDisplacementLayer &layer,
|
||||
Vec3f ¢er, Vec3f &axis) const
|
||||
{
|
||||
center = Vec3f::Zero();
|
||||
axis = Vec3f::UnitZ();
|
||||
const ModelVolume *mv = texture_volume();
|
||||
if (mv == nullptr || (layer.projection_method != TextureProjectionMethod::Cylindrical &&
|
||||
layer.projection_method != TextureProjectionMethod::Spherical))
|
||||
return 0;
|
||||
// The bake averages the *whole mesh's* vertex normals over the patch's corners, so this has to as
|
||||
// well: a patch-only average would sometimes quantize to a different world axis and wrap the
|
||||
// texture the other way round. Both meshes go through patch_in_world() first, which is the frame
|
||||
// the shaders' tex_pos lives in.
|
||||
Vec3f average_normal;
|
||||
texture_displacement_patch_frame(patch_in_world(local_patch),
|
||||
texture_displacement_vertex_normals(patch_in_world(mv->mesh().its)),
|
||||
center, axis, average_normal);
|
||||
return layer.projection_method == TextureProjectionMethod::Cylindrical ? 1 : 2;
|
||||
}
|
||||
|
||||
std::vector<Vec2f> GLGizmoTextureDisplacement::compute_layer_corner_uvs(const indexed_triangle_set &local_patch,
|
||||
const TextureDisplacementLayer &layer) const
|
||||
{
|
||||
if (layer.projection_method == TextureProjectionMethod::LSCM) {
|
||||
const indexed_triangle_set patch = patch_in_world(local_patch);
|
||||
const float aspect = layer_texture_aspect(layer);
|
||||
std::vector<Vec2f> uv = compute_lscm_corner_uvs(patch, layer);
|
||||
for (Vec2f &p : uv)
|
||||
p = apply_uv_transform(p, layer, aspect);
|
||||
return uv;
|
||||
}
|
||||
// Single-valued per point: fan the per-vertex result out over the corners.
|
||||
const std::vector<Vec2f> per_vertex = compute_layer_vertex_uvs(local_patch, layer);
|
||||
if (per_vertex.size() != local_patch.vertices.size())
|
||||
return {};
|
||||
std::vector<Vec2f> corner(local_patch.indices.size() * 3);
|
||||
for (size_t f = 0; f < local_patch.indices.size(); ++f)
|
||||
for (int k = 0; k < 3; ++k)
|
||||
corner[f * 3 + size_t(k)] = per_vertex[size_t(local_patch.indices[f][k])];
|
||||
return corner;
|
||||
}
|
||||
|
||||
void GLGizmoTextureDisplacement::rebuild_bump_preview_mesh()
|
||||
{
|
||||
m_bump_preview_glmodel.reset();
|
||||
@@ -1087,11 +1155,20 @@ void GLGizmoTextureDisplacement::rebuild_bump_preview_mesh()
|
||||
// reconstructed in the fragment shader the way a triplanar projection can. This is also what
|
||||
// makes the fast preview follow the UV editor: the uvs move when an island is dragged, so this
|
||||
// mesh rebuilds (on drag end) with them. The other projections keep projecting in-shader.
|
||||
// Per *corner*, not per vertex: the mesh below is flat (unshared) anyway, so each triangle can
|
||||
// carry its own chart's UVs - see compute_layer_corner_uvs().
|
||||
const TextureDisplacementLayer *active = active_layer();
|
||||
std::vector<Vec2f> vertex_uv = active != nullptr ? compute_layer_vertex_uvs(patch, *active) : std::vector<Vec2f>{};
|
||||
m_bump_preview_uses_vertex_uv = vertex_uv.size() == patch.vertices.size();
|
||||
std::vector<Vec2f> corner_uv = active != nullptr ? compute_layer_corner_uvs(patch, *active) : std::vector<Vec2f>{};
|
||||
m_bump_preview_uses_vertex_uv = corner_uv.size() == patch.indices.size() * 3;
|
||||
if (!m_bump_preview_uses_vertex_uv)
|
||||
vertex_uv.clear();
|
||||
corner_uv.clear();
|
||||
m_bump_projection_mode = (active != nullptr && !m_bump_preview_uses_vertex_uv) ?
|
||||
layer_projection_frame(patch, *active, m_bump_patch_center, m_bump_patch_axis) : 0;
|
||||
|
||||
// Which triangles the in-flight UV drag moves. Computed here, against the very patch this mesh is
|
||||
// built from, so the flags can never be indexed by a different triangle count than they were sized
|
||||
// for (the drag starts from the flushed facet data, a brush stroke changes the live selector).
|
||||
compute_bump_active_faces(m_bump_active_chart >= 0 ? m_island_move_set : std::vector<int>{}, patch.indices.size());
|
||||
|
||||
// Colour is quantized per *fragment* in the shader now (see the .fs), so this mesh carries no
|
||||
// colour of its own - the palette and the colour texture are uniforms, and every pixel matches the
|
||||
@@ -1112,29 +1189,30 @@ void GLGizmoTextureDisplacement::rebuild_bump_preview_mesh()
|
||||
// quality here because the bump shader takes its surface normal from screen-space derivatives of
|
||||
// position (dFdx/dFdy), not from a per-vertex normal. normal.y flags the UV-editor island being
|
||||
// dragged so the shader can move just that island via the island_delta uniform.
|
||||
const bool have_active = m_bump_active_chart >= 0 && !m_bump_active_vertex.empty();
|
||||
const size_t tri_total = patch.indices.size() + rest.indices.size();
|
||||
const size_t tri_total = patch.indices.size() + rest.indices.size();
|
||||
init_data.reserve_vertices(tri_total * 3);
|
||||
init_data.reserve_indices(tri_total * 3);
|
||||
unsigned vcount = 0;
|
||||
const auto emit_triangles = [&](const indexed_triangle_set &its, float weight) {
|
||||
for (const stl_triangle_vertex_indices &tri : its.indices) {
|
||||
const auto emit_triangles = [&](const indexed_triangle_set &its, float weight, bool painted) {
|
||||
for (size_t f = 0; f < its.indices.size(); ++f) {
|
||||
const stl_triangle_vertex_indices &tri = its.indices[f];
|
||||
// One value for the whole triangle: island_active is an interpolated varying, so the three
|
||||
// corners have to agree or the shader moves part of a triangle and not the rest.
|
||||
const float act = (painted && f < m_bump_active_face.size() && m_bump_active_face[f]) ? 1.f : 0.f;
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
const int idx = tri[i];
|
||||
const float act = (have_active && idx >= 0 && size_t(idx) < m_bump_active_vertex.size() &&
|
||||
m_bump_active_vertex[size_t(idx)]) ? 1.f : 0.f;
|
||||
const Vec2f uv = (weight > 0.5f && m_bump_preview_uses_vertex_uv && size_t(idx) < vertex_uv.size()) ?
|
||||
vertex_uv[size_t(idx)] : Vec2f::Zero();
|
||||
const Vec2f uv = (painted && m_bump_preview_uses_vertex_uv) ? corner_uv[f * 3 + size_t(i)]
|
||||
: Vec2f::Zero();
|
||||
init_data.add_vertex(its.vertices[size_t(idx)], Vec3f(weight, act, 0.f), uv);
|
||||
}
|
||||
init_data.add_triangle(vcount, vcount + 1, vcount + 2);
|
||||
vcount += 3;
|
||||
}
|
||||
};
|
||||
emit_triangles(patch, 1.f); // painted -> bumped, and coloured by the shader
|
||||
emit_triangles(patch, 1.f, true); // painted -> bumped, and coloured by the shader
|
||||
// Untouched surface: flat, so it still shows but isn't bumped - and uncoloured, which is what the
|
||||
// bake leaves it as (EnforcerBlockerType::NONE, i.e. the volume's own filament).
|
||||
emit_triangles(rest, 0.f);
|
||||
emit_triangles(rest, 0.f, false);
|
||||
|
||||
m_bump_preview_glmodel.init_from(std::move(init_data));
|
||||
// GLModel::render() unconditionally re-sets the shader's "uniform_color" from this internal
|
||||
@@ -1156,24 +1234,25 @@ void GLGizmoTextureDisplacement::rebuild_bump_preview_mesh()
|
||||
}
|
||||
}
|
||||
|
||||
void GLGizmoTextureDisplacement::compute_bump_active_vertices(const std::vector<int> &charts)
|
||||
void GLGizmoTextureDisplacement::compute_bump_active_faces(const std::vector<int> &charts, size_t patch_face_count)
|
||||
{
|
||||
m_bump_active_vertex.clear();
|
||||
const ModelVolume *mv = texture_volume();
|
||||
if (mv == nullptr || charts.empty())
|
||||
m_bump_active_face.clear();
|
||||
if (charts.empty() || patch_face_count == 0)
|
||||
return;
|
||||
const PatchUnwrap &u = m_uv_editor_unwrap;
|
||||
m_bump_active_vertex.assign(mv->mesh().its.vertices.size(), 0);
|
||||
// Flag the base vertices of every chart being moved. For a group/multi move that is more than one
|
||||
if (u.source_face.size() != u.indices.size())
|
||||
return;
|
||||
m_bump_active_face.assign(patch_face_count, 0);
|
||||
// Flag every triangle of every chart being moved. For a group/multi move that is more than one
|
||||
// chart, but since such a move is a pure translation the shader applies the same delta to them all
|
||||
// (see on_island_edited) - exactly the "joined islands move together" behaviour.
|
||||
for (size_t i = 0; i < u.uvs.size(); ++i) {
|
||||
if (i >= u.vertex_chart.size() ||
|
||||
std::find(charts.begin(), charts.end(), u.vertex_chart[i]) == charts.end())
|
||||
for (size_t t = 0; t < u.indices.size(); ++t) {
|
||||
const int f = u.source_face[t];
|
||||
const int v0 = u.indices[t][0]; // a triangle lies in one chart, so any corner names it
|
||||
if (f < 0 || size_t(f) >= m_bump_active_face.size() || v0 < 0 || size_t(v0) >= u.vertex_chart.size())
|
||||
continue;
|
||||
const int sv = (i < u.source_vertex.size()) ? u.source_vertex[i] : -1;
|
||||
if (sv >= 0 && size_t(sv) < m_bump_active_vertex.size())
|
||||
m_bump_active_vertex[size_t(sv)] = 1;
|
||||
if (std::find(charts.begin(), charts.end(), u.vertex_chart[size_t(v0)]) != charts.end())
|
||||
m_bump_active_face[size_t(f)] = 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1324,6 +1403,12 @@ void GLGizmoTextureDisplacement::render_bump_preview_mesh()
|
||||
// When set, the shader samples at the per-vertex uv baked into the mesh (LSCM) rather than
|
||||
// projecting; see rebuild_bump_preview_mesh().
|
||||
shader->set_uniform("use_vertex_uv", m_bump_preview_uses_vertex_uv);
|
||||
// Cylindrical/Spherical wrap around the painted patch's own centre, which no fragment can derive:
|
||||
// captured with the mesh (see rebuild_bump_preview_mesh()) and handed over here. 0 is the planar
|
||||
// projection every other in-shader path uses.
|
||||
shader->set_uniform("projection_mode", m_bump_projection_mode);
|
||||
shader->set_uniform("patch_center", m_bump_patch_center);
|
||||
shader->set_uniform("patch_axis", m_bump_patch_axis);
|
||||
|
||||
// The filament palette the mesh's per-triangle indices refer to. Count 0 means "no layer is
|
||||
// colouring", and the shader keeps the model's own colour for every fragment.
|
||||
@@ -1334,11 +1419,32 @@ void GLGizmoTextureDisplacement::render_bump_preview_mesh()
|
||||
(color_tex != nullptr) ? int(std::min(m_bump_preview_palette.size(), size_t(PALETTE_MAX_ENTRIES))) : 0;
|
||||
shader->set_uniform("palette_count", palette_count);
|
||||
shader->set_uniform("has_color_tex", color_tex != nullptr);
|
||||
// A flat-colour image is matched against single filaments only, as the bake does.
|
||||
shader->set_uniform("pure_only", color_tex != nullptr && analyze_texture_detail(*layer).flat_colors);
|
||||
for (int i = 0; i < palette_count; ++i) {
|
||||
const Vec3f &rgb = m_bump_preview_palette[size_t(i)].rgb;
|
||||
shader->set_uniform(("palette_rgb[" + std::to_string(i) + "]").c_str(), rgb);
|
||||
shader->set_uniform(("palette_lab[" + std::to_string(i) + "]").c_str(), srgb_to_lab(rgb));
|
||||
const PaletteEntry &e = m_bump_preview_palette[size_t(i)];
|
||||
const std::string idx = "[" + std::to_string(i) + "]";
|
||||
shader->set_uniform(("palette_rgb" + idx).c_str(), e.rgb);
|
||||
shader->set_uniform(("palette_lab" + idx).c_str(), srgb_to_lab(e.rgb));
|
||||
// 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_bump_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;
|
||||
shader->set_uniform("filament_count", filament_count);
|
||||
for (int i = 0; i < filament_count; ++i) {
|
||||
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));
|
||||
@@ -1374,6 +1480,64 @@ bool GLGizmoTextureDisplacement::bump_preview_ready() const
|
||||
return wxGetApp().get_shader("texture_displacement_bump") != nullptr;
|
||||
}
|
||||
|
||||
// Appends a painted patch to an overlay, lifted onto the displaced surface where that has the base mesh's
|
||||
// topology (see rebuild_paint_overlay()).
|
||||
static void append_paint_patch(GLModel::Geometry &out, const indexed_triangle_set &patch, const std::vector<Vec3f> *displaced)
|
||||
{
|
||||
unsigned n = unsigned(out.vertices_count());
|
||||
for (const stl_triangle_vertex_indices &tri : patch.indices) {
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
const size_t idx = size_t(tri[i]);
|
||||
out.add_vertex((displaced != nullptr && idx < displaced->size()) ? (*displaced)[idx] : patch.vertices[idx]);
|
||||
}
|
||||
out.add_triangle(n, n + 1, n + 2);
|
||||
n += 3;
|
||||
}
|
||||
}
|
||||
|
||||
void GLGizmoTextureDisplacement::rebuild_other_paint_overlay()
|
||||
{
|
||||
const ModelVolume *mv = texture_volume();
|
||||
// What it depends on: the volume, which layer is active, every other layer's paint (by its timestamp) and the
|
||||
// displaced positions it is lifted onto. Compared every frame, rebuilt only when it differs.
|
||||
std::string key;
|
||||
if (mv != nullptr) {
|
||||
key = std::to_string(mv->id().id) + ":" + std::to_string(m_active_layer_slot) + (m_use_bump_preview ? ":b:" : ":t:") +
|
||||
std::to_string(reinterpret_cast<uintptr_t>(m_preview_its.vertices.data())) + ":" +
|
||||
std::to_string(m_preview_its.vertices.size());
|
||||
for (const TextureDisplacementLayer &l : mv->texture_displacement_layers)
|
||||
if (l.slot != m_active_layer_slot && l.slot >= 0 && l.slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS))
|
||||
key += "|" + std::to_string(l.slot) + "@" + std::to_string(mv->texture_displacement_facet(l.slot).timestamp());
|
||||
}
|
||||
if (key == m_other_paint_key)
|
||||
return;
|
||||
m_other_paint_key = std::move(key);
|
||||
m_other_paint_glmodel.reset();
|
||||
if (mv == nullptr)
|
||||
return;
|
||||
|
||||
const std::vector<Vec3f> *displaced = nullptr;
|
||||
if (!m_use_bump_preview && m_preview_its.vertices.size() == mv->mesh().its.vertices.size() &&
|
||||
!m_preview_its.vertices.empty())
|
||||
displaced = &m_preview_its.vertices;
|
||||
|
||||
GLModel::Geometry init_data;
|
||||
init_data.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3 };
|
||||
for (const TextureDisplacementLayer &l : mv->texture_displacement_layers) {
|
||||
if (l.slot == m_active_layer_slot || l.slot < 0 || l.slot >= int(TEXTURE_DISPLACEMENT_MAX_LAYERS) ||
|
||||
mv->texture_displacement_facet(l.slot).empty())
|
||||
continue;
|
||||
TriangleSelector selector(mv->mesh());
|
||||
selector.deserialize(mv->texture_displacement_facet(l.slot).get_data(), false);
|
||||
append_paint_patch(init_data, selector.get_facets_strict(EnforcerBlockerType::ENFORCER), displaced);
|
||||
}
|
||||
if (init_data.is_empty())
|
||||
return;
|
||||
m_other_paint_glmodel.init_from(std::move(init_data));
|
||||
// Neutral grey: painted, but not the layer the brush is working on.
|
||||
m_other_paint_glmodel.set_color(ColorRGBA(0.55f, 0.58f, 0.60f, 0.35f));
|
||||
}
|
||||
|
||||
void GLGizmoTextureDisplacement::rebuild_paint_overlay()
|
||||
{
|
||||
m_paint_overlay_glmodel.reset();
|
||||
@@ -1403,27 +1567,18 @@ void GLGizmoTextureDisplacement::rebuild_paint_overlay()
|
||||
init_data.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3 };
|
||||
init_data.reserve_vertices(patch.indices.size() * 3);
|
||||
init_data.reserve_indices(patch.indices.size() * 3);
|
||||
unsigned n = 0;
|
||||
for (const stl_triangle_vertex_indices &tri : patch.indices) {
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
const size_t idx = size_t(tri[i]);
|
||||
init_data.add_vertex((displaced != nullptr && idx < displaced->size()) ? (*displaced)[idx]
|
||||
: patch.vertices[idx]);
|
||||
}
|
||||
init_data.add_triangle(n, n + 1, n + 2);
|
||||
n += 3;
|
||||
}
|
||||
append_paint_patch(init_data, patch, displaced);
|
||||
m_paint_overlay_glmodel.init_from(std::move(init_data));
|
||||
// GLModel::render() re-sets "uniform_color" from this field just before drawing, so the colour
|
||||
// has to be set here rather than as a uniform at draw time.
|
||||
m_paint_overlay_glmodel.set_color(ColorRGBA(0.16f, 0.79f, 0.35f, 0.38f));
|
||||
}
|
||||
|
||||
void GLGizmoTextureDisplacement::render_paint_overlay()
|
||||
void GLGizmoTextureDisplacement::render_paint_overlay(GLModel &overlay)
|
||||
{
|
||||
const ModelObject *mo = m_c->selection_info()->model_object();
|
||||
const ModelVolume *mv = texture_volume();
|
||||
if (mo == nullptr || mv == nullptr || !m_paint_overlay_glmodel.is_initialized())
|
||||
if (mo == nullptr || mv == nullptr || !overlay.is_initialized())
|
||||
return;
|
||||
GLShaderProgram *shader = wxGetApp().get_shader("flat");
|
||||
if (shader == nullptr)
|
||||
@@ -1443,7 +1598,78 @@ void GLGizmoTextureDisplacement::render_paint_overlay()
|
||||
glsafe(::glEnable(GL_POLYGON_OFFSET_FILL));
|
||||
glsafe(::glPolygonOffset(-1.5f, -1.5f));
|
||||
glsafe(::glDepthMask(GL_FALSE));
|
||||
m_paint_overlay_glmodel.render();
|
||||
overlay.render();
|
||||
glsafe(::glDepthMask(GL_TRUE));
|
||||
glsafe(::glDisable(GL_POLYGON_OFFSET_FILL));
|
||||
shader->stop_using();
|
||||
}
|
||||
|
||||
void GLGizmoTextureDisplacement::rebuild_island_overlay(const std::vector<int> &selection)
|
||||
{
|
||||
m_island_overlay_glmodel.reset();
|
||||
m_island_overlay_selection = selection;
|
||||
const ModelVolume *mv = texture_volume();
|
||||
if (mv == nullptr || selection.empty() || m_uv_editor_unwrap.empty())
|
||||
return;
|
||||
// The unwrap was made from the painted patch in the bake frame; the same extraction on the
|
||||
// volume's own mesh gives the same triangles and vertex order in local coordinates, which is the
|
||||
// frame the overlay is drawn in (with the volume's transform, like the paint tint).
|
||||
const indexed_triangle_set patch = extract_painted_patch(mv->mesh().its, m_uv_editor_state.facets);
|
||||
const PatchUnwrap &uw = m_uv_editor_unwrap;
|
||||
std::vector<uint8_t> chosen(size_t(std::max(uw.chart_count, 0)), 0);
|
||||
for (const int c : selection)
|
||||
if (c >= 0 && size_t(c) < chosen.size())
|
||||
chosen[size_t(c)] = 1;
|
||||
|
||||
GLModel::Geometry init_data;
|
||||
init_data.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3 };
|
||||
unsigned n = 0;
|
||||
for (const stl_triangle_vertex_indices &tri : uw.indices) {
|
||||
const int v0 = tri[0];
|
||||
if (v0 < 0 || size_t(v0) >= uw.vertex_chart.size())
|
||||
continue;
|
||||
const int c = uw.vertex_chart[size_t(v0)];
|
||||
if (c < 0 || size_t(c) >= chosen.size() || !chosen[size_t(c)])
|
||||
continue;
|
||||
bool ok = true;
|
||||
for (int k = 0; k < 3 && ok; ++k) {
|
||||
const int u = tri[k];
|
||||
ok = u >= 0 && size_t(u) < uw.source_vertex.size() && uw.source_vertex[size_t(u)] >= 0 &&
|
||||
size_t(uw.source_vertex[size_t(u)]) < patch.vertices.size();
|
||||
}
|
||||
if (!ok)
|
||||
continue;
|
||||
for (int k = 0; k < 3; ++k)
|
||||
init_data.add_vertex(patch.vertices[size_t(uw.source_vertex[size_t(tri[k])])]);
|
||||
init_data.add_triangle(n, n + 1, n + 2);
|
||||
n += 3;
|
||||
}
|
||||
if (n == 0)
|
||||
return;
|
||||
m_island_overlay_glmodel.init_from(std::move(init_data));
|
||||
m_island_overlay_glmodel.set_color(ColorRGBA(0.10f, 0.55f, 0.95f, 0.45f)); // the pane's selection blue
|
||||
}
|
||||
|
||||
void GLGizmoTextureDisplacement::render_island_overlay()
|
||||
{
|
||||
const ModelObject *mo = m_c->selection_info()->model_object();
|
||||
const ModelVolume *mv = texture_volume();
|
||||
if (mo == nullptr || mv == nullptr || !m_island_overlay_glmodel.is_initialized())
|
||||
return;
|
||||
GLShaderProgram *shader = wxGetApp().get_shader("flat");
|
||||
if (shader == nullptr)
|
||||
return;
|
||||
const Selection &selection = m_parent.get_selection();
|
||||
const Transform3d trafo_matrix = mo->instances[selection.get_instance_idx()]->get_transformation().get_matrix() * mv->get_matrix();
|
||||
const Camera &camera = wxGetApp().plater()->get_camera();
|
||||
shader->start_using();
|
||||
shader->set_uniform("view_model_matrix", camera.get_view_matrix() * trafo_matrix);
|
||||
shader->set_uniform("projection_matrix", camera.get_projection_matrix());
|
||||
// Above the paint tint (a larger offset), translucent, no depth writes - a marker, not geometry.
|
||||
glsafe(::glEnable(GL_POLYGON_OFFSET_FILL));
|
||||
glsafe(::glPolygonOffset(-2.0f, -2.0f));
|
||||
glsafe(::glDepthMask(GL_FALSE));
|
||||
m_island_overlay_glmodel.render();
|
||||
glsafe(::glDepthMask(GL_TRUE));
|
||||
glsafe(::glDisable(GL_POLYGON_OFFSET_FILL));
|
||||
shader->stop_using();
|
||||
@@ -1475,6 +1701,14 @@ void GLGizmoTextureDisplacement::rebuild_uvcheck_mesh()
|
||||
std::vector<Vec2f> uv = compute_layer_vertex_uvs(patch, *layer);
|
||||
const bool have_uvs = uv.size() == patch.vertices.size();
|
||||
m_uvcheck_uses_vertex_uv = have_uvs;
|
||||
m_uvcheck_projection_mode = have_uvs ? 0 :
|
||||
layer_projection_frame(patch, *layer, m_uvcheck_patch_center, m_uvcheck_patch_axis);
|
||||
// Per corner as well, for the same reason the bump mesh takes them: under LSCM a seam vertex has a
|
||||
// different uv in each island it borders, so the shared-vertex form drew one triangle per face from
|
||||
// a neighbouring island's placement. Only the *drawing* needs this; the distortion metric below is
|
||||
// a per-vertex average by construction and keeps using `uv`.
|
||||
const std::vector<Vec2f> corner_uv = compute_layer_corner_uvs(patch, *layer);
|
||||
const bool have_corner_uvs = have_uvs && corner_uv.size() == patch.indices.size() * 3;
|
||||
|
||||
// Per-vertex area distortion in [0,1] (0.5 == ideal), only when both requested and possible.
|
||||
std::vector<float> distortion(patch.vertices.size(), 0.5f);
|
||||
@@ -1513,13 +1747,23 @@ void GLGizmoTextureDisplacement::rebuild_uvcheck_mesh()
|
||||
|
||||
GLModel::Geometry init_data;
|
||||
init_data.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3N3T2 };
|
||||
init_data.reserve_vertices(patch.vertices.size());
|
||||
// Flat (one vertex per triangle corner), so each triangle can carry its own island's uv - see
|
||||
// have_corner_uvs above. Costs nothing in shading quality: the overlay shades from uv and the
|
||||
// interpolated distortion value alone, never from a per-vertex normal.
|
||||
init_data.reserve_vertices(patch.indices.size() * 3);
|
||||
init_data.reserve_indices(patch.indices.size() * 3);
|
||||
for (size_t vi = 0; vi < patch.vertices.size(); ++vi)
|
||||
init_data.add_vertex(patch.vertices[vi], Vec3f(distortion[vi], 0.f, 0.f),
|
||||
have_uvs ? uv[vi] : Vec2f::Zero());
|
||||
for (const stl_triangle_vertex_indices &tri : patch.indices)
|
||||
init_data.add_triangle(unsigned(tri[0]), unsigned(tri[1]), unsigned(tri[2]));
|
||||
unsigned vcount = 0;
|
||||
for (size_t f = 0; f < patch.indices.size(); ++f) {
|
||||
const stl_triangle_vertex_indices &tri = patch.indices[f];
|
||||
for (int k = 0; k < 3; ++k) {
|
||||
const size_t vi = size_t(tri[k]);
|
||||
init_data.add_vertex(patch.vertices[vi], Vec3f(distortion[vi], 0.f, 0.f),
|
||||
have_corner_uvs ? corner_uv[f * 3 + size_t(k)] :
|
||||
(have_uvs ? uv[vi] : Vec2f::Zero()));
|
||||
}
|
||||
init_data.add_triangle(vcount, vcount + 1, vcount + 2);
|
||||
vcount += 3;
|
||||
}
|
||||
|
||||
m_uvcheck_glmodel.init_from(std::move(init_data));
|
||||
}
|
||||
@@ -1560,6 +1804,11 @@ void GLGizmoTextureDisplacement::render_uvcheck_mesh()
|
||||
shader->set_uniform("rotation_rad", layer->rotation_deg * float(M_PI) / 180.f);
|
||||
shader->set_uniform("uv_offset", layer->offset);
|
||||
shader->set_uniform("use_vertex_uv", m_uvcheck_uses_vertex_uv);
|
||||
shader->set_uniform("projection_mode", m_uvcheck_projection_mode);
|
||||
shader->set_uniform("patch_center", m_uvcheck_patch_center);
|
||||
shader->set_uniform("patch_axis", m_uvcheck_patch_axis);
|
||||
// Was never uploaded, so the checker disagreed with the bake for any non-square height map.
|
||||
shader->set_uniform("tex_aspect", layer_texture_aspect(*layer));
|
||||
|
||||
// Coincident with the base surface, so pull it toward the camera to win the depth test.
|
||||
glsafe(::glEnable(GL_POLYGON_OFFSET_FILL));
|
||||
@@ -1870,6 +2119,8 @@ void GLGizmoTextureDisplacement::update_uv_editor()
|
||||
}
|
||||
// Padding disabled (0): the user asked to pack islands with no gap between them.
|
||||
m_uv_editor_unwrap = compute_patch_unwrap(patch, layer->lscm_seam_angle_deg, 0.f, layer->lscm_seam_edges);
|
||||
m_island_overlay_glmodel.reset(); // the islands were renumbered: rebuilt from the pane's selection next frame
|
||||
m_island_overlay_selection.clear();
|
||||
// Re-apply any stored UV edits onto the fresh unwrap, so the pane shows exactly what
|
||||
// compute_lscm_uvs() will bake (which applies the same overrides).
|
||||
apply_lscm_uv_overrides(m_uv_editor_unwrap, layer->lscm_uv_overrides);
|
||||
@@ -2159,6 +2410,17 @@ void GLGizmoTextureDisplacement::run_uv_command(int cmd, float value)
|
||||
// Closed with the pane's own X: keep it closed until asked again, and upload the background afresh then.
|
||||
m_show_uv_editor = false;
|
||||
m_uv_editor_bg = UVBackground::None;
|
||||
// The seam tool belongs to the pane: left on with the pane gone, every stroke on the model would
|
||||
// be swallowed as a seam click and nothing would paint.
|
||||
if (m_seam_edit_mode) {
|
||||
m_seam_edit_mode = false;
|
||||
m_seam_hover_edge = { -1, -1 };
|
||||
m_seam_hover_vertex = -1;
|
||||
m_seam_hover_glmodel.reset();
|
||||
m_seam_path_anchor = -1;
|
||||
m_seam_anchor_glmodel.reset();
|
||||
push_uv_pane_state();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (cmd == int(Command::SetBackground)) {
|
||||
@@ -2574,10 +2836,10 @@ void GLGizmoTextureDisplacement::on_island_edited(int island, const Vec2f &offse
|
||||
// Decide the moved set once, at drag start: the whole selection + join groups for a move, or
|
||||
// just the primary for a rotate/scale.
|
||||
m_island_move_set = is_move ? build_island_move_set(*layer, island) : std::vector<int>{ island };
|
||||
// Set up the GPU drag: flag the moved islands' vertices and bake the mesh once (via the dirty
|
||||
// flag). From then on the drag is a uniform update, no rebuild - see render_bump_preview_mesh().
|
||||
// Set up the GPU drag: bake the mesh once (via the dirty flag), which is also what flags the
|
||||
// moved islands' triangles. From then on the drag is a uniform update, no rebuild - see
|
||||
// render_bump_preview_mesh().
|
||||
m_bump_active_chart = island;
|
||||
compute_bump_active_vertices(m_island_move_set);
|
||||
m_bump_island_delta = Eigen::Matrix<float, 2, 3>::Identity();
|
||||
m_bump_preview_dirty = true;
|
||||
}
|
||||
@@ -2604,7 +2866,7 @@ void GLGizmoTextureDisplacement::on_island_edited(int island, const Vec2f &offse
|
||||
if (finished) {
|
||||
m_island_drag_active = false;
|
||||
m_bump_active_chart = -1;
|
||||
m_bump_active_vertex.clear();
|
||||
m_bump_active_face.clear();
|
||||
m_island_move_set.clear();
|
||||
m_bump_island_delta = Eigen::Matrix<float, 2, 3>::Identity();
|
||||
rebuild_preview(); // the real displaced geometry moved: recompute it once, at the end
|
||||
@@ -2989,7 +3251,13 @@ void GLGizmoTextureDisplacement::update_model_object()
|
||||
if (!mv->is_model_part())
|
||||
continue;
|
||||
++idx;
|
||||
updated |= mv->texture_displacement_facet(m_active_layer_slot).set(*m_triangle_selectors[idx]);
|
||||
FacetsAnnotation &facet = mv->texture_displacement_facet(m_active_layer_slot);
|
||||
// See m_selectors_stale: the mask could not be loaded into this selector, so an empty selector
|
||||
// here is "failed to load", not "nothing painted", and writing it back would erase the paint.
|
||||
// A selector that does hold something is the user's own work and must be flushed as usual.
|
||||
if (m_selectors_stale && !facet.empty() && m_triangle_selectors[idx]->serialize().triangles_to_split.empty())
|
||||
continue;
|
||||
updated |= facet.set(*m_triangle_selectors[idx]);
|
||||
}
|
||||
|
||||
// The fast (bump) preview reads the live selector, so it has to be rebuilt after any stroke that
|
||||
@@ -3018,13 +3286,28 @@ void GLGizmoTextureDisplacement::update_from_model_object(bool first_update)
|
||||
ebt_colors.push_back(GLVolume::NEUTRAL_COLOR);
|
||||
ebt_colors.push_back(TriangleSelectorGUI::enforcers_color);
|
||||
ebt_colors.push_back(TriangleSelectorGUI::blockers_color);
|
||||
m_selectors_stale = false;
|
||||
for (const ModelVolume *mv : mo->volumes) {
|
||||
if (!mv->is_model_part())
|
||||
continue;
|
||||
|
||||
const TriangleMesh *mesh = &mv->mesh();
|
||||
const TriangleMesh *mesh = &mv->mesh();
|
||||
const TriangleSelector::TriangleSplittingData &data =
|
||||
mv->texture_displacement_facet(m_active_layer_slot).get_data();
|
||||
// The same bound TriangleSelector::deserialize() checks before it gives up - silently, with a
|
||||
// void return and no way to report it. A mask recorded before the mesh was replaced indexes
|
||||
// triangles that no longer exist, and the selector then comes back empty even though the mask
|
||||
// is not. That has to be caught here, because the next update_model_object() would otherwise
|
||||
// write the empty selector back over the mask: the paint would vanish, and the bake would
|
||||
// report "nothing is painted" about the very data the flush had just deleted.
|
||||
const size_t facet_count = mesh->its.indices.size();
|
||||
for (const TriangleSelector::TriangleBitStreamMapping &m : data.triangles_to_split)
|
||||
if (m.triangle_idx < 0 || size_t(m.triangle_idx) >= facet_count) {
|
||||
m_selectors_stale = true;
|
||||
break;
|
||||
}
|
||||
m_triangle_selectors.emplace_back(std::make_unique<TriangleSelectorPatch>(*mesh, ebt_colors));
|
||||
m_triangle_selectors.back()->deserialize(mv->texture_displacement_facet(m_active_layer_slot).get_data(), false);
|
||||
m_triangle_selectors.back()->deserialize(data, false);
|
||||
m_triangle_selectors.back()->request_update_render_data();
|
||||
}
|
||||
|
||||
@@ -3079,6 +3362,7 @@ void GLGizmoTextureDisplacement::ensure_panel_icons()
|
||||
"texture_displacement_map_view.svg", "texture_displacement_tile_repeat.svg", "menu_mirror_x.svg",
|
||||
"texture_displacement_adjust.svg", "canvas_drag.svg", "texture_displacement_move_up.svg",
|
||||
"texture_displacement_move_down.svg", "texture_displacement_drag.svg",
|
||||
"texture_displacement_select_all.svg", "texture_displacement_erase_all.svg",
|
||||
};
|
||||
std::vector<std::string> paths;
|
||||
paths.reserve(names.size());
|
||||
@@ -3870,9 +4154,10 @@ TextureColorSettings GLGizmoTextureDisplacement::color_settings_for(const ModelV
|
||||
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.despeckle_passes = mv.texture_displacement_options.color_despeckle;
|
||||
out.layer_height = print_layer_height();
|
||||
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;
|
||||
@@ -3906,6 +4191,19 @@ std::vector<ColorRGBA> GLGizmoTextureDisplacement::filament_palette()
|
||||
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 {
|
||||
@@ -3968,7 +4266,7 @@ ColorResolveFn GLGizmoTextureDisplacement::make_mix_resolver(const std::vector<P
|
||||
const float band = std::max(layer_height, 0.01f);
|
||||
const float cell = std::max(cell_mm, 0.01f);
|
||||
|
||||
return [entries, mode, band, cell](int index, const Vec3f &pos) -> int {
|
||||
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)];
|
||||
@@ -3977,7 +4275,15 @@ ColorResolveFn GLGizmoTextureDisplacement::make_mix_resolver(const std::vector<P
|
||||
|
||||
// Which of the two filaments this point falls on. Both patterns are *ordered*, never random:
|
||||
// the eye blends a regular pattern into a flat colour, and turns a random one into noise.
|
||||
if (mode == ColorMixMode::ZBands) {
|
||||
// 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;
|
||||
@@ -4018,15 +4324,25 @@ ColorQuantizeFn GLGizmoTextureDisplacement::make_palette_quantizer(const std::ve
|
||||
float l0, a0, b0;
|
||||
const Vec3f lab0 = srgb_to_lab(Vec3f((r + 0.5f) / E, (g + 0.5f) / E, (b + 0.5f) / E));
|
||||
l0 = lab0.x(); a0 = lab0.y(); b0 = lab0.z();
|
||||
int best = 0;
|
||||
float best_d = std::numeric_limits<float>::max();
|
||||
int best = 0, best_pure = -1;
|
||||
float best_d = std::numeric_limits<float>::max(), best_pure_d = best_d;
|
||||
for (size_t i = 0; i < palette_lab.size(); ++i) {
|
||||
const float d = DeltaE00(l0, a0, b0, palette_lab[i].l, palette_lab[i].a, palette_lab[i].b);
|
||||
if (d < best_d) {
|
||||
best_d = d;
|
||||
best = int(i);
|
||||
}
|
||||
if (!palette[i].is_mix() && d < best_pure_d) {
|
||||
best_pure_d = d;
|
||||
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;
|
||||
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);
|
||||
}
|
||||
});
|
||||
@@ -4929,16 +5245,25 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
const float approx_height = m_imgui->scaled(24.f);
|
||||
y = std::min(y, bottom_limit - approx_height);
|
||||
|
||||
// Docked (the default) the panel is pinned next to the gizmo toolbar and cannot be moved, like
|
||||
// every other gizmo's. Undocked it becomes an ordinary floating window: a title bar to drag it
|
||||
// by, and no forced position - this panel is tall enough (layer stack, per-layer controls) that
|
||||
// it can cover the very part of the model being painted, and being able to shove it aside is the
|
||||
// point. The position is deliberately *not* seeded on undock, so the window stays exactly where
|
||||
// it already was and the user just gains the ability to move it from there.
|
||||
// Docked (the default) the panel is pinned to the right edge of the 3D canvas and cannot be
|
||||
// moved. Deliberately *not* next to the gizmo toolbar, which is where `x` points and where every
|
||||
// other gizmo's window goes: this panel is far taller than those (layer stack plus the whole
|
||||
// per-layer control set), so at the toolbar it sits right on top of the part of the model being
|
||||
// painted. Pinning it to the canvas edge also parks it against the UV editor, since that pane is
|
||||
// docked on the right and the canvas therefore ends exactly at the pane's left edge - so the
|
||||
// panel follows the pane in and out instead of being clipped by it.
|
||||
//
|
||||
// Undocked it becomes an ordinary floating window: a title bar to drag it by, and no forced
|
||||
// position - the position is deliberately not seeded on undock, so the window stays exactly
|
||||
// where it already was and the user just gains the ability to move it from there.
|
||||
ImGuiWindowFlags flags = ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoCollapse;
|
||||
if (!m_undocked) {
|
||||
flags |= ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoTitleBar;
|
||||
GizmoImguiSetNextWIndowPos(x, y, ImGuiCond_Always, 1.0f, 0.0f);
|
||||
// Right-aligned (pivot 1), so the width the panel auto-resized to last frame does not need to
|
||||
// be known here. Width 0 skips GizmoImguiSetNextWIndowPos()'s own left-aligned fit-to-canvas
|
||||
// clamp, which would push the window back off the edge it is being pinned to.
|
||||
float right = float(m_parent.get_canvas_size().get_width()) - m_imgui->scaled(0.5f);
|
||||
GizmoImguiSetNextWIndowPos(right, y, 0.f, 0.f, ImGuiCond_Always, 1.0f, 0.0f);
|
||||
}
|
||||
|
||||
ImGuiWrapper::push_toolbar_style(m_parent.get_scale());
|
||||
@@ -5251,7 +5576,8 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
m_erase_mode = true;
|
||||
}
|
||||
|
||||
// ---- Tools: brush / face / connected area, and the active tool's own control ----
|
||||
// ---- Tools: brush / face / connected area on the left, the whole-model actions on the right, and the
|
||||
// active tool's own control on the line below ----
|
||||
// "Face" and "Connected area" reuse the exact same selection machinery every other paint gizmo has
|
||||
// (single-facet click, and angle-limited flood fill respectively).
|
||||
{
|
||||
@@ -5276,7 +5602,36 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
m_tool_type = ToolType::SMART_FILL;
|
||||
m_cursor_type = TriangleSelector::CursorType::POINTER;
|
||||
}
|
||||
|
||||
// Whole model: paint every face with the active layer, or clear its paint from all of them. Actions
|
||||
// rather than tools, so they sit apart at the right end of the row.
|
||||
const wxString whole_na = busy ? _L("Wait for the bake to finish.") :
|
||||
active == nullptr ? _L("Add a layer first.") :
|
||||
wxString();
|
||||
const wxString erase_na = !whole_na.empty() ? whole_na :
|
||||
!slot_painted(m_active_layer_slot) ? _L("The active layer has no paint yet.") :
|
||||
wxString();
|
||||
ImGui::SameLine();
|
||||
ImGui::SetCursorPosX(std::max(ImGui::GetCursorPosX(), ImGui::GetWindowContentRegionMax().x - (2.f * icon_md + gap_s)));
|
||||
if (icon_toggle(806, "texture_displacement_select_all.svg", false, icon_md, _L("Select whole model"),
|
||||
_L("Select whole model - paint every face of the model with the active layer"), whole_na))
|
||||
select_whole_model();
|
||||
ImGui::SameLine(0.f, gap_s);
|
||||
if (icon_toggle(807, "texture_displacement_erase_all.svg", false, icon_md, _L("Erase whole model"),
|
||||
_L("Erase whole model - clear the active layer's paint from every face"), erase_na)) {
|
||||
Plater::TakeSnapshot snapshot(wxGetApp().plater(), _u8L("Reset texture displacement selection"),
|
||||
UndoRedo::SnapshotType::GizmoAction);
|
||||
int idx = -1;
|
||||
for (ModelVolume *v : mo->volumes)
|
||||
if (v->is_model_part()) {
|
||||
++idx;
|
||||
m_triangle_selectors[idx]->reset();
|
||||
m_triangle_selectors[idx]->request_update_render_data();
|
||||
}
|
||||
update_model_object();
|
||||
m_parent.set_as_dirty();
|
||||
}
|
||||
|
||||
if (is_brush_mode) {
|
||||
ImGui::SetNextItemWidth(-(3.f * gap_s + 1.f + 2.f * icon_sm));
|
||||
ImGui::SliderFloat("##cursor_radius", &m_cursor_radius, CursorRadiusMin, CursorRadiusMax, "%.2f mm",
|
||||
@@ -5302,33 +5657,6 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Whole model ----
|
||||
{
|
||||
const float half = std::floor((ImGui::GetContentRegionAvail().x - style.ItemSpacing.x) * 0.5f);
|
||||
m_imgui->disabled_begin(busy || active == nullptr);
|
||||
if (ImGui::Button(_u8L("Select whole model").c_str(), ImVec2(half, 0.f)))
|
||||
select_whole_model();
|
||||
m_imgui->disabled_end();
|
||||
hover_tip(_u8L("Paint every face of the model with the active layer"));
|
||||
ImGui::SameLine();
|
||||
m_imgui->disabled_begin(busy || active == nullptr || !slot_painted(m_active_layer_slot));
|
||||
if (ImGui::Button(_u8L("Erase whole model").c_str(), ImVec2(half, 0.f))) {
|
||||
Plater::TakeSnapshot snapshot(wxGetApp().plater(), _u8L("Reset texture displacement selection"),
|
||||
UndoRedo::SnapshotType::GizmoAction);
|
||||
int idx = -1;
|
||||
for (ModelVolume *v : mo->volumes)
|
||||
if (v->is_model_part()) {
|
||||
++idx;
|
||||
m_triangle_selectors[idx]->reset();
|
||||
m_triangle_selectors[idx]->request_update_render_data();
|
||||
}
|
||||
update_model_object();
|
||||
m_parent.set_as_dirty();
|
||||
}
|
||||
m_imgui->disabled_end();
|
||||
hover_tip(_u8L("Clear the active layer's paint from every face"));
|
||||
}
|
||||
|
||||
// ---- View: Normal / Fast / Checker / Distortion as one group, Wireframe on its own ----
|
||||
// The underlying state stays m_use_bump_preview + m_uv_check_mode.
|
||||
{
|
||||
@@ -5651,13 +5979,15 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
if (ImGui::Checkbox(_u8L("Mix filaments").c_str(), &opts.color_mix_enabled))
|
||||
m_preview_params_dirty = true;
|
||||
hover_tip(_u8L("Interleave pairs of filaments to reach colours between them, so a few "
|
||||
"filaments cover far more than a few colours. Off means every triangle "
|
||||
"prints in one of the filaments exactly."));
|
||||
"filaments cover far more than a few colours. Used only on images with "
|
||||
"continuous colour (photographs, gradients); a texture of flat colours "
|
||||
"prints in single filaments either way. Off forces single filaments."));
|
||||
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 char *mix_items[] = { mix_z.c_str(), mix_xy.c_str() };
|
||||
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))) {
|
||||
@@ -5668,7 +5998,9 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
"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."));
|
||||
"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());
|
||||
}
|
||||
@@ -6409,9 +6741,8 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
m_parent.set_as_dirty();
|
||||
}
|
||||
m_imgui->disabled_end();
|
||||
hover_tip(_u8L("Auto: the resolution and the budget are chosen from the texture (its pixel "
|
||||
"size on the model and how sharp it is) and the model's size, the way "
|
||||
"BumpMesh's smart resolution does. Untick to set them by hand."));
|
||||
hover_tip(_u8L("Auto: the resolution follows the model's size and the budget is the standard "
|
||||
"750 k, the same defaults as bumpmesh.com. Untick to set them by hand."));
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(x0 + panel_w - ImGui::GetCursorPosX());
|
||||
float shown = auto_res ? rec.edge_mm : opts.v2_refine_mm;
|
||||
@@ -6424,10 +6755,7 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
}
|
||||
m_imgui->disabled_end();
|
||||
if (auto_res && rec.edge_mm > 0.f)
|
||||
hover_tip(Slic3r::format(_u8L("%1% texture pixels per edge x %2% mm per pixel%3%. Budget %4% k."),
|
||||
rec.pixels_per_edge, Slic3r::format("%.3f", rec.texel_mm),
|
||||
rec.budget_bound ? _u8L(", held back by the triangle cap") : std::string(),
|
||||
rec.budget_k));
|
||||
hover_tip(Slic3r::format(_u8L("The model's diagonal / 250, as bumpmesh.com sets it. Budget %1% k."), rec.budget_k));
|
||||
else
|
||||
hover_tip(_u8L("Triangle edge length the painted area is refined to before displacement. "
|
||||
"Smaller carries finer texture detail and costs more triangles; the budget "
|
||||
@@ -6487,7 +6815,10 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
ImGui::PopStyleColor(5);
|
||||
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled))
|
||||
m_imgui->tooltip(mv != nullptr && !mv->is_texture_displacement_painted() ?
|
||||
_u8L("Nothing is painted yet.") :
|
||||
(m_seam_edit_mode ? _u8L("Nothing is painted yet. The UV editor's seam tool is on, so "
|
||||
"strokes on the model mark seams instead of painting - turn "
|
||||
"it off in the pane to paint.") :
|
||||
_u8L("Nothing is painted yet.")) :
|
||||
pro_mode() ?
|
||||
_u8L("Turn the painted height maps into real geometry, by moving the vertices that are "
|
||||
"already there. Use Subdivide first if the mesh is too coarse to show the detail.") :
|
||||
|
||||
@@ -104,6 +104,14 @@ public:
|
||||
// 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,
|
||||
// it does so in the print too, not only on screen. The refinement edge is chosen from the model's
|
||||
// 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
|
||||
@@ -480,6 +488,17 @@ private:
|
||||
// painting into a slot with no texture assigned is harmless, it just has no visible/bake
|
||||
// effect until a texture is added to that slot.
|
||||
int m_active_layer_slot = 0;
|
||||
// Set when m_triangle_selectors could not be loaded from the stored paint masks, because a mask
|
||||
// was recorded against a different topology: TriangleSelector::deserialize() rejects that and
|
||||
// returns without a word, leaving the selector empty even though the mask is not. While this is
|
||||
// set, an *empty* selector says nothing about the paint, so update_model_object() must not flush
|
||||
// one back - serializing it over the mask destroys the user's paint for good, and the bake then
|
||||
// reports "nothing is painted" about the data the flush had just deleted.
|
||||
//
|
||||
// Deliberately not a blanket refusal to flush: once the user paints, the selector holds real
|
||||
// content again and writing it back is exactly right - it replaces the unusable mask with one
|
||||
// recorded against the current mesh. So only the empty-over-non-empty case is held back.
|
||||
bool m_selectors_stale = false;
|
||||
bool m_bake_in_progress = false;
|
||||
|
||||
// When set, the true-displacement geometry is rebuilt on every parameter change (live), instead of
|
||||
@@ -636,11 +655,22 @@ private:
|
||||
// patch only), translucent (the preview stays visible through it) and rebuilt live during a
|
||||
// stroke.
|
||||
GLModel m_paint_overlay_glmodel;
|
||||
// The islands selected in the UV editor, tinted on the model so the pane's selection can be seen
|
||||
// in place. Rebuilt whenever the pane's selection differs from the one it was built for.
|
||||
GLModel m_island_overlay_glmodel;
|
||||
std::vector<int> m_island_overlay_selection;
|
||||
void rebuild_island_overlay(const std::vector<int> &selection);
|
||||
void render_island_overlay();
|
||||
// Set on every paint event, cleared when the overlay is rebuilt in render_painter_gizmo(). Kept
|
||||
// separate from m_bump_preview_dirty so a stroke refreshes only the small painted patch per frame,
|
||||
bool m_paint_overlay_dirty = false;
|
||||
void rebuild_paint_overlay();
|
||||
void render_paint_overlay();
|
||||
void render_paint_overlay(GLModel &overlay);
|
||||
// Every *other* layer's paint, muted, so all layers stay visible while one is edited. Rebuilt only when that
|
||||
// paint, the active layer or the preview it is lifted onto changes (m_other_paint_key).
|
||||
GLModel m_other_paint_glmodel;
|
||||
std::string m_other_paint_key;
|
||||
void rebuild_other_paint_overlay();
|
||||
// Whether render_bump_preview_mesh() would actually draw something. Checked before the real volume
|
||||
// is hidden: with no layer, no texture or no shader the bump path draws nothing, and hiding the
|
||||
// volume for it left the model invisible.
|
||||
@@ -648,6 +678,12 @@ private:
|
||||
// Whether the current bump mesh carries a precomputed per-vertex uv (LSCM) that the shader
|
||||
// should sample at directly, rather than projecting in-shader. Set by rebuild_bump_preview_mesh().
|
||||
bool m_bump_preview_uses_vertex_uv = false;
|
||||
// The projection frame handed to the bump shader, captured when the mesh is built. Cylindrical and
|
||||
// Spherical are reconstructed in the fragment shader (there is no per-vertex uv for them) and wrap
|
||||
// around the whole patch, which no fragment can work out for itself. See layer_projection_frame().
|
||||
int m_bump_projection_mode = 0;
|
||||
Vec3f m_bump_patch_center = Vec3f::Zero();
|
||||
Vec3f m_bump_patch_axis = Vec3f::UnitZ();
|
||||
// The palette the fast preview's per-triangle filament indices were built against, captured when
|
||||
// the mesh was. Empty when the active layer is not colouring, which is what tells the shader to
|
||||
// fall back to the model's own colour. Held rather than re-read at draw time so the indices baked
|
||||
@@ -659,14 +695,23 @@ private:
|
||||
// the dragged island's vertices flagged, v_normal.y = 1) and then moved purely through the shader's
|
||||
// island_delta uniform - one uniform update per mouse move, no rebuild - so it tracks the cursor
|
||||
// as smoothly as Adjust placement. m_bump_active_chart is the dragged island (or -1);
|
||||
// m_bump_active_vertex flags its base vertices; m_bump_baked_active_xf is that island's placement
|
||||
// baked into the current mesh, against which the live delta is measured; m_bump_island_delta is the
|
||||
// resulting final-uv-space affine handed to the shader (identity except mid-drag).
|
||||
// m_bump_active_face flags the dragged islands' *triangles*, indexed by painted-patch face;
|
||||
// m_bump_baked_active_xf is that island's placement baked into the current mesh, against which the
|
||||
// live delta is measured; m_bump_island_delta is the resulting final-uv-space affine handed to the
|
||||
// shader (identity except mid-drag).
|
||||
//
|
||||
// Per triangle rather than per vertex deliberately: a seam vertex belongs to every chart touching
|
||||
// it, so flagging the dragged chart's base vertices also flagged the corners its neighbours use.
|
||||
// island_active is an interpolated varying, so those neighbouring triangles then had island_delta
|
||||
// applied too - dragging one island moved every adjacent island's texture while the editor, which
|
||||
// is per chart, correctly moved only the one. A triangle belongs to exactly one chart.
|
||||
int m_bump_active_chart = -1;
|
||||
std::vector<uint8_t> m_bump_active_vertex;
|
||||
std::vector<uint8_t> m_bump_active_face;
|
||||
Eigen::Matrix<float, 2, 3> m_bump_baked_active_xf = Eigen::Matrix<float, 2, 3>::Identity();
|
||||
Eigen::Matrix<float, 2, 3> m_bump_island_delta = Eigen::Matrix<float, 2, 3>::Identity();
|
||||
void compute_bump_active_vertices(const std::vector<int> &charts);
|
||||
// Flags `charts`' triangles in m_bump_active_face, sized to `patch_face_count` (the painted patch
|
||||
// the bump mesh is being built from). Cleared if the unwrap carries no face map.
|
||||
void compute_bump_active_faces(const std::vector<int> &charts, size_t patch_face_count);
|
||||
|
||||
// The set of islands the current UV-editor drag moves together: the pane's multi-selection unioned
|
||||
// with each selected island's join group (see build_island_move_set()). Populated at drag start and
|
||||
@@ -687,6 +732,19 @@ private:
|
||||
// the shader projects on its own. Shared by the bump preview and the UV-check overlay.
|
||||
std::vector<Vec2f> compute_layer_vertex_uvs(const indexed_triangle_set &patch,
|
||||
const TextureDisplacementLayer &layer) const;
|
||||
// The same, but three UVs per patch triangle (corner 0..2 of triangle i at 3i..3i+2). This is what
|
||||
// the flat, unshared-vertex preview meshes actually want: under LSCM a seam vertex has a different
|
||||
// UV in each island it borders, so collapsing to one per vertex handed a triangle at an unjoined
|
||||
// seam its neighbour's placement - one visibly skewed triangle per face. Every other projection is
|
||||
// single-valued per point, so there a corner's UV is just its vertex's.
|
||||
std::vector<Vec2f> compute_layer_corner_uvs(const indexed_triangle_set &patch,
|
||||
const TextureDisplacementLayer &layer) const;
|
||||
// The in-shader projection for `layer` (0 Triplanar, 1 Cylindrical, 2 Spherical) plus, for the two
|
||||
// wrapping ones, the patch centroid and cylinder axis they wrap around - in the texture frame the
|
||||
// shaders project in. Taken from the bake's own texture_displacement_patch_frame(), so a preview
|
||||
// can never wrap around a different centre, or pick a different axis, than the bake will.
|
||||
int layer_projection_frame(const indexed_triangle_set &local_patch, const TextureDisplacementLayer &layer,
|
||||
Vec3f ¢er, Vec3f &axis) const;
|
||||
// `patch` with its vertices moved into world millimetres - the space the bake maps the texture in
|
||||
// (see build_texture_displacement()). Returned by value because the caller usually still needs the
|
||||
// original: the patch doubles as render geometry, which is drawn through the volume's own matrix.
|
||||
@@ -699,6 +757,10 @@ private:
|
||||
UVCheckMode m_uv_check_mode = UVCheckMode::None;
|
||||
GLModel m_uvcheck_glmodel;
|
||||
bool m_uvcheck_uses_vertex_uv = false;
|
||||
// As m_bump_projection_mode and friends, for the Checker overlay.
|
||||
int m_uvcheck_projection_mode = 0;
|
||||
Vec3f m_uvcheck_patch_center = Vec3f::Zero();
|
||||
Vec3f m_uvcheck_patch_axis = Vec3f::UnitZ();
|
||||
void rebuild_uvcheck_mesh();
|
||||
void render_uvcheck_mesh();
|
||||
|
||||
|
||||
@@ -38,6 +38,8 @@ void TextureDisplacementBakeJob::process(Ctl &ctl)
|
||||
TextureColorRequest *color = nullptr;
|
||||
if (!m_input.color.empty()) {
|
||||
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);
|
||||
@@ -79,6 +81,22 @@ void TextureDisplacementBakeJob::finalize(bool canceled, std::exception_ptr &ept
|
||||
if (canceled || eptr || m_result.empty())
|
||||
return;
|
||||
|
||||
// A bake that moved nothing - no layer could be sampled, or every sample was zero - must not be
|
||||
// committed: committing is what clears the baked layers' paint, so the user would see the painted
|
||||
// region simply vanish with no relief in its place and no idea why. Keep the paint and say so.
|
||||
{
|
||||
const indexed_triangle_set &out = m_result.its;
|
||||
bool unchanged = out.indices.size() == m_input.base_mesh.indices.size() &&
|
||||
out.vertices.size() == m_input.base_mesh.vertices.size();
|
||||
for (size_t i = 0; unchanged && i < out.vertices.size(); ++i)
|
||||
unchanged = (out.vertices[i] - m_input.base_mesh.vertices[i]).cwiseAbs().maxCoeff() < 1e-5f;
|
||||
if (unchanged) {
|
||||
show_error(nullptr, _u8L("The bake produced no displacement, so nothing was changed and the paint was kept. "
|
||||
"Check that the painted layer has a texture and a non-zero depth."));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Plater *plater = wxGetApp().plater();
|
||||
|
||||
const auto commit = [this, plater]() {
|
||||
@@ -109,11 +127,21 @@ void TextureDisplacementBakeJob::finalize(bool canceled, std::exception_ptr &ept
|
||||
|
||||
// Clear the paint mask of every layer that was actually baked so a repeat bake (or the paint
|
||||
// overlay) doesn't act on triangles that no longer represent the same unbaked surface. The
|
||||
// texture layer definitions themselves (and paint outside the baked area, if any) are left
|
||||
// untouched so the user can keep sculpting with the same textures.
|
||||
for (const TextureDisplacementLayer &layer : m_input.layers)
|
||||
if (!layer.empty() && layer.slot >= 0 && layer.slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS))
|
||||
volume->texture_displacement_facet(layer.slot).reset();
|
||||
// texture layer definitions themselves are left untouched so the user can keep sculpting with
|
||||
// the same textures.
|
||||
//
|
||||
// A mask the bake did *not* consume only still means what it did if the topology is unchanged,
|
||||
// which is true of the classic path (it moves existing vertices) but not of the one-run
|
||||
// pipeline, which rebuilds and then simplifies the mesh. A mask left behind against the old
|
||||
// topology is exactly what makes the gizmo reload an empty selector over a non-empty mask and
|
||||
// then erase it on the next flush - see GLGizmoTextureDisplacement::update_from_model_object().
|
||||
const bool topology_changed = m_input.base_mesh.indices.size() != volume->mesh().its.indices.size();
|
||||
for (int slot = 0; slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++slot) {
|
||||
const auto it = std::find_if(m_input.layers.begin(), m_input.layers.end(),
|
||||
[slot](const TextureDisplacementLayer &l) { return l.slot == slot; });
|
||||
if (topology_changed || (it != m_input.layers.end() && !it->empty()))
|
||||
volume->texture_displacement_facet(slot).reset();
|
||||
}
|
||||
|
||||
ModelObject *object = volume->get_object();
|
||||
if (object == nullptr)
|
||||
|
||||
@@ -24,6 +24,8 @@ void TextureDisplacementPreviewJob::process(Ctl &ctl)
|
||||
TextureColorRequest *color = nullptr;
|
||||
if (!m_input.color.empty()) {
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user