mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-24 17:26:47 +00:00
Relief preserved from an earlier bake is counted on top of the budget instead of eating into it: baking a second area went from ~214 k triangles to the ~1000 k it was given. When the resolution needs more triangles than the budget allows, it is said before the bake under Resolution and again afterwards.
5024 lines
262 KiB
C++
5024 lines
262 KiB
C++
#include "TextureDisplacement.hpp"
|
|
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <functional>
|
|
#include <cassert>
|
|
#include <chrono>
|
|
#include <cmath>
|
|
#include <iterator>
|
|
#include <limits>
|
|
#include <map>
|
|
#include <mutex>
|
|
#include <numeric>
|
|
#include <optional>
|
|
#include <queue>
|
|
#include <string>
|
|
#include <tuple>
|
|
#include <unordered_map>
|
|
#include <unordered_set>
|
|
|
|
#include <Eigen/Eigenvalues>
|
|
|
|
#include <tbb/blocked_range.h>
|
|
#include <tbb/parallel_for.h>
|
|
#include <tbb/parallel_sort.h>
|
|
|
|
#include <boost/log/trivial.hpp>
|
|
|
|
#include "AABBTreeIndirect.hpp"
|
|
#include "MeshBoolean.hpp"
|
|
#include "Model.hpp"
|
|
#include "PNGReadWrite.hpp"
|
|
#include "TriangleSelector.hpp"
|
|
#include "TextureBake/TextureBakeDebug.hpp"
|
|
#include "TextureBake/TextureBakeMesh.hpp"
|
|
#include "TextureBake/TextureBakePipeline.hpp"
|
|
|
|
namespace Slic3r {
|
|
|
|
bool DecodedHeightTexture::texel_tap(const Vec2f &uv, bool tile_enabled, TextureTileMethod tile_method,
|
|
TexelTap &tap) const
|
|
{
|
|
if (empty())
|
|
return false;
|
|
|
|
auto repeat01 = [](float x) {
|
|
x = std::fmod(x, 1.f);
|
|
return x < 0.f ? x + 1.f : x;
|
|
};
|
|
// Standard mirrored-repeat: reflect back and forth every other unit, so tile edges always
|
|
// line up with themselves instead of jumping from one edge of the image to the other.
|
|
auto mirrored_repeat01 = [](float x) {
|
|
x = std::fmod(std::abs(x), 2.f);
|
|
return x > 1.f ? 2.f - x : x;
|
|
};
|
|
|
|
if (!tile_enabled && (uv.x() < 0.f || uv.x() >= 1.f || uv.y() < 0.f || uv.y() >= 1.f))
|
|
// Outside the single, non-repeating placement entirely: no texture there, not "smeared
|
|
// edge pixel" - clamping the *coordinate* to [0, 1] would otherwise keep returning the
|
|
// border row/column's height forever in every direction, stretching it out to infinity.
|
|
return false;
|
|
|
|
float u, v;
|
|
if (!tile_enabled) {
|
|
u = uv.x();
|
|
v = uv.y();
|
|
} else if (tile_method == TextureTileMethod::MirroredRepeat) {
|
|
u = mirrored_repeat01(uv.x());
|
|
v = mirrored_repeat01(uv.y());
|
|
} else {
|
|
u = repeat01(uv.x());
|
|
v = repeat01(uv.y());
|
|
}
|
|
|
|
const float fx = u * float(width);
|
|
const float fy = v * float(height);
|
|
const int x0 = std::clamp(int(std::floor(fx)), 0, width - 1);
|
|
const int y0 = std::clamp(int(std::floor(fy)), 0, height - 1);
|
|
// Neighbour for bilinear filtering: wrap for tiling methods, clamp at the edge otherwise (a
|
|
// repeating neighbour would incorrectly blend against the opposite edge of the image).
|
|
tap.x0 = x0;
|
|
tap.y0 = y0;
|
|
tap.x1 = tile_enabled ? (x0 + 1) % width : std::min(x0 + 1, width - 1);
|
|
tap.y1 = tile_enabled ? (y0 + 1) % height : std::min(y0 + 1, height - 1);
|
|
tap.tx = fx - std::floor(fx);
|
|
tap.ty = fy - std::floor(fy);
|
|
return true;
|
|
}
|
|
|
|
float DecodedHeightTexture::sample(const Vec2f &uv, bool tile_enabled, TextureTileMethod tile_method) const
|
|
{
|
|
TexelTap tap;
|
|
if (!texel_tap(uv, tile_enabled, tile_method, tap))
|
|
return 0.f;
|
|
const int x0 = tap.x0, y0 = tap.y0, x1 = tap.x1, y1 = tap.y1;
|
|
const float tx = tap.tx, ty = tap.ty;
|
|
|
|
auto at = [this](int x, int y) { return float(pixels[size_t(y) * size_t(width) + size_t(x)]) / 255.f; };
|
|
const float top = at(x0, y0) * (1.f - tx) + at(x1, y0) * tx;
|
|
const float bottom = at(x0, y1) * (1.f - tx) + at(x1, y1) * tx;
|
|
return top * (1.f - ty) + bottom * ty;
|
|
}
|
|
|
|
Vec3f DecodedHeightTexture::sample_color(const Vec2f &uv, bool tile_enabled, TextureTileMethod tile_method) const
|
|
{
|
|
TexelTap tap;
|
|
if (!has_color() || !texel_tap(uv, tile_enabled, tile_method, tap))
|
|
return Vec3f::Zero();
|
|
|
|
auto at = [this](int x, int y) {
|
|
const size_t i = (size_t(y) * size_t(width) + size_t(x)) * 3;
|
|
return Vec3f(float(rgb[i]) / 255.f, float(rgb[i + 1]) / 255.f, float(rgb[i + 2]) / 255.f);
|
|
};
|
|
const Vec3f top = at(tap.x0, tap.y0) * (1.f - tap.tx) + at(tap.x1, tap.y0) * tap.tx;
|
|
const Vec3f bottom = at(tap.x0, tap.y1) * (1.f - tap.tx) + at(tap.x1, tap.y1) * tap.tx;
|
|
return top * (1.f - tap.ty) + bottom * tap.ty;
|
|
}
|
|
|
|
namespace {
|
|
// Decoding a PNG (zlib inflate + defilter) is real work, and image_data never changes in place
|
|
// once assigned to a layer (a new texture always gets a brand new image_data), so the decoded
|
|
// result can be cached for the lifetime of that specific image_data allocation. This matters
|
|
// because the GUI's live preview calls decode_height_texture() again on every rebuild (every
|
|
// paint stroke / parameter tweak), which would otherwise re-decode the same unchanged bytes over
|
|
// and over. Keyed by a weak_ptr (not just the raw pointer) so a freed image_data's address being
|
|
// reused by an unrelated later allocation can never alias a stale cache entry: a weak_ptr to a
|
|
// destroyed object always fails to lock, forcing a correct re-decode instead of a false hit.
|
|
struct DecodedTextureCache
|
|
{
|
|
std::mutex mutex;
|
|
std::unordered_map<const void *, std::pair<std::weak_ptr<const std::vector<unsigned char>>, DecodedHeightTexture>> entries;
|
|
};
|
|
DecodedTextureCache g_decoded_texture_cache;
|
|
|
|
// The smoothed copy is cached too, one per image: the thumbnail, the shaded-preview height texture, the
|
|
// colour texture, the projector texture, the UV editor's background and the preview job all ask for
|
|
// the same (image, smoothing) pair in the same frame while the Smoothing slider moves, and each of them
|
|
// blurring its own copy is what froze the UI. Keyed like the raw cache; a different smoothing value
|
|
// simply replaces the entry.
|
|
struct SmoothedTextureCache
|
|
{
|
|
struct Entry
|
|
{
|
|
std::weak_ptr<const std::vector<unsigned char>> source;
|
|
float smoothing = 0.f;
|
|
DecodedHeightTexture texture;
|
|
};
|
|
std::mutex mutex;
|
|
std::unordered_map<const void *, Entry> entries;
|
|
};
|
|
SmoothedTextureCache g_smoothed_texture_cache;
|
|
} // namespace
|
|
|
|
namespace {
|
|
// A few passes of a separable box blur approximate a Gaussian, cheaply. `radius` is in whole texels;
|
|
// 0 is a no-op. Wraps at the edges so a tiling height map stays seamless after smoothing. Operates on
|
|
// the grayscale byte buffer in place.
|
|
//
|
|
// Each pass is a sliding window - one add and one subtract per texel - so the cost is the image size,
|
|
// not the image size times the radius. The Smoothing slider drives this on every frame it moves, for
|
|
// every consumer of the layer, and the radius goes up to 48 texels: the per-window loop this replaces
|
|
// stalled the UI for seconds on a large map. Rows (and column blocks) run in parallel. The rounding is
|
|
// the old code's exactly, so a blur gives the same bytes as before.
|
|
void smooth_height_pixels_box(std::vector<uint8_t> &pixels, int width, int height, int radius)
|
|
{
|
|
if (radius <= 0 || width <= 0 || height <= 0 || pixels.size() != size_t(width) * size_t(height))
|
|
return;
|
|
|
|
const int window = 2 * radius + 1;
|
|
const float inv = 1.f / float(window);
|
|
const auto wrap = [](int i, int n) { return (i % n + n) % n; };
|
|
std::vector<uint8_t> tmp(pixels.size());
|
|
|
|
for (int pass = 0; pass < 2; ++pass) { // two passes -> smoother than a single box
|
|
// Horizontal: a running sum per row.
|
|
tbb::parallel_for(tbb::blocked_range<int>(0, height), [&](const tbb::blocked_range<int> &r) {
|
|
for (int y = r.begin(); y < r.end(); ++y) {
|
|
const uint8_t *src = pixels.data() + size_t(y) * size_t(width);
|
|
uint8_t *dst = tmp.data() + size_t(y) * size_t(width);
|
|
uint32_t sum = 0;
|
|
for (int k = -radius; k <= radius; ++k)
|
|
sum += src[wrap(k, width)];
|
|
for (int x = 0; x < width; ++x) {
|
|
dst[x] = uint8_t(std::lround(float(sum) * inv));
|
|
sum += src[wrap(x + radius + 1, width)];
|
|
sum -= src[wrap(x - radius, width)];
|
|
}
|
|
}
|
|
});
|
|
// Vertical: one running sum per column, advanced row by row so the reads stay row-major.
|
|
tbb::parallel_for(tbb::blocked_range<int>(0, width, 256), [&](const tbb::blocked_range<int> &r) {
|
|
std::vector<uint32_t> sum(size_t(r.size()), 0);
|
|
for (int k = -radius; k <= radius; ++k) {
|
|
const uint8_t *row = tmp.data() + size_t(wrap(k, height)) * size_t(width);
|
|
for (int x = r.begin(); x < r.end(); ++x)
|
|
sum[size_t(x - r.begin())] += row[x];
|
|
}
|
|
for (int y = 0; y < height; ++y) {
|
|
uint8_t *dst = pixels.data() + size_t(y) * size_t(width);
|
|
const uint8_t *add = tmp.data() + size_t(wrap(y + radius + 1, height)) * size_t(width);
|
|
const uint8_t *sub = tmp.data() + size_t(wrap(y - radius, height)) * size_t(width);
|
|
for (int x = r.begin(); x < r.end(); ++x) {
|
|
uint32_t &sx = sum[size_t(x - r.begin())];
|
|
dst[x] = uint8_t(std::lround(float(sx) * inv));
|
|
sx += add[x];
|
|
sx -= sub[x];
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// The same blur with a *continuous* radius, which is what the Smoothing slider drives.
|
|
//
|
|
// A box blur can only work in whole texels, so mapping the slider straight onto a rounded radius
|
|
// made it move in visible jumps - and its very first step off zero was a full one-texel blur rather
|
|
// than a hint of one, which is what made the control feel like it switched on rather than ramped up.
|
|
// Blur at the next whole texel up and cross-fade the raw image back in by the fraction left over:
|
|
// below one texel that fade *is* the sub-texel kernel, and above it it turns each integer step into
|
|
// a continuous ramp.
|
|
void smooth_height_pixels(std::vector<uint8_t> &pixels, int width, int height, float radius)
|
|
{
|
|
if (radius <= 0.f || width <= 0 || height <= 0 || pixels.size() != size_t(width) * size_t(height))
|
|
return;
|
|
|
|
const int whole = std::max(1, int(std::ceil(radius)));
|
|
const float mix = std::clamp(radius / float(whole), 0.f, 1.f);
|
|
const std::vector<uint8_t> raw = (mix < 0.999f) ? pixels : std::vector<uint8_t>{};
|
|
smooth_height_pixels_box(pixels, width, height, whole);
|
|
if (!raw.empty())
|
|
for (size_t i = 0; i < pixels.size(); ++i)
|
|
pixels[i] = uint8_t(std::lround(float(raw[i]) + (float(pixels[i]) - float(raw[i])) * mix));
|
|
}
|
|
} // namespace
|
|
|
|
DecodedHeightTexture decode_height_texture(const TextureDisplacementLayer &layer)
|
|
{
|
|
DecodedHeightTexture result;
|
|
if (layer.empty())
|
|
return result;
|
|
|
|
// The raw (unsmoothed) decode is what gets cached, keyed by the image_data allocation - decoding
|
|
// a PNG is the expensive part and never changes for a given image. Smoothing is applied afterwards
|
|
// to a throwaway copy, so moving the smoothing slider never invalidates the decode cache.
|
|
const void *key = layer.image_data.get();
|
|
bool have_raw = false;
|
|
{
|
|
std::lock_guard<std::mutex> lock(g_decoded_texture_cache.mutex);
|
|
auto it = g_decoded_texture_cache.entries.find(key);
|
|
if (it != g_decoded_texture_cache.entries.end() && it->second.first.lock() == layer.image_data) {
|
|
result = it->second.second;
|
|
have_raw = true;
|
|
}
|
|
}
|
|
|
|
if (!have_raw) {
|
|
const png::ReadBuf rbuf{ layer.image_data->data(), layer.image_data->size() };
|
|
if (!png::is_png(rbuf))
|
|
// PNG only. The GUI converts any other imported format (jpg, bmp, ...) on import, so this
|
|
// code needs no dependency on wxWidgets/libjpeg to read arbitrary user images.
|
|
return result;
|
|
|
|
png::ImageGreyscale img;
|
|
if (png::decode_png(rbuf, img) && img.cols > 0 && img.rows > 0) {
|
|
// The shipped library, and anything imported before colour was kept.
|
|
result.width = int(img.cols);
|
|
result.height = int(img.rows);
|
|
result.pixels = std::move(img.buf);
|
|
} else {
|
|
// A colour source: keep the colour, and take the height from its luminance. The
|
|
// coefficients are wxImage::ConvertToGreyscale()'s, which is what the importer used to
|
|
// apply on the way in - so a texture that used to be flattened to grey at import time
|
|
// displaces identically now that its colour is preserved.
|
|
// The loop below steps bytes_per_pixel per texel, which only holds at 8 bits per channel:
|
|
// decode_colored_png() does not narrow a 16-bit image, so that would read as noise.
|
|
png::ImageColorscale col;
|
|
if (!png::decode_colored_png(rbuf, col) || col.cols == 0 || col.rows == 0 ||
|
|
col.bytes_per_pixel < 3 || col.buf.size() != col.cols * col.rows * size_t(col.bytes_per_pixel))
|
|
return result;
|
|
|
|
const int w = int(col.cols);
|
|
const int h = int(col.rows);
|
|
const size_t bpp = size_t(col.bytes_per_pixel);
|
|
result.width = w;
|
|
result.height = h;
|
|
result.pixels.resize(size_t(w) * size_t(h));
|
|
result.rgb.resize(size_t(w) * size_t(h) * 3);
|
|
// decode_colored_png() hands its buffer back bottom-up - it is shared with the CLI's
|
|
// plate-thumbnail loader, which expects that - while this type, and decode_png()'s
|
|
// grayscale path above, are top-to-bottom. Reverse the rows on the way in so a colour
|
|
// height map displaces the same way up as a grayscale one.
|
|
for (int y = 0; y < h; ++y)
|
|
for (int x = 0; x < w; ++x) {
|
|
const uint8_t *src = col.buf.data() + (size_t(h - 1 - y) * size_t(w) + size_t(x)) * bpp;
|
|
const size_t dst = size_t(y) * size_t(w) + size_t(x);
|
|
const uint8_t r = src[0], g = src[1], b = src[2];
|
|
result.rgb[dst * 3] = r;
|
|
result.rgb[dst * 3 + 1] = g;
|
|
result.rgb[dst * 3 + 2] = b;
|
|
result.pixels[dst] = uint8_t(std::lround(0.299 * r + 0.587 * g + 0.114 * b));
|
|
}
|
|
}
|
|
|
|
std::lock_guard<std::mutex> lock(g_decoded_texture_cache.mutex);
|
|
// Opportunistically drop entries for image_data that no longer exists anywhere, so the
|
|
// cache doesn't grow without bound across many add/remove-texture cycles in a long session.
|
|
auto &entries = g_decoded_texture_cache.entries;
|
|
for (auto it = entries.begin(); it != entries.end();)
|
|
it = it->second.first.expired() ? entries.erase(it) : std::next(it);
|
|
entries[key] = { std::weak_ptr<const std::vector<unsigned char>>(layer.image_data), result };
|
|
}
|
|
|
|
// Smoothing radius scales with the texture so the same slider value blurs the same *fraction* of
|
|
// the image whatever resolution it came in at, and stays continuous in the slider - see
|
|
// smooth_height_pixels(). The cap is a cost limit, not part of the mapping: the blur is
|
|
// O(width * height * radius) per pass, so a large map with the slider at the top would otherwise
|
|
// stall every preview rebuild.
|
|
if (layer.smoothing > 0.f) {
|
|
{
|
|
std::lock_guard<std::mutex> lock(g_smoothed_texture_cache.mutex);
|
|
auto it = g_smoothed_texture_cache.entries.find(key);
|
|
if (it != g_smoothed_texture_cache.entries.end() && it->second.smoothing == layer.smoothing &&
|
|
it->second.source.lock() == layer.image_data)
|
|
return it->second.texture;
|
|
}
|
|
const float span = 0.05f * float(std::min(result.width, result.height));
|
|
const float radius = std::clamp(layer.smoothing, 0.f, 1.f) * std::min(span, 48.f);
|
|
smooth_height_pixels(result.pixels, result.width, result.height, radius);
|
|
// Colour gets the same blur, per channel. It is the same knob for the same reason: detail in
|
|
// the image finer than the mesh can carry is noise either way, and low-passing it here is the
|
|
// cheapest place to remove it - one blur of the texture, rather than a fight per triangle.
|
|
if (result.has_color()) {
|
|
const size_t n = size_t(result.width) * size_t(result.height);
|
|
std::vector<uint8_t> channel(n);
|
|
for (int c = 0; c < 3; ++c) {
|
|
for (size_t i = 0; i < n; ++i)
|
|
channel[i] = result.rgb[i * 3 + size_t(c)];
|
|
smooth_height_pixels(channel, result.width, result.height, radius);
|
|
for (size_t i = 0; i < n; ++i)
|
|
result.rgb[i * 3 + size_t(c)] = channel[i];
|
|
}
|
|
}
|
|
std::lock_guard<std::mutex> lock(g_smoothed_texture_cache.mutex);
|
|
auto &entries = g_smoothed_texture_cache.entries;
|
|
for (auto it = entries.begin(); it != entries.end();)
|
|
it = it->second.source.expired() ? entries.erase(it) : std::next(it);
|
|
entries[key] = { std::weak_ptr<const std::vector<unsigned char>>(layer.image_data), layer.smoothing, result };
|
|
}
|
|
return result;
|
|
}
|
|
|
|
namespace {
|
|
struct TextureDetailCache
|
|
{
|
|
std::mutex mutex;
|
|
std::unordered_map<const void *, std::pair<std::weak_ptr<const std::vector<unsigned char>>, TextureDetail>> entries;
|
|
};
|
|
TextureDetailCache g_texture_detail_cache;
|
|
} // namespace
|
|
|
|
TextureDetail analyze_texture_detail(const TextureDisplacementLayer &layer)
|
|
{
|
|
TextureDetail out;
|
|
if (layer.empty())
|
|
return out;
|
|
const void *key = layer.image_data.get();
|
|
{
|
|
std::lock_guard<std::mutex> lock(g_texture_detail_cache.mutex);
|
|
auto it = g_texture_detail_cache.entries.find(key);
|
|
if (it != g_texture_detail_cache.entries.end() && it->second.first.lock() == layer.image_data)
|
|
return it->second.second;
|
|
}
|
|
// On the image as imported: the Smoothing slider must not move the recommendation around.
|
|
TextureDisplacementLayer raw = layer;
|
|
raw.smoothing = 0.f;
|
|
const DecodedHeightTexture tex = decode_height_texture(raw);
|
|
const int w = tex.width, h = tex.height;
|
|
if (w >= 3 && h >= 3) {
|
|
double sum = 0.0;
|
|
size_t sharp = 0, n = 0;
|
|
for (int y = 1; y < h - 1; ++y) {
|
|
const uint8_t *row = tex.pixels.data() + size_t(y) * size_t(w);
|
|
for (int x = 1; x < w - 1; ++x) {
|
|
const float dx = 0.5f * (float(row[x + 1]) - float(row[x - 1]));
|
|
const float dy = 0.5f * (float(row[x + w]) - float(row[x - w]));
|
|
const float mag = std::sqrt(dx * dx + dy * dy);
|
|
sum += mag;
|
|
sharp += mag > 30.f;
|
|
++n;
|
|
}
|
|
}
|
|
out.mean_gradient = float(sum / double(n));
|
|
out.sharp_fraction = float(sharp) / float(n);
|
|
if (out.sharp_fraction > 0.15f || out.mean_gradient > 50.f) out.pixels_per_edge = 1.f;
|
|
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;
|
|
for (auto it = entries.begin(); it != entries.end();)
|
|
it = it->second.first.expired() ? entries.erase(it) : std::next(it);
|
|
entries[key] = { std::weak_ptr<const std::vector<unsigned char>>(layer.image_data), out };
|
|
return out;
|
|
}
|
|
|
|
V2Resolution recommend_v2_resolution(const indexed_triangle_set &mesh,
|
|
const std::vector<TextureDisplacementLayer> &layers,
|
|
const Transform3d &volume_to_world)
|
|
{
|
|
// The defaults on model load: edge = diagonal / 250 in [0.05, 5] mm, budget 750 k. A texture-driven
|
|
// variant (resolution from the texture's own detail) was measured to give better walls on step
|
|
// textures at 2-10x the bake time and up to 2 M output triangles, and was not worth that; these
|
|
// defaults stayed. 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;
|
|
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 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;
|
|
}
|
|
}
|
|
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);
|
|
}
|
|
const double diag = (bmax - bmin).norm();
|
|
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;
|
|
}
|
|
|
|
Vec2f project_planar(const Vec3f &position, const Vec3f &normal)
|
|
{
|
|
// Planar-project onto the two axes orthogonal to the dominant component of `normal`. Called
|
|
// with each vertex's *own* normal (TextureProjectionMethod::Triplanar), this is a standard
|
|
// tri-planar/cube projection; a patch spanning several differently-oriented faces gets each
|
|
// face projected along its own best-fit axis instead of all faces sharing one axis picked
|
|
// from a single averaged normal (which looks correct on one face but visibly distorts on any
|
|
// other face in the same patch - exactly the bug an earlier version of this feature had).
|
|
const Vec3f n = normal.cwiseAbs();
|
|
if (n.x() >= n.y() && n.x() >= n.z())
|
|
return Vec2f(position.y(), position.z());
|
|
if (n.y() >= n.x() && n.y() >= n.z())
|
|
return Vec2f(position.x(), position.z());
|
|
return Vec2f(position.x(), position.y());
|
|
}
|
|
|
|
namespace {
|
|
// Wrapped around patch_axis, centered at patch_center. u is the arc length (mm) around the axis at
|
|
// this point's own radius, v is the signed distance along the axis - a reasonable approximation
|
|
// for roughly cylindrical selections, not an exact fit for arbitrary geometry.
|
|
Vec2f project_cylindrical(const Vec3f &position, const Vec3f &patch_center, const Vec3f &patch_axis)
|
|
{
|
|
Vec3f up = patch_axis;
|
|
up = (up.norm() > 1e-8f) ? Vec3f(up.normalized()) : Vec3f::UnitZ();
|
|
const Vec3f arbitrary = (std::abs(up.dot(Vec3f::UnitZ())) < 0.9f) ? Vec3f::UnitZ() : Vec3f::UnitX();
|
|
const Vec3f right = Vec3f(up.cross(arbitrary).normalized());
|
|
const Vec3f fwd = Vec3f(right.cross(up).normalized());
|
|
|
|
const Vec3f rel = position - patch_center;
|
|
const float along_axis = rel.dot(up);
|
|
const float x = rel.dot(right);
|
|
const float y = rel.dot(fwd);
|
|
const float radius = std::sqrt(x * x + y * y);
|
|
const float angle = std::atan2(y, x);
|
|
|
|
return Vec2f(angle * radius, along_axis);
|
|
}
|
|
|
|
// Longitude/latitude around patch_center. u/v are scaled by this point's own distance from the
|
|
// center so the result is in roughly the same mm-ish units tiling_scale expects, rather than bare
|
|
// radians - again an approximation, not an exact geodesic parametrization.
|
|
Vec2f project_spherical(const Vec3f &position, const Vec3f &patch_center)
|
|
{
|
|
const Vec3f rel = position - patch_center;
|
|
const float radius = rel.norm();
|
|
if (radius < 1e-8f)
|
|
return Vec2f::Zero();
|
|
|
|
const Vec3f dir = rel / radius;
|
|
const float longitude = std::atan2(dir.y(), dir.x());
|
|
const float latitude = std::asin(std::clamp(dir.z(), -1.f, 1.f));
|
|
return Vec2f(longitude, latitude) * radius;
|
|
}
|
|
|
|
// CGAL's LSCM parameterizer expects a clean mesh with no isolated (unreferenced) vertices - but
|
|
// `patch` here (from TriangleSelector::get_facets_strict()) carries the *entire* mesh's vertex
|
|
// array, only its `indices` filtered to the painted triangles. Build a compacted copy referencing
|
|
// only the vertices `patch.indices` actually uses, plus a map back to the original vertex index so
|
|
// the resulting per-vertex UVs can be looked up by the caller's own (uncompacted) indexing.
|
|
indexed_triangle_set compact_patch_with_map(const indexed_triangle_set &patch, std::vector<int> &original_to_compact)
|
|
{
|
|
original_to_compact.assign(patch.vertices.size(), -1);
|
|
indexed_triangle_set compact;
|
|
compact.vertices.reserve(patch.vertices.size());
|
|
compact.indices.reserve(patch.indices.size());
|
|
for (const stl_triangle_vertex_indices &tri : patch.indices) {
|
|
stl_triangle_vertex_indices new_tri;
|
|
for (int i = 0; i < 3; ++i) {
|
|
const int vi = tri[i];
|
|
if (original_to_compact[vi] < 0) {
|
|
original_to_compact[vi] = int(compact.vertices.size());
|
|
compact.vertices.push_back(patch.vertices[vi]);
|
|
}
|
|
new_tri[i] = original_to_compact[vi];
|
|
}
|
|
compact.indices.push_back(new_tri);
|
|
}
|
|
return compact;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
namespace {
|
|
|
|
// Union-find over triangles, used to grow charts.
|
|
struct UnionFind
|
|
{
|
|
std::vector<int> parent;
|
|
explicit UnionFind(size_t n) : parent(n) { std::iota(parent.begin(), parent.end(), 0); }
|
|
int find(int x)
|
|
{
|
|
while (parent[size_t(x)] != x) {
|
|
parent[size_t(x)] = parent[size_t(parent[size_t(x)])]; // path halving
|
|
x = parent[size_t(x)];
|
|
}
|
|
return x;
|
|
}
|
|
void unite(int a, int b)
|
|
{
|
|
a = find(a);
|
|
b = find(b);
|
|
if (a != b)
|
|
parent[size_t(b)] = a;
|
|
}
|
|
};
|
|
|
|
uint64_t undirected_edge_key(int a, int b)
|
|
{
|
|
if (a > b)
|
|
std::swap(a, b);
|
|
return (uint64_t(uint32_t(a)) << 32) | uint32_t(b);
|
|
}
|
|
|
|
Vec3f face_normal(const indexed_triangle_set &mesh, const stl_triangle_vertex_indices &tri)
|
|
{
|
|
const Vec3f n = (mesh.vertices[tri[1]] - mesh.vertices[tri[0]]).cross(mesh.vertices[tri[2]] - mesh.vertices[tri[0]]);
|
|
const float len = n.norm();
|
|
return (len > 1e-12f) ? Vec3f(n / len) : Vec3f::UnitZ();
|
|
}
|
|
|
|
// Whether a chart can be laid flat as one island: a topological disk (V - E + F = 1 with a single boundary loop,
|
|
// which LSCM requires) whose faces all point within ~100 degrees of their average - past that even a disk folds
|
|
// over itself when flattened. Degenerate faces carry no direction and are left out of the normal test.
|
|
bool chart_is_flattenable(const indexed_triangle_set &mesh, const std::vector<Vec3f> &normals, const std::vector<int> &faces)
|
|
{
|
|
constexpr float MIN_NORMAL_COS = -0.17f; // cos(100 deg)
|
|
|
|
std::unordered_map<uint64_t, int> edge_use;
|
|
std::unordered_map<int, int> local_vertex;
|
|
edge_use.reserve(faces.size() * 2);
|
|
local_vertex.reserve(faces.size());
|
|
Vec3f normal_sum = Vec3f::Zero();
|
|
for (const int f : faces) {
|
|
const stl_triangle_vertex_indices &tri = mesh.indices[size_t(f)];
|
|
for (int i = 0; i < 3; ++i) {
|
|
++edge_use[undirected_edge_key(tri[i], tri[(i + 1) % 3])];
|
|
local_vertex.emplace(tri[i], int(local_vertex.size()));
|
|
}
|
|
normal_sum += normals[size_t(f)];
|
|
}
|
|
if (int(local_vertex.size()) - int(edge_use.size()) + int(faces.size()) != 1)
|
|
return false;
|
|
|
|
UnionFind loops(local_vertex.size());
|
|
int boundary_seed = -1;
|
|
for (const auto &[key, uses] : edge_use) {
|
|
if (uses > 2)
|
|
return false; // non-manifold
|
|
if (uses == 1) {
|
|
const int a = local_vertex[int(key >> 32)], b = local_vertex[int(uint32_t(key))];
|
|
loops.unite(a, b);
|
|
boundary_seed = a;
|
|
}
|
|
}
|
|
if (boundary_seed < 0)
|
|
return false; // closed
|
|
const int loop = loops.find(boundary_seed);
|
|
for (const auto &[key, uses] : edge_use)
|
|
if (uses == 1 && loops.find(local_vertex[int(key >> 32)]) != loop)
|
|
return false; // a second boundary loop: a ring, or a disk with a hole
|
|
|
|
const float len = normal_sum.norm();
|
|
if (len < 1e-6f)
|
|
return false;
|
|
const Vec3f mean = normal_sum / len;
|
|
for (const int f : faces) {
|
|
const stl_triangle_vertex_indices &tri = mesh.indices[size_t(f)];
|
|
const bool degenerate = (mesh.vertices[tri[1]] - mesh.vertices[tri[0]]).cross(mesh.vertices[tri[2]] - mesh.vertices[tri[0]])
|
|
.squaredNorm() < 1e-20f;
|
|
if (!degenerate && normals[size_t(f)].dot(mean) < MIN_NORMAL_COS)
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// Which side of a cut each face of an unflattenable chart goes to (true / false, parallel to `faces`). The cut
|
|
// separates the faces' normals along their widest spread, so a tube splits lengthwise into two half-tubes and a
|
|
// closed sphere into two hemispheres. Where the normals barely vary (a flat ring) it splits the face centroids
|
|
// along their longest axis instead.
|
|
std::vector<char> split_chart_sides(const indexed_triangle_set &mesh, const std::vector<Vec3f> &normals, const std::vector<int> &faces)
|
|
{
|
|
const auto principal_axis = [](const std::vector<Vec3f> &samples, Vec3f &mean, Vec3f &axis) {
|
|
mean = Vec3f::Zero();
|
|
for (const Vec3f &s : samples)
|
|
mean += s;
|
|
mean /= float(samples.size());
|
|
Eigen::Matrix3f covariance = Eigen::Matrix3f::Zero();
|
|
for (const Vec3f &s : samples) {
|
|
const Vec3f d = s - mean;
|
|
covariance += d * d.transpose();
|
|
}
|
|
const Eigen::SelfAdjointEigenSolver<Eigen::Matrix3f> solver(covariance);
|
|
axis = solver.eigenvectors().col(2); // eigenvalues come sorted ascending
|
|
return solver.eigenvalues()(2) / float(samples.size());
|
|
};
|
|
|
|
std::vector<Vec3f> samples(faces.size());
|
|
for (size_t i = 0; i < faces.size(); ++i)
|
|
samples[i] = normals[size_t(faces[i])];
|
|
Vec3f mean, axis;
|
|
if (principal_axis(samples, mean, axis) < 1e-3f) {
|
|
for (size_t i = 0; i < faces.size(); ++i) {
|
|
const stl_triangle_vertex_indices &tri = mesh.indices[size_t(faces[i])];
|
|
samples[i] = (mesh.vertices[tri[0]] + mesh.vertices[tri[1]] + mesh.vertices[tri[2]]) / 3.f;
|
|
}
|
|
principal_axis(samples, mean, axis);
|
|
}
|
|
std::vector<char> side(faces.size());
|
|
for (size_t i = 0; i < faces.size(); ++i)
|
|
side[i] = (samples[i] - mean).dot(axis) > 0.f;
|
|
return side;
|
|
}
|
|
|
|
// Groups triangles into charts. First two triangles sharing an edge join the same chart only if the angle between
|
|
// their normals is below `seam_angle_deg` (and the edge is not a marked seam): everything sharper is a seam. The
|
|
// test uses each face's own normal - averaging in its neighbours, as this once did, cuts low-poly flat faces apart,
|
|
// because the two triangles of one cube face average in different neighbouring faces.
|
|
//
|
|
// Then every chart that cannot be laid flat as one island (see chart_is_flattenable()) is cut in two and each
|
|
// connected piece checked again: a cylinder's smooth side is one chart by angle but a ring, which no flattening
|
|
// can open without a cut, and a sphere is closed.
|
|
std::vector<int> segment_into_charts(const indexed_triangle_set &mesh, const std::vector<Vec3f> &normals,
|
|
float seam_angle_deg, const std::unordered_set<uint64_t> &seam_keys,
|
|
int &chart_count)
|
|
{
|
|
constexpr int MAX_SPLIT_DEPTH = 8;
|
|
const int n_faces = int(mesh.indices.size());
|
|
const float cos_threshold = std::cos(std::clamp(seam_angle_deg, 0.f, 180.f) * float(M_PI) / 180.f);
|
|
|
|
// Each shared edge, with the (up to two) faces on it.
|
|
std::unordered_map<uint64_t, std::pair<int, int>> edge_faces;
|
|
edge_faces.reserve(size_t(n_faces) * 3);
|
|
for (int f = 0; f < n_faces; ++f) {
|
|
const stl_triangle_vertex_indices &tri = mesh.indices[size_t(f)];
|
|
for (int i = 0; i < 3; ++i) {
|
|
const uint64_t key = undirected_edge_key(tri[i], tri[(i + 1) % 3]);
|
|
const auto [it, inserted] = edge_faces.emplace(key, std::make_pair(f, -1));
|
|
if (!inserted)
|
|
it->second.second = f;
|
|
}
|
|
}
|
|
|
|
// Faces joined across each edge that does not cut. A face has three edges, so at most three such neighbours.
|
|
std::vector<std::array<int, 3>> adjacent(static_cast<size_t>(n_faces), { -1, -1, -1 });
|
|
const auto link = [&adjacent](int f, int nb) {
|
|
for (int &slot : adjacent[size_t(f)])
|
|
if (slot < 0) {
|
|
slot = nb;
|
|
return;
|
|
}
|
|
};
|
|
for (const auto &[key, fp] : edge_faces) {
|
|
if (fp.second < 0 || fp.first == fp.second)
|
|
continue; // a boundary edge of the patch, nothing on the far side to join
|
|
// A manually/auto marked seam always cuts, whatever the dihedral angle - that is exactly
|
|
// what lets "mark seam" / "cut island" split a chart that is otherwise flat enough to merge.
|
|
if (!seam_keys.empty() && seam_keys.count(key))
|
|
continue;
|
|
if (normals[size_t(fp.first)].dot(normals[size_t(fp.second)]) >= cos_threshold) {
|
|
link(fp.first, fp.second);
|
|
link(fp.second, fp.first);
|
|
}
|
|
}
|
|
|
|
// Connected pieces of `faces` over `adjacent`, staying within each face's current `group`.
|
|
std::vector<int> group(static_cast<size_t>(n_faces), 0), visited(static_cast<size_t>(n_faces), -1);
|
|
int visit_pass = 0;
|
|
const auto connected_pieces = [&](const std::vector<int> &faces, std::vector<std::vector<int>> &out) {
|
|
const int pass = visit_pass++;
|
|
std::vector<int> stack;
|
|
for (const int seed : faces) {
|
|
if (visited[size_t(seed)] == pass)
|
|
continue;
|
|
visited[size_t(seed)] = pass;
|
|
std::vector<int> piece{ seed };
|
|
stack.assign(1, seed);
|
|
while (!stack.empty()) {
|
|
const int f = stack.back();
|
|
stack.pop_back();
|
|
for (const int nb : adjacent[size_t(f)])
|
|
if (nb >= 0 && visited[size_t(nb)] != pass && group[size_t(nb)] == group[size_t(f)]) {
|
|
visited[size_t(nb)] = pass;
|
|
piece.push_back(nb);
|
|
stack.push_back(nb);
|
|
}
|
|
}
|
|
out.push_back(std::move(piece));
|
|
}
|
|
};
|
|
|
|
std::vector<int> all_faces(static_cast<size_t>(n_faces));
|
|
std::iota(all_faces.begin(), all_faces.end(), 0);
|
|
std::vector<std::vector<int>> pieces;
|
|
connected_pieces(all_faces, pieces);
|
|
std::vector<std::pair<std::vector<int>, int>> pending; // a piece, and how many cuts produced it
|
|
for (std::vector<int> &piece : pieces)
|
|
pending.emplace_back(std::move(piece), 0);
|
|
|
|
std::vector<std::vector<int>> charts;
|
|
int next_group = 1;
|
|
while (!pending.empty()) {
|
|
std::vector<int> faces = std::move(pending.back().first);
|
|
const int depth = pending.back().second;
|
|
pending.pop_back();
|
|
if (faces.size() < 2 || depth >= MAX_SPLIT_DEPTH || chart_is_flattenable(mesh, normals, faces)) {
|
|
charts.push_back(std::move(faces));
|
|
continue;
|
|
}
|
|
const std::vector<char> side = split_chart_sides(mesh, normals, faces);
|
|
std::vector<int> halves[2];
|
|
for (size_t i = 0; i < faces.size(); ++i)
|
|
halves[side[i] ? 1 : 0].push_back(faces[i]);
|
|
if (halves[0].empty() || halves[1].empty()) {
|
|
charts.push_back(std::move(faces));
|
|
continue;
|
|
}
|
|
for (std::vector<int> &half : halves) {
|
|
for (const int f : half)
|
|
group[size_t(f)] = next_group;
|
|
++next_group;
|
|
pieces.clear();
|
|
connected_pieces(half, pieces);
|
|
for (std::vector<int> &piece : pieces)
|
|
pending.emplace_back(std::move(piece), depth + 1);
|
|
}
|
|
}
|
|
|
|
// Chart ids in first-encountered-triangle order, which TextureIsland indexing documents.
|
|
std::vector<std::pair<int, size_t>> first_face(charts.size());
|
|
for (size_t c = 0; c < charts.size(); ++c)
|
|
first_face[c] = { *std::min_element(charts[c].begin(), charts[c].end()), c };
|
|
std::sort(first_face.begin(), first_face.end());
|
|
|
|
std::vector<int> chart_of(static_cast<size_t>(n_faces), -1);
|
|
for (size_t id = 0; id < first_face.size(); ++id)
|
|
for (const int f : charts[first_face[id].second])
|
|
chart_of[size_t(f)] = int(id);
|
|
chart_count = int(charts.size());
|
|
return chart_of;
|
|
}
|
|
|
|
// Projects a chart onto an orthonormal basis of its own average normal. This is *isometric* for a
|
|
// flat chart - lengths and angles come out exactly right - which is why a flat chart never needs a
|
|
// solve at all, and why this also serves as the fallback for a chart LSCM cannot handle.
|
|
std::vector<Vec2f> project_to_tangent_plane(const indexed_triangle_set &chart, const Vec3f &normal)
|
|
{
|
|
const Vec3f seed = (std::abs(normal.z()) < 0.9f) ? Vec3f::UnitZ() : Vec3f::UnitX();
|
|
const Vec3f u = Vec3f(seed.cross(normal).normalized());
|
|
const Vec3f v = Vec3f(normal.cross(u).normalized());
|
|
|
|
std::vector<Vec2f> uvs(chart.vertices.size());
|
|
for (size_t i = 0; i < chart.vertices.size(); ++i)
|
|
uvs[i] = Vec2f(chart.vertices[i].dot(u), chart.vertices[i].dot(v));
|
|
return uvs;
|
|
}
|
|
|
|
float area_3d(const indexed_triangle_set &mesh)
|
|
{
|
|
float area = 0.f;
|
|
for (const stl_triangle_vertex_indices &t : mesh.indices)
|
|
area += 0.5f * (mesh.vertices[t[1]] - mesh.vertices[t[0]]).cross(mesh.vertices[t[2]] - mesh.vertices[t[0]]).norm();
|
|
return area;
|
|
}
|
|
|
|
float area_2d(const std::vector<Vec2f> &uvs, const std::vector<stl_triangle_vertex_indices> &indices)
|
|
{
|
|
float area = 0.f;
|
|
for (const stl_triangle_vertex_indices &t : indices) {
|
|
const Vec2f e0 = uvs[size_t(t[1])] - uvs[size_t(t[0])];
|
|
const Vec2f e1 = uvs[size_t(t[2])] - uvs[size_t(t[0])];
|
|
area += 0.5f * std::abs(e0.x() * e1.y() - e0.y() * e1.x());
|
|
}
|
|
return area;
|
|
}
|
|
|
|
// FNV-1a over the patch's geometry plus the seam angle. The unwrap depends on nothing else about a
|
|
// layer - not depth, tiling, rotation, offset or even which texture is on it - so keying the cache
|
|
// on just this is what lets every one of those sliders be dragged without paying for a re-solve.
|
|
uint64_t unwrap_cache_key(const indexed_triangle_set &patch, float seam_angle_deg, float padding_mm,
|
|
const std::vector<std::pair<int, int>> &seam_edges)
|
|
{
|
|
uint64_t h = 1469598103934665603ull;
|
|
const auto mix = [&h](const void *data, size_t bytes) {
|
|
const unsigned char *p = static_cast<const unsigned char *>(data);
|
|
for (size_t i = 0; i < bytes; ++i) {
|
|
h ^= p[i];
|
|
h *= 1099511628211ull;
|
|
}
|
|
};
|
|
mix(patch.vertices.data(), patch.vertices.size() * sizeof(Vec3f));
|
|
mix(patch.indices.data(), patch.indices.size() * sizeof(stl_triangle_vertex_indices));
|
|
mix(&seam_angle_deg, sizeof(seam_angle_deg));
|
|
mix(&padding_mm, sizeof(padding_mm));
|
|
mix(seam_edges.data(), seam_edges.size() * sizeof(std::pair<int, int>));
|
|
return h;
|
|
}
|
|
|
|
struct UnwrapCache
|
|
{
|
|
std::mutex mutex;
|
|
std::unordered_map<uint64_t, PatchUnwrap> entries;
|
|
};
|
|
UnwrapCache g_unwrap_cache;
|
|
const size_t UNWRAP_CACHE_MAX_ENTRIES = 8;
|
|
|
|
} // namespace
|
|
|
|
PatchUnwrap compute_patch_unwrap(const indexed_triangle_set &patch, float seam_angle_deg, float padding_mm,
|
|
const std::vector<std::pair<int, int>> &seam_edges)
|
|
{
|
|
PatchUnwrap result;
|
|
if (patch.indices.empty())
|
|
return result;
|
|
|
|
const uint64_t key = unwrap_cache_key(patch, seam_angle_deg, padding_mm, seam_edges);
|
|
{
|
|
std::lock_guard<std::mutex> lock(g_unwrap_cache.mutex);
|
|
if (auto it = g_unwrap_cache.entries.find(key); it != g_unwrap_cache.entries.end())
|
|
return it->second;
|
|
}
|
|
|
|
// CGAL wants a mesh with no unreferenced vertices; `patch` carries the whole mesh's vertex array
|
|
// (see get_facets_strict()), so compact it and keep the map back to the caller's numbering.
|
|
std::vector<int> patch_to_compact;
|
|
const indexed_triangle_set compact = compact_patch_with_map(patch, patch_to_compact);
|
|
std::vector<int> compact_to_patch(compact.vertices.size(), -1);
|
|
for (size_t vi = 0; vi < patch_to_compact.size(); ++vi)
|
|
if (patch_to_compact[vi] >= 0)
|
|
compact_to_patch[size_t(patch_to_compact[vi])] = int(vi);
|
|
|
|
std::vector<Vec3f> normals(compact.indices.size());
|
|
for (size_t f = 0; f < compact.indices.size(); ++f)
|
|
normals[f] = face_normal(compact, compact.indices[f]);
|
|
|
|
// Seam edges arrive in patch (== mesh) vertex space; translate to the compact numbering the
|
|
// segmentation runs in. An edge whose endpoints didn't both survive compaction is simply ignored.
|
|
std::unordered_set<uint64_t> seam_keys;
|
|
seam_keys.reserve(seam_edges.size() * 2);
|
|
for (const auto &[a, b] : seam_edges) {
|
|
if (a < 0 || b < 0 || size_t(a) >= patch_to_compact.size() || size_t(b) >= patch_to_compact.size())
|
|
continue;
|
|
const int ca = patch_to_compact[size_t(a)];
|
|
const int cb = patch_to_compact[size_t(b)];
|
|
if (ca >= 0 && cb >= 0)
|
|
seam_keys.insert(undirected_edge_key(ca, cb));
|
|
}
|
|
|
|
// Once the user has marked seams by hand, those seams define the islands: the automatic
|
|
// sharp-angle cutting must not keep splitting faces the user left un-seamed, or islands the user
|
|
// meant to be one piece never merge (the "seam angle overrides it" the user hit). So when any
|
|
// manual seam exists, the angle threshold is dropped to 180 degrees - nothing is cut except the
|
|
// marked seams. With no manual seams, the seam angle behaves exactly as before.
|
|
const float effective_seam_angle = seam_keys.empty() ? seam_angle_deg : 180.f;
|
|
int chart_count = 0;
|
|
const std::vector<int> chart_of = segment_into_charts(compact, normals, effective_seam_angle, seam_keys, chart_count);
|
|
|
|
struct FlatChart
|
|
{
|
|
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();
|
|
};
|
|
// static_cast, not size_t(...): the latter is a most-vexing-parse and declares a function.
|
|
std::vector<FlatChart> charts(static_cast<size_t>(chart_count));
|
|
|
|
for (int c = 0; c < chart_count; ++c) {
|
|
FlatChart &chart = charts[size_t(c)];
|
|
|
|
// Build the chart's own sub-mesh. Each chart gets its *own* compact->local vertex map, which
|
|
// is exactly what duplicates a seam vertex: it appears once in each chart that touches it,
|
|
// free to hold a different UV in each.
|
|
indexed_triangle_set chart_mesh;
|
|
std::vector<int> compact_to_local(compact.vertices.size(), -1);
|
|
Vec3f normal_sum = Vec3f::Zero();
|
|
for (size_t f = 0; f < compact.indices.size(); ++f) {
|
|
if (chart_of[f] != c)
|
|
continue;
|
|
const stl_triangle_vertex_indices &tri = compact.indices[f];
|
|
// Area-weighted, so a chart's average normal isn't dragged around by slivers.
|
|
normal_sum += (compact.vertices[tri[1]] - compact.vertices[tri[0]]).cross(compact.vertices[tri[2]] - compact.vertices[tri[0]]);
|
|
|
|
stl_triangle_vertex_indices local_tri;
|
|
for (int i = 0; i < 3; ++i) {
|
|
const int cv = tri[i];
|
|
if (compact_to_local[size_t(cv)] < 0) {
|
|
compact_to_local[size_t(cv)] = int(chart_mesh.vertices.size());
|
|
chart_mesh.vertices.push_back(compact.vertices[size_t(cv)]);
|
|
chart.to_patch.push_back(compact_to_patch[size_t(cv)]);
|
|
}
|
|
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;
|
|
|
|
const Vec3f chart_normal = (normal_sum.norm() > 1e-12f) ? Vec3f(normal_sum.normalized()) : Vec3f::UnitZ();
|
|
|
|
// Is the chart flat? Charts are grown by a *pairwise* angle threshold, so a chart can still
|
|
// curve gradually across many triangles - being merged is not the same as being planar. But
|
|
// when it is planar (a cube face, and after seam-cutting that is the common case), the
|
|
// tangent-plane projection is already the exact answer, and skipping the solve is the single
|
|
// biggest speed-up here.
|
|
bool planar = true;
|
|
for (size_t f = 0; f < compact.indices.size() && planar; ++f)
|
|
if (chart_of[f] == c)
|
|
planar = normals[f].dot(chart_normal) >= 0.9998f; // ~1 degree
|
|
|
|
// Measured before chart_mesh.indices is moved out from under it, below - area_3d() iterates
|
|
// those indices, so taking it afterwards silently measures an empty mesh and returns 0.
|
|
const float mesh_area_3d = area_3d(chart_mesh);
|
|
|
|
std::optional<std::vector<Vec2f>> uvs;
|
|
if (!planar)
|
|
uvs = MeshBoolean::cgal::parameterize_lscm(chart_mesh);
|
|
// Flat chart, or one LSCM refused (not a topological disk - closed, or holed).
|
|
chart.uvs = uvs ? std::move(*uvs) : project_to_tangent_plane(chart_mesh, chart_normal);
|
|
chart.indices = std::move(chart_mesh.indices);
|
|
|
|
// LSCM's output is only defined up to a similarity, so charts come back at arbitrary and
|
|
// mutually inconsistent scales. Rescale each to its true surface area, so `Tile size (mm)`
|
|
// means the same thing on every chart and the texture doesn't change density across a seam.
|
|
const float uv_area = area_2d(chart.uvs, chart.indices);
|
|
if (uv_area > 1e-12f && mesh_area_3d > 1e-12f) {
|
|
const float scale = std::sqrt(mesh_area_3d / uv_area);
|
|
for (Vec2f &uv : chart.uvs)
|
|
uv *= scale;
|
|
}
|
|
|
|
Vec2f lo(std::numeric_limits<float>::max(), std::numeric_limits<float>::max());
|
|
Vec2f hi(std::numeric_limits<float>::lowest(), std::numeric_limits<float>::lowest());
|
|
for (const Vec2f &uv : chart.uvs) {
|
|
lo = lo.cwiseMin(uv);
|
|
hi = hi.cwiseMax(uv);
|
|
}
|
|
chart.min = lo;
|
|
chart.size = hi - lo;
|
|
}
|
|
|
|
// Shelf-pack the charts side by side so they don't overlap. Overlap would be harmless for a
|
|
// repeating texture but wrong for a non-tiled (decal) one, and it makes the UV editor unreadable.
|
|
float total_area = 0.f;
|
|
float widest = 0.f;
|
|
for (const FlatChart &chart : charts) {
|
|
total_area = total_area + chart.size.x() * chart.size.y();
|
|
widest = std::max(widest, chart.size.x());
|
|
}
|
|
const float shelf_width = std::max(widest, std::sqrt(std::max(total_area, 0.f)) * 1.4f);
|
|
// Negative padding means auto: a small fraction of the packed size, which is scale-independent
|
|
// and so does something sensible for a 5 mm patch and a 500 mm one alike.
|
|
const float margin = (padding_mm >= 0.f) ? padding_mm : std::max(shelf_width * 0.02f, 1e-4f);
|
|
|
|
std::vector<int> order(charts.size());
|
|
std::iota(order.begin(), order.end(), 0);
|
|
std::sort(order.begin(), order.end(), // tallest first: the usual way to keep shelves tight
|
|
[&charts](int a, int b) { return charts[size_t(a)].size.y() > charts[size_t(b)].size.y(); });
|
|
|
|
float cursor_x = 0.f, cursor_y = 0.f, row_height = 0.f;
|
|
for (const int c : order) {
|
|
FlatChart &chart = charts[size_t(c)];
|
|
if (chart.uvs.empty())
|
|
continue;
|
|
if (cursor_x > 0.f && cursor_x + chart.size.x() > shelf_width) {
|
|
cursor_x = 0.f;
|
|
cursor_y += row_height + margin;
|
|
row_height = 0.f;
|
|
}
|
|
const Vec2f translation = Vec2f(cursor_x, cursor_y) - chart.min;
|
|
for (Vec2f &uv : chart.uvs)
|
|
uv += translation;
|
|
|
|
cursor_x += chart.size.x() + margin;
|
|
row_height = std::max(row_height, chart.size.y());
|
|
}
|
|
|
|
// Concatenate the packed charts into the flat, duplicated-vertex form PatchUnwrap describes.
|
|
result.chart_centroid.assign(size_t(chart_count), Vec2f::Zero());
|
|
for (int c = 0; c < chart_count; ++c) {
|
|
FlatChart &chart = charts[size_t(c)];
|
|
const int base = int(result.uvs.size());
|
|
result.uvs.insert(result.uvs.end(), chart.uvs.begin(), chart.uvs.end());
|
|
result.source_vertex.insert(result.source_vertex.end(), chart.to_patch.begin(), chart.to_patch.end());
|
|
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();
|
|
for (const Vec2f &uv : chart.uvs)
|
|
sum += uv;
|
|
result.chart_centroid[size_t(c)] = sum / float(chart.uvs.size());
|
|
}
|
|
}
|
|
result.chart_count = chart_count;
|
|
|
|
// Island outlines: an edge used by exactly one triangle. Charts have disjoint vertex sets (seam
|
|
// vertices are duplicated), so an edge along a seam shows up once in each chart and is correctly
|
|
// reported as a boundary of both.
|
|
{
|
|
std::unordered_map<uint64_t, int> edge_use;
|
|
edge_use.reserve(result.indices.size() * 3);
|
|
for (const stl_triangle_vertex_indices &tri : result.indices)
|
|
for (int i = 0; i < 3; ++i)
|
|
++edge_use[undirected_edge_key(tri[i], tri[(i + 1) % 3])];
|
|
for (const stl_triangle_vertex_indices &tri : result.indices)
|
|
for (int i = 0; i < 3; ++i) {
|
|
const int a = tri[i], b = tri[(i + 1) % 3];
|
|
if (edge_use[undirected_edge_key(a, b)] == 1)
|
|
result.boundary_edges.emplace_back(a, b);
|
|
}
|
|
}
|
|
|
|
{
|
|
std::lock_guard<std::mutex> lock(g_unwrap_cache.mutex);
|
|
// A patch changes on every paint stroke, so this would grow without bound over a session.
|
|
// Nothing here is worth an LRU: the working set is "the patch I am editing right now".
|
|
if (g_unwrap_cache.entries.size() >= UNWRAP_CACHE_MAX_ENTRIES)
|
|
g_unwrap_cache.entries.clear();
|
|
g_unwrap_cache.entries.emplace(key, result);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
Eigen::Matrix<float, 2, 3> island_transform_matrix(int chart, const PatchUnwrap &unwrap, const std::vector<TextureIsland> &islands)
|
|
{
|
|
Eigen::Matrix<float, 2, 3> m;
|
|
m << 1.f, 0.f, 0.f,
|
|
0.f, 1.f, 0.f;
|
|
if (chart < 0 || size_t(chart) >= islands.size() || size_t(chart) >= unwrap.chart_centroid.size())
|
|
return m; // no hand placement for this island: leave it where the packing put it
|
|
|
|
const TextureIsland &island = islands[size_t(chart)];
|
|
const Vec2f centre = unwrap.chart_centroid[size_t(chart)];
|
|
|
|
const float rad = island.rotation_deg * float(M_PI) / 180.f;
|
|
const float s = island.scale;
|
|
const float cs = std::cos(rad) * s;
|
|
const float sn = std::sin(rad) * s;
|
|
|
|
// uv -> centre + R*S*(uv - centre) + offset, with the linear part and the constant part split
|
|
// out so this can be handed to a shader as-is.
|
|
Eigen::Matrix2f linear;
|
|
linear << cs, -sn,
|
|
sn, cs;
|
|
const Vec2f translation = centre + island.offset - linear * centre;
|
|
|
|
m.block<2, 2>(0, 0) = linear;
|
|
m.col(2) = translation;
|
|
return m;
|
|
}
|
|
|
|
Vec2f apply_island_transform(const Vec2f &uv, int chart, const PatchUnwrap &unwrap, const std::vector<TextureIsland> &islands)
|
|
{
|
|
const Eigen::Matrix<float, 2, 3> m = island_transform_matrix(chart, unwrap, islands);
|
|
return m.block<2, 2>(0, 0) * uv + m.col(2);
|
|
}
|
|
|
|
namespace {
|
|
// One chart's copy of a shared mesh edge: the uv-vertex indices of its two endpoints, ordered so that
|
|
// .first is always the lower-numbered base vertex (so two charts' copies line up by base vertex).
|
|
struct ChartEdge { int chart = -1; int uv_lo = -1; int uv_hi = -1; };
|
|
|
|
// base edge (lo,hi mesh vertex) -> every chart that has it on its boundary. A mesh edge shared by two
|
|
// charts is a seam between them and appears once in each chart's boundary (charts have disjoint uv
|
|
// vertices), so its entry lists both charts.
|
|
std::map<std::pair<int, int>, std::vector<ChartEdge>> build_shared_edges(const PatchUnwrap &u)
|
|
{
|
|
std::map<std::pair<int, int>, std::vector<ChartEdge>> edges;
|
|
for (const auto &[ua, ub] : u.boundary_edges) {
|
|
if (ua < 0 || ub < 0 || size_t(ua) >= u.source_vertex.size() || size_t(ub) >= u.source_vertex.size())
|
|
continue;
|
|
const int ba = u.source_vertex[size_t(ua)], bb = u.source_vertex[size_t(ub)];
|
|
if (ba < 0 || bb < 0 || ba == bb)
|
|
continue;
|
|
const int chart = (size_t(ua) < u.vertex_chart.size()) ? u.vertex_chart[size_t(ua)] : -1;
|
|
if (chart < 0)
|
|
continue;
|
|
const std::pair<int, int> key{ std::min(ba, bb), std::max(ba, bb) };
|
|
// Order the uv endpoints to match the base-vertex order in the key.
|
|
ChartEdge e{ chart, ua, ub };
|
|
if (u.source_vertex[size_t(ua)] != key.first)
|
|
std::swap(e.uv_lo, e.uv_hi);
|
|
edges[key].push_back(e);
|
|
}
|
|
return edges;
|
|
}
|
|
|
|
// Solve the placement that maps child's raw uv edge (rc0->rc1) onto the parent's placed edge
|
|
// (pp0->pp1): a rigid rotation about the child chart's centroid plus an offset, scale kept at 1 so the
|
|
// texel density is unchanged. cen is the child chart's centroid.
|
|
TextureIsland solve_edge_alignment(const Vec2f &rc0, const Vec2f &rc1, const Vec2f &pp0, const Vec2f &pp1, const Vec2f &cen)
|
|
{
|
|
const Vec2f dP = pp1 - pp0, dC = rc1 - rc0;
|
|
const float rho = std::atan2(dP.y(), dP.x()) - std::atan2(dC.y(), dC.x());
|
|
const float cs = std::cos(rho), sn = std::sin(rho);
|
|
const auto rot = [&](const Vec2f &v) { return Vec2f(v.x() * cs - v.y() * sn, v.x() * sn + v.y() * cs); };
|
|
|
|
TextureIsland island;
|
|
island.scale = 1.f;
|
|
island.rotation_deg = rho * 180.f / float(M_PI);
|
|
island.offset = pp0 - (rot(rc0 - cen) + cen); // island_transform_matrix places rc0 exactly here
|
|
return island;
|
|
}
|
|
} // namespace
|
|
|
|
bool join_chart_placement(const PatchUnwrap &unwrap, const std::vector<TextureIsland> &islands, int child, int parent,
|
|
TextureIsland &out_child)
|
|
{
|
|
if (child < 0 || parent < 0 || child == parent || size_t(child) >= unwrap.chart_centroid.size())
|
|
return false;
|
|
const auto edges = build_shared_edges(unwrap);
|
|
for (const auto &[base_edge, list] : edges) {
|
|
const ChartEdge *pe = nullptr;
|
|
const ChartEdge *ce = nullptr;
|
|
for (const ChartEdge &e : list) {
|
|
if (e.chart == parent) pe = &e;
|
|
if (e.chart == child) ce = &e;
|
|
}
|
|
if (pe == nullptr || ce == nullptr)
|
|
continue;
|
|
// Parent's *placed* edge; child's *raw* edge, matched endpoint-to-endpoint by base vertex.
|
|
const Vec2f pp0 = apply_island_transform(unwrap.uvs[size_t(pe->uv_lo)], parent, unwrap, islands);
|
|
const Vec2f pp1 = apply_island_transform(unwrap.uvs[size_t(pe->uv_hi)], parent, unwrap, islands);
|
|
out_child = solve_edge_alignment(unwrap.uvs[size_t(ce->uv_lo)], unwrap.uvs[size_t(ce->uv_hi)], pp0, pp1,
|
|
unwrap.chart_centroid[size_t(child)]);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
namespace {
|
|
using Tri2 = std::array<Vec2f, 3>;
|
|
|
|
// Whether two triangles overlap by more than `eps` (separating axis test). Triangles that merely share an edge or
|
|
// a corner, as neighbours in a net do, do not.
|
|
bool triangles_overlap(const Tri2 &a, const Tri2 &b, float eps)
|
|
{
|
|
for (const Tri2 *t : { &a, &b })
|
|
for (int i = 0; i < 3; ++i) {
|
|
const Vec2f edge = (*t)[(i + 1) % 3] - (*t)[i];
|
|
const float len = edge.norm();
|
|
if (len < 1e-12f)
|
|
continue;
|
|
const Vec2f axis(-edge.y() / len, edge.x() / len);
|
|
float a_min = std::numeric_limits<float>::max(), a_max = std::numeric_limits<float>::lowest();
|
|
float b_min = a_min, b_max = a_max;
|
|
for (int k = 0; k < 3; ++k) {
|
|
const float pa = axis.dot(a[k]), pb = axis.dot(b[k]);
|
|
a_min = std::min(a_min, pa);
|
|
a_max = std::max(a_max, pa);
|
|
b_min = std::min(b_min, pb);
|
|
b_max = std::max(b_max, pb);
|
|
}
|
|
if (a_max <= b_min + eps || b_max <= a_min + eps)
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// The triangles already placed in one net, bucketed in a uniform grid so a candidate chart is only tested against
|
|
// its neighbourhood. Triangles spanning many cells go into a list that is tested against everything instead.
|
|
struct NetGrid
|
|
{
|
|
static constexpr int BIG_SPAN = 16;
|
|
float cell;
|
|
float eps;
|
|
std::unordered_map<uint64_t, std::vector<Tri2>> cells;
|
|
std::vector<Tri2> big;
|
|
|
|
static uint64_t key(int x, int y) { return (uint64_t(uint32_t(x)) << 32) | uint32_t(y); }
|
|
bool range(const Tri2 &t, int &x0, int &y0, int &x1, int &y1) const
|
|
{
|
|
const Vec2f lo = t[0].cwiseMin(t[1]).cwiseMin(t[2]), hi = t[0].cwiseMax(t[1]).cwiseMax(t[2]);
|
|
x0 = int(std::floor(lo.x() / cell));
|
|
y0 = int(std::floor(lo.y() / cell));
|
|
x1 = int(std::floor(hi.x() / cell));
|
|
y1 = int(std::floor(hi.y() / cell));
|
|
return x1 - x0 <= BIG_SPAN && y1 - y0 <= BIG_SPAN;
|
|
}
|
|
bool overlaps(const Tri2 &t) const
|
|
{
|
|
for (const Tri2 &b : big)
|
|
if (triangles_overlap(t, b, eps))
|
|
return true;
|
|
int x0, y0, x1, y1;
|
|
if (!range(t, x0, y0, x1, y1)) {
|
|
for (const auto &[k, tris] : cells)
|
|
for (const Tri2 &b : tris)
|
|
if (triangles_overlap(t, b, eps))
|
|
return true;
|
|
return false;
|
|
}
|
|
for (int x = x0; x <= x1; ++x)
|
|
for (int y = y0; y <= y1; ++y)
|
|
if (const auto it = cells.find(key(x, y)); it != cells.end())
|
|
for (const Tri2 &b : it->second)
|
|
if (triangles_overlap(t, b, eps))
|
|
return true;
|
|
return false;
|
|
}
|
|
void insert(const Tri2 &t)
|
|
{
|
|
int x0, y0, x1, y1;
|
|
if (!range(t, x0, y0, x1, y1)) {
|
|
big.push_back(t);
|
|
return;
|
|
}
|
|
for (int x = x0; x <= x1; ++x)
|
|
for (int y = y0; y <= y1; ++y)
|
|
cells[key(x, y)].push_back(t);
|
|
}
|
|
};
|
|
} // namespace
|
|
|
|
std::vector<TextureIsland> compute_connected_net(const PatchUnwrap &unwrap)
|
|
{
|
|
const int n = std::max(unwrap.chart_count, 0);
|
|
std::vector<TextureIsland> islands(static_cast<size_t>(n));
|
|
if (n <= 1)
|
|
return islands;
|
|
|
|
// Chart adjacency, with one representative shared edge per adjacent pair.
|
|
const auto edges = build_shared_edges(unwrap);
|
|
struct PairEdge { ChartEdge a, b; };
|
|
std::map<std::pair<int, int>, PairEdge> pair_edge;
|
|
std::vector<std::vector<int>> adj(static_cast<size_t>(n));
|
|
for (const auto &[base_edge, list] : edges) {
|
|
for (size_t i = 0; i < list.size(); ++i)
|
|
for (size_t j = i + 1; j < list.size(); ++j) {
|
|
const int c1 = list[i].chart, c2 = list[j].chart;
|
|
if (c1 == c2 || c1 < 0 || c2 < 0 || c1 >= n || c2 >= n)
|
|
continue;
|
|
const std::pair<int, int> pk{ std::min(c1, c2), std::max(c1, c2) };
|
|
if (pair_edge.count(pk))
|
|
continue; // keep the first shared edge as the fold line for this pair
|
|
pair_edge[pk] = (c1 < c2) ? PairEdge{ list[i], list[j] } : PairEdge{ list[j], list[i] };
|
|
adj[size_t(pk.first)].push_back(pk.second);
|
|
adj[size_t(pk.second)].push_back(pk.first);
|
|
}
|
|
}
|
|
|
|
// Per chart: its vertices, its triangles and its flattened area.
|
|
std::vector<std::vector<int>> chart_verts(static_cast<size_t>(n)), chart_tris(static_cast<size_t>(n));
|
|
std::vector<float> chart_area(static_cast<size_t>(n), 0.f);
|
|
for (size_t i = 0; i < unwrap.uvs.size(); ++i)
|
|
if (const int c = unwrap.vertex_chart[i]; c >= 0 && c < n)
|
|
chart_verts[size_t(c)].push_back(int(i));
|
|
float extent_sum = 0.f, extent = 0.f;
|
|
for (size_t t = 0; t < unwrap.indices.size(); ++t) {
|
|
const stl_triangle_vertex_indices &tri = unwrap.indices[t];
|
|
const int c = unwrap.vertex_chart[size_t(tri[0])];
|
|
if (c < 0 || c >= n)
|
|
continue;
|
|
chart_tris[size_t(c)].push_back(int(t));
|
|
const Vec2f &p0 = unwrap.uvs[size_t(tri[0])], &p1 = unwrap.uvs[size_t(tri[1])], &p2 = unwrap.uvs[size_t(tri[2])];
|
|
const Vec2f e0 = p1 - p0, e1 = p2 - p0;
|
|
chart_area[size_t(c)] += 0.5f * std::abs(e0.x() * e1.y() - e0.y() * e1.x());
|
|
const Vec2f size = p0.cwiseMax(p1).cwiseMax(p2) - p0.cwiseMin(p1).cwiseMin(p2);
|
|
extent_sum += std::max(size.x(), size.y());
|
|
extent = std::max({ extent, p0.cwiseAbs().maxCoeff(), p1.cwiseAbs().maxCoeff(), p2.cwiseAbs().maxCoeff() });
|
|
}
|
|
const float cell = std::max(extent_sum / float(std::max<size_t>(unwrap.indices.size(), 1)), 1e-6f);
|
|
// Touching neighbours may overlap by rounding: a fraction of a typical triangle, but at least what float
|
|
// rounding of a placement at this distance from the origin can produce.
|
|
const float eps = std::max(0.02f * cell, 4e-6f * extent);
|
|
|
|
const auto placed = [&](const Eigen::Matrix<float, 2, 3> &m, int t) {
|
|
const stl_triangle_vertex_indices &tri = unwrap.indices[size_t(t)];
|
|
Tri2 out;
|
|
for (int k = 0; k < 3; ++k)
|
|
out[size_t(k)] = m.block<2, 2>(0, 0) * unwrap.uvs[size_t(tri[k])] + m.col(2);
|
|
return out;
|
|
};
|
|
|
|
// Grow a net from the largest chart not yet in one, unfolding each neighbour onto the chart it was reached from
|
|
// (bigger neighbours first, so slivers don't claim the good spots) unless its triangles would overlap the net.
|
|
// A chart that doesn't fit stays out and roots a net of its own later, so nothing is left in a random spot.
|
|
std::vector<int> by_area(static_cast<size_t>(n));
|
|
std::iota(by_area.begin(), by_area.end(), 0);
|
|
std::stable_sort(by_area.begin(), by_area.end(), [&chart_area](int a, int b) { return chart_area[size_t(a)] > chart_area[size_t(b)]; });
|
|
|
|
std::vector<int> net_of(static_cast<size_t>(n), -1);
|
|
int net_count = 0;
|
|
for (const int root : by_area) {
|
|
if (net_of[size_t(root)] >= 0 || chart_tris[size_t(root)].empty())
|
|
continue;
|
|
const int net = net_count++;
|
|
NetGrid grid{ cell, eps, {}, {} };
|
|
net_of[size_t(root)] = net;
|
|
{
|
|
const Eigen::Matrix<float, 2, 3> m = island_transform_matrix(root, unwrap, islands);
|
|
for (const int t : chart_tris[size_t(root)])
|
|
grid.insert(placed(m, t));
|
|
}
|
|
std::queue<int> q;
|
|
q.push(root);
|
|
while (!q.empty()) {
|
|
const int p = q.front();
|
|
q.pop();
|
|
std::vector<int> neighbours = adj[size_t(p)];
|
|
std::stable_sort(neighbours.begin(), neighbours.end(),
|
|
[&chart_area](int a, int b) { return chart_area[size_t(a)] > chart_area[size_t(b)]; });
|
|
for (const int c : neighbours) {
|
|
if (net_of[size_t(c)] >= 0 || chart_tris[size_t(c)].empty())
|
|
continue;
|
|
const auto it = pair_edge.find({ std::min(p, c), std::max(p, c) });
|
|
if (it == pair_edge.end())
|
|
continue;
|
|
const ChartEdge &pe = (p < c) ? it->second.a : it->second.b;
|
|
const ChartEdge &ce = (p < c) ? it->second.b : it->second.a;
|
|
const Vec2f pp0 = apply_island_transform(unwrap.uvs[size_t(pe.uv_lo)], p, unwrap, islands);
|
|
const Vec2f pp1 = apply_island_transform(unwrap.uvs[size_t(pe.uv_hi)], p, unwrap, islands);
|
|
islands[size_t(c)] = solve_edge_alignment(unwrap.uvs[size_t(ce.uv_lo)], unwrap.uvs[size_t(ce.uv_hi)], pp0,
|
|
pp1, unwrap.chart_centroid[size_t(c)]);
|
|
|
|
const Eigen::Matrix<float, 2, 3> m = island_transform_matrix(c, unwrap, islands);
|
|
std::vector<Tri2> tris;
|
|
tris.reserve(chart_tris[size_t(c)].size());
|
|
bool fits = true;
|
|
for (const int t : chart_tris[size_t(c)]) {
|
|
tris.push_back(placed(m, t));
|
|
if (grid.overlaps(tris.back())) {
|
|
fits = false;
|
|
break;
|
|
}
|
|
}
|
|
if (!fits) {
|
|
islands[size_t(c)] = TextureIsland{};
|
|
continue;
|
|
}
|
|
for (const Tri2 &t : tris)
|
|
grid.insert(t);
|
|
net_of[size_t(c)] = net;
|
|
q.push(c);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Shelf-pack the nets side by side, tallest first, the way compute_patch_unwrap() packs charts.
|
|
std::vector<Vec2f> lo(static_cast<size_t>(net_count), Vec2f::Constant(std::numeric_limits<float>::max()));
|
|
std::vector<Vec2f> hi(static_cast<size_t>(net_count), Vec2f::Constant(std::numeric_limits<float>::lowest()));
|
|
for (int c = 0; c < n; ++c) {
|
|
const int net = net_of[size_t(c)];
|
|
if (net < 0)
|
|
continue;
|
|
const Eigen::Matrix<float, 2, 3> m = island_transform_matrix(c, unwrap, islands);
|
|
for (const int v : chart_verts[size_t(c)]) {
|
|
const Vec2f p = m.block<2, 2>(0, 0) * unwrap.uvs[size_t(v)] + m.col(2);
|
|
lo[size_t(net)] = lo[size_t(net)].cwiseMin(p);
|
|
hi[size_t(net)] = hi[size_t(net)].cwiseMax(p);
|
|
}
|
|
}
|
|
float total_area = 0.f, widest = 0.f;
|
|
for (int k = 0; k < net_count; ++k) {
|
|
const Vec2f size = hi[size_t(k)] - lo[size_t(k)];
|
|
total_area += size.x() * size.y();
|
|
widest = std::max(widest, size.x());
|
|
}
|
|
const float shelf_width = std::max(widest, std::sqrt(std::max(total_area, 0.f)) * 1.4f);
|
|
const float margin = std::max(shelf_width * 0.02f, 1e-4f);
|
|
std::vector<int> order(static_cast<size_t>(net_count));
|
|
std::iota(order.begin(), order.end(), 0);
|
|
std::stable_sort(order.begin(), order.end(), [&](int a, int b) {
|
|
return hi[size_t(a)].y() - lo[size_t(a)].y() > hi[size_t(b)].y() - lo[size_t(b)].y();
|
|
});
|
|
std::vector<Vec2f> shift(static_cast<size_t>(net_count), Vec2f::Zero());
|
|
float cursor_x = 0.f, cursor_y = 0.f, row_height = 0.f;
|
|
for (const int k : order) {
|
|
const Vec2f size = hi[size_t(k)] - lo[size_t(k)];
|
|
if (cursor_x > 0.f && cursor_x + size.x() > shelf_width) {
|
|
cursor_x = 0.f;
|
|
cursor_y += row_height + margin;
|
|
row_height = 0.f;
|
|
}
|
|
shift[size_t(k)] = Vec2f(cursor_x, cursor_y) - lo[size_t(k)];
|
|
cursor_x += size.x() + margin;
|
|
row_height = std::max(row_height, size.y());
|
|
}
|
|
for (int c = 0; c < n; ++c)
|
|
if (net_of[size_t(c)] >= 0)
|
|
islands[size_t(c)].offset += shift[size_t(net_of[size_t(c)])];
|
|
return islands;
|
|
}
|
|
|
|
void average_island_scales(std::vector<TextureIsland> &islands)
|
|
{
|
|
if (islands.empty())
|
|
return;
|
|
float sum = 0.f;
|
|
for (const TextureIsland &island : islands)
|
|
sum += island.scale;
|
|
const float mean = sum / float(islands.size());
|
|
for (TextureIsland &island : islands)
|
|
island.scale = mean;
|
|
}
|
|
|
|
std::vector<bool> apply_lscm_uv_overrides(PatchUnwrap &unwrap, const std::vector<std::pair<int, Vec2f>> &overrides)
|
|
{
|
|
std::vector<bool> edited(unwrap.uvs.size(), false);
|
|
if (overrides.empty())
|
|
return edited;
|
|
// Mesh-vertex keys first, so an edit on one copy of the same vertex takes precedence over them.
|
|
std::map<int, Vec2f> by_mesh_vertex;
|
|
for (const auto &[key, uv] : overrides)
|
|
if (key >= 0)
|
|
by_mesh_vertex[key] = uv;
|
|
if (!by_mesh_vertex.empty())
|
|
for (size_t i = 0; i < unwrap.uvs.size(); ++i)
|
|
if (const auto it = by_mesh_vertex.find(unwrap.source_vertex[i]); it != by_mesh_vertex.end()) {
|
|
unwrap.uvs[i] = it->second;
|
|
edited[i] = true;
|
|
}
|
|
for (const auto &[key, uv] : overrides)
|
|
if (const int i = -key - 1; key < 0 && size_t(i) < unwrap.uvs.size()) {
|
|
unwrap.uvs[size_t(i)] = uv;
|
|
edited[size_t(i)] = true;
|
|
}
|
|
return edited;
|
|
}
|
|
|
|
std::vector<Vec2f> compute_lscm_uvs(const indexed_triangle_set &patch, const TextureDisplacementLayer &layer)
|
|
{
|
|
// Padding disabled (0), matching the UV editor: the packed islands the editor shows and the ones the
|
|
// bake/preview samples must be laid out identically, or a hand placement made in the editor would
|
|
// land somewhere else in the baked result.
|
|
const PatchUnwrap unwrap = compute_patch_unwrap(patch, layer.lscm_seam_angle_deg, 0.f, layer.lscm_seam_edges);
|
|
if (unwrap.empty())
|
|
return {};
|
|
|
|
// Manual UV edits (UV editor Vertex/Edge modes) replace the automatic raw unwrap coordinate, before the island
|
|
// transform - so the edit rides along with any island move/rotate exactly like the rest of the island.
|
|
PatchUnwrap edited_unwrap = unwrap;
|
|
const std::vector<bool> edited = apply_lscm_uv_overrides(edited_unwrap, layer.lscm_uv_overrides);
|
|
|
|
// One UV per patch vertex: a seam vertex has several (one per chart it touches) and has to
|
|
// settle on one, since it can only be displaced to a single position. See compute_lscm_uvs()'s
|
|
// header comment - the surface stays watertight regardless. A copy that was edited by hand wins,
|
|
// so the edit is what bakes; otherwise the first copy does.
|
|
std::vector<Vec2f> per_vertex(patch.vertices.size(), Vec2f::Zero());
|
|
std::vector<bool> assigned(patch.vertices.size(), false);
|
|
for (const bool edited_pass : { true, false })
|
|
for (size_t i = 0; i < edited_unwrap.uvs.size(); ++i) {
|
|
const int pv = edited_unwrap.source_vertex[i];
|
|
if (pv < 0 || assigned[size_t(pv)] || edited[i] != edited_pass)
|
|
continue;
|
|
per_vertex[size_t(pv)] = apply_island_transform(edited_unwrap.uvs[i], edited_unwrap.vertex_chart[i], unwrap, layer.islands);
|
|
assigned[size_t(pv)] = true;
|
|
}
|
|
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
|
|
// of them was most of the per-sample arithmetic.
|
|
struct UVTransform
|
|
{
|
|
float scale, cs, sn, aspect;
|
|
Vec2f offset;
|
|
|
|
UVTransform(const TextureDisplacementLayer &layer, float aspect_)
|
|
{
|
|
scale = (layer.tiling_scale > 1e-6f) ? (1.f / layer.tiling_scale) : 1.f;
|
|
const float rad = layer.rotation_deg * float(M_PI) / 180.f;
|
|
cs = std::cos(rad);
|
|
sn = std::sin(rad);
|
|
aspect = aspect_;
|
|
offset = layer.offset;
|
|
}
|
|
Vec2f operator()(const Vec2f &planar) const
|
|
{
|
|
const Vec2f scaled = planar * scale;
|
|
Vec2f rotated(scaled.x() * cs - scaled.y() * sn, scaled.x() * sn + scaled.y() * cs);
|
|
// See apply_uv_transform() for why the aspect correction follows the rotation.
|
|
if (aspect > 0.f && aspect != 1.f)
|
|
rotated.y() *= aspect;
|
|
return rotated + offset;
|
|
}
|
|
};
|
|
|
|
// |n|^TRIPLANAR_BLEND_SHARPNESS per component. The exponent is a compile-time 4, so two squarings do
|
|
// what three std::pow calls per sample did.
|
|
static_assert(TRIPLANAR_BLEND_SHARPNESS == 4.f, "triplanar_weights() hard-codes the fourth power");
|
|
inline Vec3f triplanar_weights(const Vec3f &normal)
|
|
{
|
|
Vec3f w = normal.cwiseAbs();
|
|
w = w.cwiseProduct(w);
|
|
return w.cwiseProduct(w);
|
|
}
|
|
} // namespace
|
|
|
|
Vec2f apply_uv_transform(const Vec2f &planar, const TextureDisplacementLayer &layer, float aspect)
|
|
{
|
|
const float scale = (layer.tiling_scale > 1e-6f) ? (1.f / layer.tiling_scale) : 1.f;
|
|
const Vec2f scaled = planar * scale;
|
|
|
|
const float rad = layer.rotation_deg * float(M_PI) / 180.f;
|
|
const float cs = std::cos(rad);
|
|
const float sn = std::sin(rad);
|
|
Vec2f rotated(scaled.x() * cs - scaled.y() * sn, scaled.x() * sn + scaled.y() * cs);
|
|
|
|
// Non-square textures. Without this the [0,1] square of uv covers the whole image whatever its
|
|
// proportions, so a 2:1 image is squeezed into a square tile and every feature in it comes out
|
|
// half as wide as it should be. `tiling_scale` is the tile's size along u; the tile is
|
|
// `tiling_scale * height / width` mm along v, which is exactly what keeps texels square - so
|
|
// dividing v by that extent is the same as multiplying it by width / height. A square texture has
|
|
// aspect 1 and is untouched, which is why this changes nothing for the shipped library.
|
|
//
|
|
// Applied after the rotation, not before: scaling one axis of an already-rotated coordinate is a
|
|
// shear, and doing it the other way round would make "Rotation" skew the pattern instead of
|
|
// turning it.
|
|
if (aspect > 0.f && aspect != 1.f)
|
|
rotated.y() *= aspect;
|
|
|
|
return rotated + layer.offset;
|
|
}
|
|
|
|
float blend_displacement(float accumulated, float value, TextureBlendMode mode)
|
|
{
|
|
switch (mode) {
|
|
case TextureBlendMode::Subtract: return accumulated - value;
|
|
// Multiply/Divide scale rather than offset, so they take `value` as a factor relative to 1 mm
|
|
// (see TextureBlendMode): a 1 mm-deep layer sampling a white texel is then exactly neutral.
|
|
case TextureBlendMode::Multiply: return accumulated * value;
|
|
case TextureBlendMode::Divide: {
|
|
// Every height map has black regions, and a black texel samples to *exactly* zero - so this
|
|
// divisor really does hit zero in ordinary use, not just in some contrived edge case. Floor
|
|
// its magnitude: an unbounded 1/0 would not merely look wrong, it would fling vertices
|
|
// thousands of mm away and poison the mesh's bounding box (and with it every plate/print
|
|
// volume check downstream). The floor doubles as a cap on how far Divide can ever amplify
|
|
// the relief beneath it - at most 1/0.05 = 20x.
|
|
constexpr float min_divisor = 0.05f;
|
|
const float divisor = (std::abs(value) < min_divisor) ? std::copysign(min_divisor, value < 0.f ? -1.f : 1.f) :
|
|
value;
|
|
return accumulated / divisor;
|
|
}
|
|
case TextureBlendMode::Add:
|
|
default: return accumulated + value;
|
|
}
|
|
}
|
|
|
|
bool project_uv_projective(const std::array<float, 12> &m, const Vec3f &position, Vec2f &uv)
|
|
{
|
|
const float x = position.x(), y = position.y(), z = position.z();
|
|
const float w = m[8] * x + m[9] * y + m[10] * z + m[11];
|
|
// Strictly greater than zero: at w == 0 the point sits on the projector's plane and maps to
|
|
// infinity, and at w < 0 it is behind the projector, where dividing yields a plausible-looking
|
|
// but mirrored uv - the classic way a projected decal reappears on the back of a model.
|
|
if (!(w > 1e-6f))
|
|
return false;
|
|
uv = Vec2f((m[0] * x + m[1] * y + m[2] * z + m[3]) / w, (m[4] * x + m[5] * y + m[6] * z + m[7]) / w);
|
|
return true;
|
|
}
|
|
|
|
float sample_layer_height(const DecodedHeightTexture &texture, const TextureDisplacementLayer &layer,
|
|
const Vec3f &position, const Vec3f &normal,
|
|
const Vec3f &patch_center, const Vec3f &patch_axis, const Vec2f *lscm_uv)
|
|
{
|
|
if (texture.empty())
|
|
return 0.f;
|
|
|
|
// width / height of the height map, so a non-square image keeps its proportions (see
|
|
// apply_uv_transform()). Every projection except the projective "from view" one funnels through
|
|
// here, so this one line is what makes them all aspect-correct.
|
|
const float aspect = (texture.height > 0) ? float(texture.width) / float(texture.height) : 1.f;
|
|
const UVTransform xf(layer, aspect);
|
|
auto sample_at = [&](const Vec2f &planar) {
|
|
return texture.sample(xf(planar), layer.tile_enabled, layer.tile_method);
|
|
};
|
|
|
|
// Precomputed per-patch LSCM solve wins over the layer's own method (see the header): the
|
|
// caller only passes it when the patch actually parameterized successfully, so a patch that
|
|
// failed to unwrap falls through to the analytic methods below as its documented fallback.
|
|
if (lscm_uv != nullptr)
|
|
return sample_at(*lscm_uv);
|
|
|
|
switch (layer.projection_method) {
|
|
case TextureProjectionMethod::Cylindrical:
|
|
return sample_at(project_cylindrical(position, patch_center, patch_axis));
|
|
case TextureProjectionMethod::Spherical:
|
|
return sample_at(project_spherical(position, patch_center));
|
|
case TextureProjectionMethod::ViewProjected:
|
|
if (layer.view_project_projective) {
|
|
// Exact projective placement written by the projection-frame overlay. Sampled directly,
|
|
// *without* apply_uv_transform(): the matrix already maps the window's border to the uv
|
|
// unit square, so the tiling/rotation/offset controls would displace it off the frame
|
|
// the user just aligned. A point behind the projector has no uv at all -> no height.
|
|
Vec2f uv;
|
|
if (!project_uv_projective(layer.view_project_matrix, position, uv))
|
|
return 0.f;
|
|
return texture.sample(uv, layer.tile_enabled, layer.tile_method);
|
|
}
|
|
// Flat projection onto the captured projector plane. Single-valued per point, so unlike
|
|
// blended triplanar it is one sample, and it is what "project from view" places.
|
|
return sample_at(Vec2f(position.dot(layer.view_project_right), position.dot(layer.view_project_up)));
|
|
case TextureProjectionMethod::LSCM: // no usable unwrap for this patch - fall back to Triplanar
|
|
case TextureProjectionMethod::Triplanar:
|
|
default: break;
|
|
}
|
|
|
|
// Blended tri-planar: sample all three axis-aligned planes and cross-fade between them by the
|
|
// normal's own components, instead of hard-switching to whichever single axis dominates. The
|
|
// hard switch is what produced a visible seam wherever the dominant axis flips (see
|
|
// TextureProjectionMethod::Triplanar); a weighted blend is continuous across that transition
|
|
// by construction, since the weight of the axis being left behind falls smoothly to zero.
|
|
Vec3f w = triplanar_weights(normal);
|
|
const float w_sum = w.x() + w.y() + w.z();
|
|
if (w_sum < 1e-8f)
|
|
// Degenerate normal: no axis is meaningfully dominant, so no blend is meaningful either.
|
|
return sample_at(Vec2f(position.x(), position.y()));
|
|
w /= w_sum;
|
|
|
|
// Each plane drops the axis it is named for, matching project_planar()'s own convention (which
|
|
// the GUI's on-canvas placement gizmo also relies on).
|
|
return w.x() * sample_at(Vec2f(position.y(), position.z())) +
|
|
w.y() * sample_at(Vec2f(position.x(), position.z())) +
|
|
w.z() * sample_at(Vec2f(position.x(), position.y()));
|
|
}
|
|
|
|
bool sample_layer_color(const DecodedHeightTexture &texture, const TextureDisplacementLayer &layer,
|
|
const Vec3f &position, const Vec3f &normal, Vec3f &out, const Vec3f &patch_center,
|
|
const Vec3f &patch_axis, const Vec2f *lscm_uv)
|
|
{
|
|
if (!texture.has_color())
|
|
return false;
|
|
|
|
// Deliberately a transcription of sample_layer_height()'s dispatch rather than a shared template:
|
|
// the two differ in what "nothing here" means. Height returns 0, which is a perfectly good height
|
|
// (no displacement); colour has no such neutral value - black is a colour - so every path that
|
|
// returns 0 there has to report false here instead, and the caller leaves the triangle uncoloured.
|
|
const float aspect = (texture.height > 0) ? float(texture.width) / float(texture.height) : 1.f;
|
|
const UVTransform xf(layer, aspect);
|
|
auto sample_at = [&](const Vec2f &planar) {
|
|
return texture.sample_color(xf(planar), layer.tile_enabled, layer.tile_method);
|
|
};
|
|
// Outside a non-tiled placement there is no texture at all - the same hard edge sample() gives the
|
|
// height. Checked explicitly because sample_color() reports it as black, which is a real colour.
|
|
auto covered = [&](const Vec2f &planar) {
|
|
if (layer.tile_enabled)
|
|
return true;
|
|
const Vec2f uv = xf(planar);
|
|
return uv.x() >= 0.f && uv.x() < 1.f && uv.y() >= 0.f && uv.y() < 1.f;
|
|
};
|
|
|
|
if (lscm_uv != nullptr) {
|
|
if (!covered(*lscm_uv))
|
|
return false;
|
|
out = sample_at(*lscm_uv);
|
|
return true;
|
|
}
|
|
|
|
switch (layer.projection_method) {
|
|
case TextureProjectionMethod::Cylindrical: {
|
|
const Vec2f p = project_cylindrical(position, patch_center, patch_axis);
|
|
if (!covered(p))
|
|
return false;
|
|
out = sample_at(p);
|
|
return true;
|
|
}
|
|
case TextureProjectionMethod::Spherical: {
|
|
const Vec2f p = project_spherical(position, patch_center);
|
|
if (!covered(p))
|
|
return false;
|
|
out = sample_at(p);
|
|
return true;
|
|
}
|
|
case TextureProjectionMethod::ViewProjected:
|
|
if (layer.view_project_projective) {
|
|
// The frame's own rectangle is the placement, so no apply_uv_transform() - see
|
|
// sample_layer_height(). A point behind the projector has no uv, hence no colour.
|
|
Vec2f uv;
|
|
if (!project_uv_projective(layer.view_project_matrix, position, uv))
|
|
return false;
|
|
if (!layer.tile_enabled && (uv.x() < 0.f || uv.x() >= 1.f || uv.y() < 0.f || uv.y() >= 1.f))
|
|
return false;
|
|
out = texture.sample_color(uv, layer.tile_enabled, layer.tile_method);
|
|
return true;
|
|
} else {
|
|
const Vec2f p(position.dot(layer.view_project_right), position.dot(layer.view_project_up));
|
|
if (!covered(p))
|
|
return false;
|
|
out = sample_at(p);
|
|
return true;
|
|
}
|
|
case TextureProjectionMethod::LSCM: // no usable unwrap for this patch - fall back to Triplanar
|
|
case TextureProjectionMethod::Triplanar:
|
|
default: break;
|
|
}
|
|
|
|
// Blended tri-planar, weighted exactly as the height is, so colour and relief stay registered
|
|
// across the cross-fade band at a 90-degree edge.
|
|
Vec3f w = triplanar_weights(normal);
|
|
const float w_sum = w.x() + w.y() + w.z();
|
|
if (w_sum < 1e-8f) {
|
|
const Vec2f p(position.x(), position.y());
|
|
if (!covered(p))
|
|
return false;
|
|
out = sample_at(p);
|
|
return true;
|
|
}
|
|
w /= w_sum;
|
|
|
|
// A blend of three planes is only "not covered" where *every* contributing plane is outside the
|
|
// placement; where some are, the covered ones are renormalised so the colour does not fade toward
|
|
// black at the edge of an untiled tri-planar layer.
|
|
const std::array<Vec2f, 3> planes = { Vec2f(position.y(), position.z()), Vec2f(position.x(), position.z()),
|
|
Vec2f(position.x(), position.y()) };
|
|
Vec3f acc = Vec3f::Zero();
|
|
float acc_w = 0.f;
|
|
for (int i = 0; i < 3; ++i)
|
|
if (w[i] > 0.f && covered(planes[size_t(i)])) {
|
|
acc += w[i] * sample_at(planes[size_t(i)]);
|
|
acc_w += w[i];
|
|
}
|
|
if (acc_w <= 0.f)
|
|
return false;
|
|
out = acc / acc_w;
|
|
return true;
|
|
}
|
|
|
|
indexed_triangle_set extract_painted_patch(const indexed_triangle_set &base_mesh,
|
|
const TriangleSelector::TriangleSplittingData &facet_data)
|
|
{
|
|
if (facet_data.triangles_to_split.empty())
|
|
return {};
|
|
|
|
const TriangleMesh selector_mesh(base_mesh);
|
|
TriangleSelector selector(selector_mesh);
|
|
selector.deserialize(facet_data, false);
|
|
return selector.get_facets_strict(EnforcerBlockerType::ENFORCER);
|
|
}
|
|
|
|
bool compute_layer_paint_anchor(const indexed_triangle_set &base_mesh,
|
|
const TriangleSelector::TriangleSplittingData &facet_data,
|
|
Vec3f &anchor_pos,
|
|
Vec3f &anchor_normal)
|
|
{
|
|
const indexed_triangle_set patch = extract_painted_patch(base_mesh, facet_data);
|
|
if (patch.indices.empty())
|
|
return false;
|
|
|
|
Vec3f centroid_sum = Vec3f::Zero();
|
|
Vec3f normal_sum = Vec3f::Zero();
|
|
for (const stl_triangle_vertex_indices &tri : patch.indices) {
|
|
const Vec3f &a = patch.vertices[tri[0]];
|
|
const Vec3f &b = patch.vertices[tri[1]];
|
|
const Vec3f &c = patch.vertices[tri[2]];
|
|
// Cross product magnitude is twice the face area, so this area-weights both sums the same
|
|
// way texture_displacement_vertex_normals() does above.
|
|
normal_sum += (b - a).cross(c - a);
|
|
centroid_sum += (a + b + c) / 3.f;
|
|
}
|
|
|
|
anchor_pos = centroid_sum / float(patch.indices.size());
|
|
anchor_normal = (normal_sum.norm() > 1e-8f) ? Vec3f(normal_sum.normalized()) : Vec3f::UnitZ();
|
|
return true;
|
|
}
|
|
|
|
// Area-weighted vertex normals of the undisplaced mesh. build_texture_displacement() computes
|
|
// 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.
|
|
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) {
|
|
const Vec3f &a = its.vertices[tri[0]];
|
|
const Vec3f &b = its.vertices[tri[1]];
|
|
const Vec3f &c = its.vertices[tri[2]];
|
|
// Cross product magnitude is twice the face area, so this naturally area-weights the
|
|
// contribution of each incident face to its vertices.
|
|
const Vec3f area_weighted_normal = (b - a).cross(c - a);
|
|
normals[tri[0]] += area_weighted_normal;
|
|
normals[tri[1]] += area_weighted_normal;
|
|
normals[tri[2]] += area_weighted_normal;
|
|
}
|
|
for (Vec3f &n : normals) {
|
|
const float len = n.norm();
|
|
n = (len > 1e-8f) ? Vec3f(n / len) : Vec3f::UnitZ();
|
|
}
|
|
return normals;
|
|
}
|
|
|
|
namespace {
|
|
// Shortest along-surface distance from every patch vertex to the patch boundary (the pinned vertices
|
|
// shared with the untouched surface), by a multi-source Dijkstra over the patch edges. Used for edge
|
|
// smoothing: the displacement is faded out as this distance goes to zero.
|
|
std::vector<float> patch_boundary_distance(const indexed_triangle_set &patch, const std::vector<bool> &is_boundary)
|
|
{
|
|
const size_t n = patch.vertices.size();
|
|
std::vector<std::vector<std::pair<int, float>>> adj(n);
|
|
for (const stl_triangle_vertex_indices &tri : patch.indices)
|
|
for (int i = 0; i < 3; ++i) {
|
|
const int a = tri[i], b = tri[(i + 1) % 3];
|
|
if (a < 0 || b < 0 || size_t(a) >= n || size_t(b) >= n)
|
|
continue;
|
|
const float w = (patch.vertices[size_t(a)] - patch.vertices[size_t(b)]).norm();
|
|
adj[size_t(a)].push_back({ b, w });
|
|
adj[size_t(b)].push_back({ a, w });
|
|
}
|
|
|
|
std::vector<float> dist(n, std::numeric_limits<float>::infinity());
|
|
using QN = std::pair<float, int>;
|
|
std::priority_queue<QN, std::vector<QN>, std::greater<QN>> pq;
|
|
for (size_t v = 0; v < n; ++v)
|
|
if (v < is_boundary.size() && is_boundary[v]) {
|
|
dist[v] = 0.f;
|
|
pq.push({ 0.f, int(v) });
|
|
}
|
|
while (!pq.empty()) {
|
|
const auto [d, u] = pq.top();
|
|
pq.pop();
|
|
if (d > dist[size_t(u)])
|
|
continue;
|
|
for (const auto &[w, ew] : adj[size_t(u)]) {
|
|
const float nd = d + ew;
|
|
if (nd < dist[size_t(w)]) {
|
|
dist[size_t(w)] = nd;
|
|
pq.push({ nd, w });
|
|
}
|
|
}
|
|
}
|
|
return dist;
|
|
}
|
|
} // namespace
|
|
|
|
namespace {
|
|
// Majority filter over face adjacency: each triangle takes the most common colour among itself and
|
|
// the (up to three) triangles across its edges. Ties, and a triangle whose own colour is already the
|
|
// most common, keep what they had - so the filter only ever removes a facet that disagrees with its
|
|
// whole neighbourhood, and cannot drift a large region.
|
|
//
|
|
// Read from a snapshot of the previous pass, so the result does not depend on triangle order.
|
|
// Uncoloured triangles (-1) neither vote nor get voted on: the paint boundary is not noise.
|
|
void despeckle_triangle_colors(const indexed_triangle_set &mesh, std::vector<int> &color, int passes)
|
|
{
|
|
if (passes <= 0 || color.size() != mesh.indices.size())
|
|
return;
|
|
const std::vector<Vec3i32> neighbors = its_face_neighbors(mesh);
|
|
if (neighbors.size() != mesh.indices.size())
|
|
return;
|
|
|
|
std::vector<int> prev;
|
|
for (int pass = 0; pass < passes; ++pass) {
|
|
prev = color;
|
|
tbb::parallel_for(tbb::blocked_range<size_t>(0, color.size()),
|
|
[&](const tbb::blocked_range<size_t> &range) {
|
|
for (size_t i = range.begin(); i < range.end(); ++i) {
|
|
if (prev[i] < 0)
|
|
continue;
|
|
// At most four candidates (self plus three neighbours), so counting by a linear scan
|
|
// is cheaper than any map.
|
|
int cand[4] = { prev[i], -1, -1, -1 };
|
|
int count[4] = { 1, 0, 0, 0 };
|
|
int n = 1;
|
|
for (int e = 0; e < 3; ++e) {
|
|
const int nb = neighbors[i][e];
|
|
if (nb < 0 || size_t(nb) >= prev.size() || prev[size_t(nb)] < 0)
|
|
continue;
|
|
const int c = prev[size_t(nb)];
|
|
int k = 0;
|
|
for (; k < n; ++k)
|
|
if (cand[k] == c) {
|
|
++count[k];
|
|
break;
|
|
}
|
|
if (k == n && n < 4) {
|
|
cand[n] = c;
|
|
count[n] = 1;
|
|
++n;
|
|
}
|
|
}
|
|
// Strictly greater, so a tie leaves the triangle alone.
|
|
int best = 0;
|
|
for (int k = 1; k < n; ++k)
|
|
if (count[k] > count[best])
|
|
best = k;
|
|
if (count[best] > count[0])
|
|
color[i] = cand[best];
|
|
}
|
|
});
|
|
}
|
|
}
|
|
} // 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
|
|
// projections behave identically in both paths and the comparison is between the meshing strategies.
|
|
indexed_triangle_set build_texture_displacement_v2(const indexed_triangle_set &mesh,
|
|
const std::vector<TextureDisplacementLayer> &layers,
|
|
const TextureDisplacementFacetsData &facets_data,
|
|
const TextureDisplacementOptions &options,
|
|
const DisplacementProgressFn &progress,
|
|
const TextureColorRequest *color,
|
|
bool flip_normals,
|
|
BakeStageRecorder *debug,
|
|
TextureBakeStats *stats)
|
|
{
|
|
HeightFieldSampler combined = make_combined_displacement_sampler(mesh, layers, facets_data);
|
|
if (!combined)
|
|
return mesh; // nothing decodable to displace with
|
|
|
|
// 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);
|
|
bool dirty = false;
|
|
for (const TriangleSelector::TriangleSplittingData &data : facets_data) {
|
|
if (data.triangles_to_split.empty())
|
|
continue;
|
|
selector.deserialize(data, dirty);
|
|
dirty = true;
|
|
std::vector<int> piece_src;
|
|
const indexed_triangle_set patch =
|
|
selector.get_facets_strict(EnforcerBlockerType::ENFORCER, &piece_src);
|
|
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; }) || 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;
|
|
const V2Resolution rec = (auto_edge || auto_budget) ? recommend_v2_resolution(mesh, layers) : V2Resolution{};
|
|
TextureBake::PipelineSettings settings;
|
|
settings.refine_length = auto_edge ? std::max(0.05, double(rec.edge_mm)) : std::max(0.01f, options.v2_refine_mm);
|
|
settings.regularize = options.v2_regularize;
|
|
settings.max_triangles = size_t(auto_budget ? std::max(0, rec.budget_k) : options.v2_max_triangles_k) * 1000;
|
|
BOOST_LOG_TRIVIAL(info) << "TextureBake resolution: " << settings.refine_length << " mm"
|
|
<< (auto_edge ? " (auto)" : "") << ", budget " << settings.max_triangles / 1000 << " k"
|
|
<< (auto_budget ? " (auto)" : "");
|
|
settings.preserve_untextured = true;
|
|
// Always on: relief driven under the plate is unprintable whichever pipeline produced it, so this
|
|
// is no longer a choice the user has to make. Only geometry that ends up below the model's own
|
|
// bottom is moved - downward relief that stays clear of the plate is untouched.
|
|
settings.clamp_below_plate = true;
|
|
settings.relocate = options.v2_relocate;
|
|
settings.flip_edges = options.v2_flip_edges;
|
|
// The sampler already returns millimetres, so the displacement stage must not scale it again.
|
|
settings.displace.amplitude = 1.f;
|
|
settings.displace.symmetric = false;
|
|
// The paint decides what moves here, so the angle limits stay off.
|
|
settings.displace.bottom_angle_limit = 0.f;
|
|
settings.displace.top_angle_limit = 0.f;
|
|
// The sampler's normal only picks the projection blend (triplanar weights); the displacement
|
|
// direction is the pipeline's own smooth normal. Handing it the Laplacian-smoothed blend normal
|
|
// spreads a crease's 50/50 blend over a band of rows instead of one: on a cube edge the single row
|
|
// 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();
|
|
for (const Vec3f &v : mesh.vertices) {
|
|
bounds.min = bounds.min.cwiseMin(v);
|
|
bounds.max = bounds.max.cwiseMax(v);
|
|
}
|
|
|
|
const auto sample = [&combined](const Vec3f &pos, const Vec3f &, const Vec3f &blend_normal) {
|
|
// The blend normal chooses the projection; the move itself is along the smooth normal, which
|
|
// the pipeline applies on its own.
|
|
return combined(pos, blend_normal);
|
|
};
|
|
|
|
// The pipeline takes its displacement direction from the soup's winding, so a mirrored placement
|
|
// would drive the whole relief inwards. The paint masks were read off `mesh` above, against its
|
|
// own vertex order, so the winding can only be turned round after that - here, on the copy that
|
|
// becomes the soup - and has to be turned back on the way out, since the caller undoes the same
|
|
// mirror when it maps the result back into the volume's coordinates.
|
|
indexed_triangle_set oriented = mesh;
|
|
if (flip_normals)
|
|
for (stl_triangle_vertex_indices &t : oriented.indices)
|
|
std::swap(t[1], t[2]);
|
|
|
|
// 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.
|
|
const size_t debug_mark = (debug != nullptr) ? debug->mark() : 0;
|
|
TextureBake::PipelineResult result = TextureBake::run_pipeline(
|
|
TextureBake::to_soup(oriented, excluded), sample, settings, bounds, mode, excluded,
|
|
[&progress](const char *, double f) {
|
|
return !progress || progress(std::clamp(int(f * 100.0), 0, 99));
|
|
},
|
|
debug, color_sample);
|
|
if (debug != nullptr && flip_normals)
|
|
debug->rebase(debug_mark, nullptr, /* flip_winding */ true);
|
|
if (result.canceled || result.geometry.empty())
|
|
return {};
|
|
|
|
if (stats != nullptr) {
|
|
stats->triangles_refined = result.triangles_refined;
|
|
stats->triangles_out = result.geometry.triangle_count();
|
|
stats->triangles_budget = result.triangles_budget;
|
|
stats->budget_limited = result.budget_limited;
|
|
}
|
|
indexed_triangle_set out = TextureBake::to_indexed_triangle_set(result.geometry);
|
|
if (out.indices.empty())
|
|
return mesh;
|
|
if (flip_normals)
|
|
for (stl_triangle_vertex_indices &t : out.indices)
|
|
std::swap(t[1], t[2]);
|
|
|
|
// 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 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 (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; });
|
|
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;
|
|
// 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);
|
|
}
|
|
});
|
|
// 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 &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));
|
|
}
|
|
}
|
|
*color->out_triangle = std::move(out_color);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
// The bake proper. Runs entirely in whatever space `base_mesh` is given in; the public entry point
|
|
// below is what puts it in world space and brings the result back.
|
|
// `flip_normals` says the mesh is wound the opposite way round from its outward direction, which is
|
|
// what a mirroring world transform leaves behind: the positions are right, but every normal derived
|
|
// from the winding points into the model. See build_texture_displacement().
|
|
static indexed_triangle_set build_texture_displacement_in_place(
|
|
const indexed_triangle_set &base_mesh,
|
|
const std::vector<TextureDisplacementLayer> &layers,
|
|
const TextureDisplacementFacetsData &facets_data,
|
|
const TextureDisplacementOptions &options,
|
|
const DisplacementProgressFn &progress,
|
|
const TextureColorRequest *color,
|
|
bool flip_normals,
|
|
BakeStageRecorder *debug,
|
|
TextureBakeStats *stats)
|
|
{
|
|
// The classic path moves the vertices the mesh already has, so there is no budget to report on.
|
|
(void) stats;
|
|
// Returns true to keep going. An aborted run returns {} (see the header): an empty mesh is the
|
|
// one result no caller can mistake for a finished bake and commit onto the volume.
|
|
const auto report = [&progress](int percent) { return !progress || progress(percent); };
|
|
|
|
// Stage capture for the debug view. A stage is timed from the end of the previous one, so the
|
|
// capture itself - a copy plus an edge scan - sits outside every measurement it reports.
|
|
auto stage_clock = std::chrono::steady_clock::now();
|
|
const auto capture = [&](const char *name, const indexed_triangle_set &m,
|
|
const std::string &detail = {}) {
|
|
if (debug == nullptr)
|
|
return;
|
|
const double ms =
|
|
std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - stage_clock).count();
|
|
debug->capture(name, m.vertices, m.indices, ms, detail);
|
|
stage_clock = std::chrono::steady_clock::now();
|
|
};
|
|
|
|
indexed_triangle_set mesh = base_mesh;
|
|
// TriangleSelector's vertex array starts with the mesh's own vertices (any extra ones, created
|
|
// where a brush stroke split a triangle, are appended after them), and get_facets_strict()
|
|
// emits exactly the *referenced* ones, in order. So selector vertex index i is our vertex i -
|
|
// but only if every vertex of `mesh` is referenced by some triangle, which is precisely what
|
|
// this call establishes. It is a no-op (indices untouched) for any mesh that already is, which
|
|
// in practice is all of them; it exists so an input carrying stray unreferenced vertices can't
|
|
// silently shift the indexing and displace the wrong vertices.
|
|
its_compactify_vertices(mesh);
|
|
if (mesh.vertices.empty() || mesh.indices.empty())
|
|
return mesh;
|
|
|
|
capture("input", mesh, "as handed to the bake");
|
|
|
|
if (options.pipeline_v2)
|
|
return build_texture_displacement_v2(mesh, layers, facets_data, options, progress, color, flip_normals,
|
|
debug, stats);
|
|
|
|
// Layers are combined in slot order, like stacked layers in an image editor: each one folds its
|
|
// own displacement into the running total via its blend mode (see TextureBlendMode).
|
|
std::vector<const TextureDisplacementLayer *> ordered_layers;
|
|
for (const TextureDisplacementLayer &layer : layers)
|
|
if (!layer.empty() && layer.slot >= 0 && size_t(layer.slot) < TEXTURE_DISPLACEMENT_MAX_LAYERS)
|
|
ordered_layers.push_back(&layer);
|
|
std::sort(ordered_layers.begin(), ordered_layers.end(),
|
|
[](const TextureDisplacementLayer *a, const TextureDisplacementLayer *b) { return a->slot < b->slot; });
|
|
|
|
// Every layer measures its displacement against the *original* surface - normals included -
|
|
// rather than against whatever the previous layer left behind. That is what lets all the layers
|
|
// be evaluated independently and merged per vertex, instead of having to re-mesh and remap the
|
|
// paint masks between them (see the header for why that earlier design was dropped).
|
|
std::vector<Vec3f> vertex_normals = texture_displacement_vertex_normals(mesh);
|
|
|
|
// ... with one correction, applied where the painted area does not cover every triangle around a
|
|
// vertex: there the direction to move in is the normal of the *painted* surface, not of the whole
|
|
// mesh. On the rim of a fully painted top face the whole-mesh normal is the 45 degrees bisector
|
|
// between the face and the side wall it meets, so displacing along it flares the rim outwards
|
|
// instead of raising it. Taken over the union of every layer's paint (triangles_to_split is
|
|
// exactly the set of original triangles a layer's brush touched - serialize() records an entry for
|
|
// each one that is split or carries a non-default state), so a vertex still has one single
|
|
// direction however many layers cover it. Interior vertices are unaffected: all their triangles
|
|
// are painted, so the two normals coincide.
|
|
{
|
|
std::vector<uint8_t> painted_face(mesh.indices.size(), 0);
|
|
bool any_paint = false;
|
|
for (const TextureDisplacementLayer *layer : ordered_layers)
|
|
for (const TriangleSelector::TriangleBitStreamMapping &m : facets_data[size_t(layer->slot)].triangles_to_split)
|
|
if (size_t(m.triangle_idx) < mesh.indices.size()) {
|
|
painted_face[size_t(m.triangle_idx)] = 1;
|
|
any_paint = true;
|
|
}
|
|
if (any_paint) {
|
|
std::vector<Vec3f> painted_normals(mesh.vertices.size(), Vec3f::Zero());
|
|
for (size_t i = 0; i < mesh.indices.size(); ++i) {
|
|
if (!painted_face[i])
|
|
continue;
|
|
const stl_triangle_vertex_indices &t = mesh.indices[i];
|
|
const Vec3f fn = (mesh.vertices[t[1]] - mesh.vertices[t[0]]).cross(mesh.vertices[t[2]] - mesh.vertices[t[0]]);
|
|
for (int k = 0; k < 3; ++k)
|
|
painted_normals[size_t(t[k])] += fn; // area-weighted, same convention as the full normals
|
|
}
|
|
for (size_t v = 0; v < vertex_normals.size(); ++v)
|
|
if (const float l = painted_normals[v].norm(); l > 1e-8f)
|
|
vertex_normals[v] = painted_normals[v] / l;
|
|
// else: no painted triangle touches this vertex, so it will not be displaced anyway -
|
|
// leave the whole-mesh normal in place rather than zeroing it.
|
|
}
|
|
}
|
|
|
|
// Both normal passes above read their direction out of the triangle winding, so a mirrored
|
|
// placement leaves every one of them pointing into the model - the relief would be carved rather
|
|
// than raised. Correct them once, here, where every later stage (displacement direction, the
|
|
// planar/triplanar projection axes, the cylinder axis) picks them up already right.
|
|
if (flip_normals)
|
|
for (Vec3f &n : vertex_normals)
|
|
n = -n;
|
|
|
|
if (!report(5))
|
|
return {};
|
|
|
|
std::vector<float> displacement(mesh.vertices.size(), 0.f);
|
|
// uint8_t rather than std::vector<bool>: the sampling loop below writes these from several
|
|
// threads at once, and vector<bool>'s bit packing makes writes to *distinct* elements a data
|
|
// race on the shared word.
|
|
std::vector<uint8_t> displaced(mesh.vertices.size(), 0);
|
|
// Union, over every layer, of that layer's patch border - the vertices the post-process smoothing
|
|
// holds when TextureDisplacementOptions::smooth_skip_border is set. A vertex on any patch's edge
|
|
// counts, which is the conservative choice: hold it rather than let one layer's smoothing melt the
|
|
// rim another layer put there.
|
|
std::vector<bool> on_patch_border(mesh.vertices.size(), false);
|
|
bool any_displacement = false;
|
|
|
|
// Colour is accumulated per *triangle*, not per vertex: it ends up in the volume's
|
|
// mmu_segmentation_facets, which assigns one filament to a whole facet. Layers are visited in
|
|
// ascending slot order, so a higher layer simply overwrites a lower one's colour where they
|
|
// overlap - the painter's-algorithm reading of a layer stack, and the one that matches how the
|
|
// panel lists them.
|
|
const bool want_color = color != nullptr && color->out_triangle != nullptr && bool(color->quantize);
|
|
// Palette indices, not filament indices: -1 for "no colour here". Kept in perceived-colour space
|
|
// for the whole pass so the despeckle filter below operates on what the eye sees, and the
|
|
// interleaving that turns a mixed entry into two real filaments happens once, at the very end.
|
|
std::vector<int> triangle_palette;
|
|
if (want_color)
|
|
triangle_palette.assign(mesh.indices.size(), -1);
|
|
|
|
const TriangleMesh selector_mesh(mesh);
|
|
// One selector for the whole stack, re-deserialized per layer. Its constructor computes
|
|
// its_face_neighbors() and its_face_normals() over the *entire* mesh, which on a subdivided model
|
|
// is by far the most expensive thing here - building a fresh one per layer paid that cost up to
|
|
// eight times over. reset() (what deserialize(..., true) calls) only rebuilds the vertex/triangle
|
|
// arrays; the neighbour and face-normal tables are immutable members and survive it.
|
|
TriangleSelector selector(selector_mesh);
|
|
bool selector_dirty = false;
|
|
|
|
const int layer_count = std::max(int(ordered_layers.size()), 1);
|
|
int layer_index = 0;
|
|
for (const TextureDisplacementLayer *layer : ordered_layers) {
|
|
// Progress spans 5..65% across the layers; the apply and smoothing passes take it from there.
|
|
if (!report(5 + (60 * layer_index++) / layer_count))
|
|
return {};
|
|
|
|
const TriangleSelector::TriangleSplittingData &data = facets_data[size_t(layer->slot)];
|
|
if (data.triangles_to_split.empty())
|
|
continue;
|
|
|
|
const DecodedHeightTexture height = decode_height_texture(*layer);
|
|
if (height.empty())
|
|
continue;
|
|
|
|
// needs_reset only from the second layer on: the selector is already pristine on the first.
|
|
selector.deserialize(data, selector_dirty);
|
|
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);
|
|
if (patch.indices.empty())
|
|
continue;
|
|
// get_facets_strict() returns the same vertex array whichever state is asked for (only the
|
|
// triangles are filtered), so `patch` and `rest` share one indexing - and, per the
|
|
// compactify above, it is our own.
|
|
const indexed_triangle_set rest = selector.get_facets_strict(EnforcerBlockerType::NONE);
|
|
|
|
// A vertex used by even one *unpainted* triangle sits on this layer's boundary. It is still
|
|
// needed either way - the edge-smoothing falloff measures distance from it - but whether it is
|
|
// held flat is now the user's call (TextureDisplacementOptions::displace_border), because
|
|
// nothing can tear: the bake is topology-preserving, so a border vertex is one vertex shared
|
|
// by both regions and moving it just tilts the unpainted triangles that use it.
|
|
std::vector<bool> is_boundary(patch.vertices.size(), false);
|
|
for (const stl_triangle_vertex_indices &tri : rest.indices)
|
|
for (int i = 0; i < 3; ++i) {
|
|
is_boundary[tri[i]] = true;
|
|
if (tri[i] < int(mesh.vertices.size()))
|
|
on_patch_border[size_t(tri[i])] = true;
|
|
}
|
|
const bool pin_boundary = !options.displace_border;
|
|
|
|
// 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
|
|
// for every slider tweak costs a hash rather than a re-solve (see compute_patch_unwrap()).
|
|
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
|
|
// than of the whole triangle's worth of texture - and so a triangle straddling a colour
|
|
// boundary lands on whichever side covers more of it, instead of on whichever sub-triangle
|
|
// happened to be emitted first. One quantize call per triangle, after the averaging.
|
|
if (color_this_layer) {
|
|
const DecodedHeightTexture &tex = height;
|
|
if (tex.has_color()) {
|
|
std::vector<Vec3f> sum(mesh.indices.size(), Vec3f::Zero());
|
|
std::vector<float> sum_area(mesh.indices.size(), 0.f);
|
|
for (size_t j = 0; j < patch.indices.size() && j < patch_source.size(); ++j) {
|
|
const size_t S = size_t(patch_source[j]);
|
|
if (S >= mesh.indices.size())
|
|
continue;
|
|
const stl_triangle_vertex_indices &t = patch.indices[j];
|
|
const Vec3f &pa = patch.vertices[size_t(t[0])];
|
|
const Vec3f &pb = patch.vertices[size_t(t[1])];
|
|
const Vec3f &pc = patch.vertices[size_t(t[2])];
|
|
const float area2 = (pb - pa).cross(pc - pa).norm();
|
|
if (area2 <= 0.f)
|
|
continue;
|
|
const Vec3f centroid = (pa + pb + pc) / 3.f;
|
|
|
|
// The normal the triplanar blend weights by, and the unwrap coordinate the LSCM
|
|
// path needs, both averaged over the sub-triangle's corners - the same quantities
|
|
// the per-vertex height sampling uses, evaluated at the centroid instead.
|
|
Vec3f n = Vec3f::Zero();
|
|
Vec2f uv = Vec2f::Zero();
|
|
bool have_uv = !lscm_uvs.empty();
|
|
for (int k = 0; k < 3; ++k) {
|
|
const int vi = t[k];
|
|
if (vi < int(vertex_normals.size()))
|
|
n += vertex_normals[size_t(vi)];
|
|
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;
|
|
}
|
|
n = (n.norm() > 1e-8f) ? Vec3f(n.normalized()) : average_normal;
|
|
uv /= 3.f;
|
|
|
|
Vec3f rgb;
|
|
if (sample_layer_color(tex, *layer, centroid, n, rgb, patch_centroid, patch_axis,
|
|
have_uv ? &uv : nullptr)) {
|
|
sum[S] += area2 * rgb;
|
|
sum_area[S] += area2;
|
|
}
|
|
}
|
|
for (size_t i = 0; i < mesh.indices.size(); ++i)
|
|
if (sum_area[i] > 0.f) {
|
|
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)
|
|
triangle_palette[i] = idx;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Edge smoothing: a per-vertex weight in [0, 1] that fades the displacement to zero toward the
|
|
// patch boundary. amount->0 leaves only the very edge softened; amount->1 fades the whole patch
|
|
// flat. k = (1-a)/a turns the normalized boundary distance into that weight (see the header).
|
|
std::vector<float> edge_weight;
|
|
if (layer->edge_smoothing && layer->edge_smoothing_amount > 0.f) {
|
|
const std::vector<float> bdist = patch_boundary_distance(patch, is_boundary);
|
|
float max_d = 0.f;
|
|
for (const float d : bdist)
|
|
if (std::isfinite(d))
|
|
max_d = std::max(max_d, d);
|
|
edge_weight.assign(patch.vertices.size(), 1.f);
|
|
if (max_d > 1e-6f) {
|
|
const float a = std::clamp(layer->edge_smoothing_amount, 0.f, 1.f);
|
|
const float k = (a < 1.f) ? (1.f - a) / std::max(a, 1e-3f) : 0.f;
|
|
for (size_t v = 0; v < bdist.size() && v < edge_weight.size(); ++v) {
|
|
const float d = std::isfinite(bdist[v]) ? bdist[v] : max_d;
|
|
edge_weight[v] = std::clamp((d / max_d) * k, 0.f, 1.f);
|
|
}
|
|
}
|
|
}
|
|
|
|
const float sign = layer->invert ? -1.f : 1.f;
|
|
// A vertex may be reached by several of the patch's triangles; each must fold into the
|
|
// running total exactly once, or a Multiply/Subtract layer would apply two or three times
|
|
// over depending on how many painted triangles happen to share the vertex. Collecting the
|
|
// unique list up front (cheap, one pass) is also what lets the expensive part - the texture
|
|
// sampling, which is three bilinear fetches plus three pow()s per vertex for triplanar - run
|
|
// in parallel below, instead of serially inside the triangle walk.
|
|
std::vector<int> layer_vertices;
|
|
std::vector<char> visited(patch.vertices.size(), 0);
|
|
layer_vertices.reserve(patch.vertices.size());
|
|
for (const stl_triangle_vertex_indices &tri : patch.indices)
|
|
for (int i = 0; i < 3; ++i) {
|
|
const int vi = tri[i];
|
|
// Split vertices the brush introduced live past the end of our own vertex array;
|
|
// they carry no displacement of their own and are not part of the output mesh.
|
|
if (vi >= int(mesh.vertices.size()) || (pin_boundary && is_boundary[vi]) || visited[vi])
|
|
continue;
|
|
visited[vi] = 1;
|
|
layer_vertices.push_back(vi);
|
|
}
|
|
if (layer_vertices.empty())
|
|
continue;
|
|
|
|
std::vector<float> sampled(layer_vertices.size(), 0.f);
|
|
tbb::parallel_for(tbb::blocked_range<size_t>(0, layer_vertices.size()),
|
|
[&](const tbb::blocked_range<size_t> &range) {
|
|
for (size_t k = range.begin(); k < range.end(); ++k) {
|
|
const size_t vi = size_t(layer_vertices[k]);
|
|
const Vec2f *lscm_uv = lscm_uvs.empty() ? nullptr : &lscm_uvs[vi];
|
|
const float h = sample_layer_height(height, *layer, mesh.vertices[vi], vertex_normals[vi],
|
|
patch_centroid, patch_axis, lscm_uv);
|
|
// midlevel is the height that means "stay put", so anything below it displaces
|
|
// *inwards* - see TextureDisplacementLayer::midlevel. At the default of 0 this is
|
|
// exactly the old outward-only behaviour.
|
|
sampled[k] = (h - layer->midlevel) * layer->depth_mm * sign;
|
|
}
|
|
});
|
|
|
|
for (size_t k = 0; k < layer_vertices.size(); ++k) {
|
|
const size_t vi = size_t(layer_vertices[k]);
|
|
// The first layer to reach a vertex has nothing underneath it to blend with, so it
|
|
// always starts the total off additively - a Multiply/Divide against an implicit
|
|
// zero base would otherwise annihilate (or blow up) it, which is never what the
|
|
// user means by putting a mask on the bottom of the stack.
|
|
const float accumulated = displacement[vi];
|
|
const float blended = blend_displacement(accumulated, sampled[k],
|
|
displaced[vi] ? layer->blend_mode : TextureBlendMode::Add);
|
|
// Edge smoothing fades this layer's *effect*, not its input. Scaling the input instead is
|
|
// only correct for Add/Subtract, whose neutral value is 0: on a Multiply layer a faded
|
|
// input approaches 0, which annihilates everything beneath it at the rim rather than
|
|
// leaving it alone, and on a Divide layer it approaches the 0.05 divisor floor, which
|
|
// amplifies the relief underneath by up to 20x exactly where it was meant to fade out.
|
|
// Interpolating the blended result back toward the accumulated total is the neutral
|
|
// element for every mode at once, and reduces to the old formula exactly for Add.
|
|
const float edge_w = edge_weight.empty() ? 1.f : edge_weight[vi];
|
|
displacement[vi] = accumulated + (blended - accumulated) * edge_w;
|
|
displaced[vi] = 1;
|
|
}
|
|
any_displacement = true;
|
|
}
|
|
|
|
if (!report(65))
|
|
return {};
|
|
|
|
if (!any_displacement)
|
|
return mesh;
|
|
|
|
// The model's own resting plane, taken before anything moves - see the clamp after smoothing.
|
|
float resting_z = std::numeric_limits<float>::max();
|
|
for (const Vec3f &v : mesh.vertices)
|
|
resting_z = std::min(resting_z, v.z());
|
|
|
|
size_t moved_count = 0;
|
|
for (size_t vi = 0; vi < mesh.vertices.size(); ++vi)
|
|
if (displaced[vi]) {
|
|
mesh.vertices[vi] += vertex_normals[vi] * displacement[vi];
|
|
++moved_count;
|
|
}
|
|
capture("displace", mesh, std::to_string(moved_count) + " of " +
|
|
std::to_string(mesh.vertices.size()) + " vertices moved");
|
|
|
|
// Post-process relaxation of what the height maps left behind, restricted to the vertices that
|
|
// actually moved - the untouched part of the model keeps its exact geometry, and the ring of
|
|
// vertices just outside the displaced set stays put and anchors the smoothing so the relief does
|
|
// not creep outward. `smooth_skip_border` additionally holds the patch's own outermost ring, whose
|
|
// neighbours are those pinned outsiders: relaxing it would drag the rim of the relief back down and
|
|
// leave the pattern looking half-melted right where it meets the edge.
|
|
if (options.smooth_enabled && options.smooth_strength > 0.f && options.smooth_iterations > 0) {
|
|
if (!report(70))
|
|
return {};
|
|
std::vector<uint8_t> movable(mesh.vertices.size(), 0);
|
|
for (size_t vi = 0; vi < mesh.vertices.size(); ++vi)
|
|
movable[vi] = (displaced[vi] && !(options.smooth_skip_border && on_patch_border[vi])) ? 1 : 0;
|
|
// The pass hook only *stops* the relaxation early; the report(99) below is what turns a
|
|
// cancellation into an empty (uncommittable) result, since a cancelled run keeps reporting
|
|
// cancelled.
|
|
smooth_mesh_vertices(mesh, movable, options.smooth_strength, options.smooth_iterations,
|
|
progress ? DisplacementProgressFn([&report, it = options.smooth_iterations](int pass) {
|
|
return report(70 + (29 * (pass + 1)) / std::max(it, 1));
|
|
}) : DisplacementProgressFn{});
|
|
capture("smooth", mesh,
|
|
std::to_string(options.smooth_iterations) + " iterations at strength " +
|
|
std::to_string(options.smooth_strength));
|
|
}
|
|
|
|
// Nothing driven below the model's own resting plane can be printed: it is either through the
|
|
// build plate or, once the slicer drops the part back down onto it, holding the whole model up in
|
|
// the air. Push it back up to the plane. Only vertices the displacement actually moved are
|
|
// eligible - untouched geometry is already exactly where it started - and only those that ended
|
|
// up below it, so downward relief that stays clear of the plate is left alone. Runs in world
|
|
// space (see build_texture_displacement()), so this really is the plate and not some scaled
|
|
// stand-in for it.
|
|
size_t clamped_count = 0;
|
|
if (resting_z < std::numeric_limits<float>::max())
|
|
for (size_t vi = 0; vi < mesh.vertices.size(); ++vi)
|
|
if (displaced[vi] && mesh.vertices[vi].z() < resting_z) {
|
|
mesh.vertices[vi].z() = resting_z;
|
|
++clamped_count;
|
|
}
|
|
capture("clamp to plate", mesh, std::to_string(clamped_count) + " vertices raised to z = " +
|
|
std::to_string(resting_z));
|
|
|
|
if (!report(99))
|
|
return {};
|
|
if (want_color) {
|
|
// Despeckle in perceived-colour space, then resolve each entry to a real filament. The order
|
|
// matters both ways round: filtering after the interleave would erase the bands it is supposed
|
|
// 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 &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));
|
|
}
|
|
// Handed over only on a run that completed: every early return above is a cancellation, and
|
|
// the caller must not commit a half-computed colouring any more than a half-displaced mesh.
|
|
*color->out_triangle = std::move(out_color);
|
|
}
|
|
return mesh;
|
|
}
|
|
|
|
indexed_triangle_set build_texture_displacement(const indexed_triangle_set &base_mesh,
|
|
const std::vector<TextureDisplacementLayer> &layers,
|
|
const TextureDisplacementFacetsData &facets_data,
|
|
const TextureDisplacementOptions &options,
|
|
const DisplacementProgressFn &progress,
|
|
const TextureColorRequest *color,
|
|
const Transform3d &volume_to_world,
|
|
BakeStageRecorder *debug,
|
|
TextureBakeStats *stats)
|
|
{
|
|
// An untransformed volume on an untransformed instance is by far the common case, and the round
|
|
// trip costs two matrix multiplies per vertex on a mesh that can carry millions of them - so take
|
|
// the identity out of the way rather than paying for it.
|
|
// The texture is projected in the bake frame - world orientation and scale, anchored at the
|
|
// volume's origin (see texture_displacement_bake_frame()).
|
|
const Transform3d frame = texture_displacement_bake_frame(volume_to_world);
|
|
if (frame.matrix().isApprox(Transform3d::Identity().matrix()))
|
|
return build_texture_displacement_in_place(base_mesh, layers, facets_data, options, progress, color,
|
|
false, debug, stats);
|
|
|
|
const Transform3d to_local = frame.inverse();
|
|
// A mirroring placement leaves the positions correct but every winding-derived normal pointing
|
|
// the wrong way. The winding itself is deliberately *not* touched here: the paint masks encode
|
|
// each split triangle against its own vertex order, so reordering a triangle's vertices would
|
|
// mirror the paint inside it. The bake is told instead, and negates the normals it computes.
|
|
const bool mirrored = frame.linear().determinant() < 0.0;
|
|
|
|
indexed_triangle_set world = base_mesh;
|
|
for (Vec3f &v : world.vertices)
|
|
v = (frame * v.cast<double>()).cast<float>();
|
|
|
|
const size_t debug_mark = (debug != nullptr) ? debug->mark() : 0;
|
|
indexed_triangle_set out = build_texture_displacement_in_place(world, layers, facets_data, options,
|
|
progress, color, mirrored, debug, stats);
|
|
// Everything the bake recorded is in world millimetres, like `out` itself. The debug view draws in
|
|
// the volume's local frame, so the stages are brought back the same way the result is.
|
|
if (debug != nullptr)
|
|
debug->rebase(debug_mark, &to_local, /* flip_winding */ false);
|
|
// A cancelled run returns {} and must stay {} - an empty mesh is the signal the caller checks
|
|
// before committing anything onto the volume.
|
|
if (out.vertices.empty())
|
|
return out;
|
|
|
|
for (Vec3f &v : out.vertices)
|
|
v = (to_local * v.cast<double>()).cast<float>();
|
|
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
|
|
// texture then rides with the model when it is moved about the plate (and a single, untiled stamp
|
|
// starts on the model rather than at the plate's corner), while a tile is still `tiling_scale`
|
|
// printed millimetres whatever the instance's scale.
|
|
return Eigen::Translation3d(-volume_to_world.translation()) * volume_to_world;
|
|
}
|
|
|
|
Transform3d texture_displacement_volume_to_world(const ModelVolume &volume)
|
|
{
|
|
const ModelObject *object = volume.get_object();
|
|
if (object == nullptr || object->instances.empty() || object->instances.front() == nullptr)
|
|
return volume.get_matrix();
|
|
return object->instances.front()->get_matrix() * volume.get_matrix();
|
|
}
|
|
|
|
indexed_triangle_set build_texture_displacement(const ModelVolume &volume)
|
|
{
|
|
TextureDisplacementFacetsData facets_data;
|
|
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i)
|
|
facets_data[size_t(i)] = volume.texture_displacement_facet(i).get_data();
|
|
|
|
return build_texture_displacement(volume.mesh().its, volume.texture_displacement_layers, facets_data,
|
|
volume.texture_displacement_options, {}, nullptr,
|
|
texture_displacement_volume_to_world(volume));
|
|
}
|
|
|
|
void smooth_mesh_vertices(indexed_triangle_set &mesh, const std::vector<uint8_t> &movable, float strength,
|
|
int iterations, const DisplacementProgressFn &on_pass)
|
|
{
|
|
if (iterations <= 0 || mesh.vertices.empty() || movable.size() != mesh.vertices.size())
|
|
return;
|
|
strength = std::clamp(strength, 0.f, 1.f);
|
|
if (strength <= 0.f)
|
|
return;
|
|
if (std::none_of(movable.begin(), movable.end(), [](uint8_t m) { return m != 0; }))
|
|
return;
|
|
|
|
// One-ring neighbours as a CSR-style pair of arrays: counted, prefix-summed, then filled. A
|
|
// triangle contributes each of its edges to both endpoints, so a shared edge is listed once per
|
|
// incident triangle - the duplicates are harmless here, they just weight an interior edge the same
|
|
// way from both sides, and dropping them would cost a sort per vertex for no visible difference.
|
|
const size_t nv = mesh.vertices.size();
|
|
std::vector<int> start(nv + 1, 0);
|
|
for (const stl_triangle_vertex_indices &t : mesh.indices)
|
|
for (int e = 0; e < 3; ++e) {
|
|
++start[size_t(t[e]) + 1];
|
|
++start[size_t(t[(e + 1) % 3]) + 1];
|
|
}
|
|
for (size_t v = 0; v < nv; ++v)
|
|
start[v + 1] += start[v];
|
|
const size_t total_refs = size_t(start[nv]);
|
|
std::vector<int> nbr(total_refs, 0);
|
|
std::vector<int> fill(start.begin(), start.begin() + nv);
|
|
for (const stl_triangle_vertex_indices &t : mesh.indices)
|
|
for (int e = 0; e < 3; ++e) {
|
|
const int a = t[e], b = t[(e + 1) % 3];
|
|
nbr[size_t(fill[size_t(a)]++)] = b;
|
|
nbr[size_t(fill[size_t(b)]++)] = a;
|
|
}
|
|
|
|
// Read every pass from a snapshot of the previous one, so the result does not depend on the order
|
|
// vertices happen to be visited in (a Gauss-Seidel sweep would smooth several times as hard at the
|
|
// end of the array as at the start).
|
|
std::vector<Vec3f> prev;
|
|
for (int it = 0; it < iterations; ++it) {
|
|
prev = mesh.vertices;
|
|
// Each vertex reads only from `prev` and writes only its own slot, so the sweep parallelises
|
|
// with no synchronisation at all.
|
|
tbb::parallel_for(tbb::blocked_range<size_t>(0, nv), [&](const tbb::blocked_range<size_t> &range) {
|
|
for (size_t v = range.begin(); v < range.end(); ++v) {
|
|
if (!movable[v] || start[v] == start[v + 1])
|
|
continue;
|
|
Vec3f sum = Vec3f::Zero();
|
|
for (int k = start[v]; k < start[v + 1]; ++k)
|
|
sum += prev[size_t(nbr[size_t(k)])];
|
|
const Vec3f avg = sum / float(start[v + 1] - start[v]);
|
|
mesh.vertices[v] = prev[v] + (avg - prev[v]) * strength;
|
|
}
|
|
});
|
|
if (on_pass && !on_pass(it))
|
|
return; // cancelled: leave the passes done so far in place, the caller decides what to do
|
|
}
|
|
}
|
|
|
|
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
|
|
// where a layer is placed. `need_color` additionally drops layers that cannot contribute colour.
|
|
std::shared_ptr<std::vector<PreparedLayer>> prepare_sampleable_layers(
|
|
const indexed_triangle_set &base_mesh, const std::vector<TextureDisplacementLayer> &layers,
|
|
const TextureDisplacementFacetsData &facets_data, bool need_color)
|
|
{
|
|
auto prepared = std::make_shared<std::vector<PreparedLayer>>();
|
|
|
|
if (base_mesh.indices.empty())
|
|
return prepared;
|
|
|
|
std::vector<const TextureDisplacementLayer *> ordered;
|
|
for (const TextureDisplacementLayer &l : layers)
|
|
if (l.slot >= 0 && l.slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS))
|
|
ordered.push_back(&l);
|
|
std::sort(ordered.begin(), ordered.end(),
|
|
[](const TextureDisplacementLayer *a, const TextureDisplacementLayer *b) { return a->slot < b->slot; });
|
|
|
|
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) {
|
|
// 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)];
|
|
if (data.triangles_to_split.empty())
|
|
continue;
|
|
const DecodedHeightTexture tex = decode_height_texture(*layer);
|
|
if (tex.empty() || (need_color && !tex.has_color()))
|
|
continue;
|
|
|
|
TriangleSelector selector(selector_mesh);
|
|
selector.deserialize(data, false);
|
|
const indexed_triangle_set patch = selector.get_facets_strict(EnforcerBlockerType::ENFORCER);
|
|
if (patch.indices.empty())
|
|
continue;
|
|
|
|
// 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 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);
|
|
}
|
|
}
|
|
|
|
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, std::move(lscm), std::move(region) });
|
|
}
|
|
return prepared;
|
|
}
|
|
} // namespace
|
|
|
|
ColorFieldSampler make_combined_color_sampler(const indexed_triangle_set &base_mesh,
|
|
const std::vector<TextureDisplacementLayer> &layers,
|
|
const TextureDisplacementFacetsData &facets_data,
|
|
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, 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 (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, lscm_uv))
|
|
if (const int idx = ((*pure)[i] ? quantize_pure : quantize)(rgb); idx >= 0)
|
|
result = idx;
|
|
}
|
|
return result;
|
|
};
|
|
}
|
|
|
|
HeightFieldSampler make_combined_displacement_sampler(const indexed_triangle_set &base_mesh,
|
|
const std::vector<TextureDisplacementLayer> &layers,
|
|
const TextureDisplacementFacetsData &facets_data)
|
|
{
|
|
auto prepared = prepare_sampleable_layers(base_mesh, layers, facets_data, /* need_color */ false);
|
|
if (prepared->empty())
|
|
return nullptr;
|
|
|
|
return [prepared](const Vec3f &pos, const Vec3f &normal) -> float {
|
|
float total = 0.f;
|
|
bool any = false;
|
|
for (const PreparedLayer &p : *prepared) {
|
|
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 -
|
|
// same rule build_texture_displacement() applies per vertex.
|
|
total = blend_displacement(total, signed_h, any ? p.layer.blend_mode : TextureBlendMode::Add);
|
|
any = true;
|
|
}
|
|
return total;
|
|
};
|
|
}
|
|
|
|
indexed_triangle_set subdivide_mesh_uniform(const indexed_triangle_set &mesh, float max_edge_length_mm, int max_iterations)
|
|
{
|
|
indexed_triangle_set current = mesh;
|
|
const float max_edge_sq = max_edge_length_mm * max_edge_length_mm;
|
|
|
|
for (int iter = 0; iter < max_iterations; ++iter) {
|
|
// One vertex-index pair (always stored low-index-first) -> the midpoint vertex already
|
|
// created for it in this pass, so the two triangles sharing that edge both get the exact
|
|
// same new vertex instead of two separate, coincident-but-distinct ones (which would leave
|
|
// the mesh non-manifold even though it looks fine).
|
|
std::unordered_map<uint64_t, int> midpoint_cache;
|
|
auto edge_key = [](int a, int b) -> uint64_t {
|
|
if (a > b)
|
|
std::swap(a, b);
|
|
return (uint64_t(uint32_t(a)) << 32) | uint32_t(b);
|
|
};
|
|
auto get_midpoint = [&](int a, int b) -> int {
|
|
const uint64_t key = edge_key(a, b);
|
|
auto it = midpoint_cache.find(key);
|
|
if (it != midpoint_cache.end())
|
|
return it->second;
|
|
const int idx = int(current.vertices.size());
|
|
current.vertices.push_back((current.vertices[a] + current.vertices[b]) * 0.5f);
|
|
midpoint_cache.emplace(key, idx);
|
|
return idx;
|
|
};
|
|
|
|
std::vector<stl_triangle_vertex_indices> new_indices;
|
|
new_indices.reserve(current.indices.size());
|
|
bool any_split = false;
|
|
for (const stl_triangle_vertex_indices &tri : current.indices) {
|
|
const Vec3f &a = current.vertices[tri[0]];
|
|
const Vec3f &b = current.vertices[tri[1]];
|
|
const Vec3f &c = current.vertices[tri[2]];
|
|
if ((b - a).squaredNorm() <= max_edge_sq && (c - b).squaredNorm() <= max_edge_sq && (a - c).squaredNorm() <= max_edge_sq) {
|
|
new_indices.push_back(tri);
|
|
continue;
|
|
}
|
|
|
|
any_split = true;
|
|
const int m01 = get_midpoint(tri[0], tri[1]);
|
|
const int m12 = get_midpoint(tri[1], tri[2]);
|
|
const int m20 = get_midpoint(tri[2], tri[0]);
|
|
new_indices.push_back(stl_triangle_vertex_indices(tri[0], m01, m20));
|
|
new_indices.push_back(stl_triangle_vertex_indices(m01, tri[1], m12));
|
|
new_indices.push_back(stl_triangle_vertex_indices(m20, m12, tri[2]));
|
|
new_indices.push_back(stl_triangle_vertex_indices(m01, m12, m20));
|
|
}
|
|
|
|
current.indices = std::move(new_indices);
|
|
if (!any_split)
|
|
break;
|
|
}
|
|
|
|
return current;
|
|
}
|
|
|
|
indexed_triangle_set subdivide_mesh_adaptive(const indexed_triangle_set &mesh,
|
|
const std::vector<uint8_t> &refine_region,
|
|
float target_edge_length_mm, int max_triangles,
|
|
std::vector<int> *out_source, const HeightFieldSampler &sampler,
|
|
float chord_tolerance_mm, float min_edge_length_mm,
|
|
float border_edge_length_mm,
|
|
const DisplacementProgressFn &progress,
|
|
const ColorFieldSampler &color, float color_edge_length_mm,
|
|
bool split_multi_crossings)
|
|
{
|
|
// Neighbour slots that are not a triangle index.
|
|
constexpr int NB_BOUNDARY = -1; // open edge: terminal on its own, bisected from this side alone
|
|
constexpr int NB_NONMANIFOLD = -2; // >2 triangles on the edge: never bisected, that would tear it
|
|
|
|
// v[] and nb[] are parallel: nb[e] is the triangle across edge (v[e], v[(e+1)%3]). `src` is the
|
|
// input triangle this one descends from - children inherit it, so a caller can carry per-triangle
|
|
// data (a paint mask) across the topology change with no geometric remap.
|
|
struct Tri { int v[3]; int nb[3]; int src; };
|
|
|
|
std::vector<Vec3f> verts = mesh.vertices;
|
|
std::vector<Tri> tris(mesh.indices.size());
|
|
for (size_t i = 0; i < mesh.indices.size(); ++i)
|
|
tris[i] = { { mesh.indices[i][0], mesh.indices[i][1], mesh.indices[i][2] },
|
|
{ NB_BOUNDARY, NB_BOUNDARY, NB_BOUNDARY }, int(i) };
|
|
|
|
auto emit = [&]() -> indexed_triangle_set {
|
|
indexed_triangle_set out;
|
|
out.vertices = verts;
|
|
out.indices.reserve(tris.size());
|
|
if (out_source) {
|
|
out_source->clear();
|
|
out_source->reserve(tris.size());
|
|
}
|
|
for (const Tri &t : tris) {
|
|
out.indices.emplace_back(t.v[0], t.v[1], t.v[2]);
|
|
if (out_source)
|
|
out_source->push_back(t.src);
|
|
}
|
|
return out;
|
|
};
|
|
|
|
// Feature-adaptive when a sampler and a positive tolerance are supplied; otherwise refinement is
|
|
// driven by the length baseline alone.
|
|
const bool feature_mode = bool(sampler) && chord_tolerance_mm > 0.f;
|
|
const bool color_mode = bool(color) && color_edge_length_mm > 0.f;
|
|
const bool multi_mode = feature_mode && split_multi_crossings;
|
|
float step_iso_mm = 0.f; // the mid-level a step crosses; set once the region is sampled
|
|
float step_dev_mm = std::numeric_limits<float>::max(); // deviations above this are steps
|
|
const float color_sq = color_edge_length_mm > 0.f ? color_edge_length_mm * color_edge_length_mm : 0.f;
|
|
const float min_floor_sq = min_edge_length_mm > 0.f ? min_edge_length_mm * min_edge_length_mm : 0.f;
|
|
const float target_sq = target_edge_length_mm > 0.f ? target_edge_length_mm * target_edge_length_mm : 0.f;
|
|
const float border_sq = border_edge_length_mm > 0.f ? border_edge_length_mm * border_edge_length_mm : 0.f;
|
|
|
|
// refine_region is indexed by input-triangle index, and every triangle's src stays in that range
|
|
// (children inherit their parent's src), so a wrong size would be an out-of-bounds read. Guard it.
|
|
if (refine_region.size() != mesh.indices.size() || int(tris.size()) + 2 > max_triangles)
|
|
return emit();
|
|
if (!feature_mode && !color_mode && target_sq <= 0.f && border_sq <= 0.f)
|
|
return emit(); // no criterion at all
|
|
if (std::none_of(refine_region.begin(), refine_region.end(), [](uint8_t v) { return v != 0; }))
|
|
return emit(); // nothing flagged: no-op
|
|
|
|
auto edge_key = [](int a, int b) -> uint64_t {
|
|
if (a > b)
|
|
std::swap(a, b);
|
|
return (uint64_t(uint32_t(a)) << 32) | uint32_t(b);
|
|
};
|
|
|
|
// Edge adjacency, built once here and then maintained incrementally by bisect() below. Rebuilding
|
|
// it per refinement pass is what made the previous version's cost scale with the whole model
|
|
// instead of with the refined region, and what forced the tiny pass budget that stopped
|
|
// refinement short.
|
|
{
|
|
// Sort the half-edges by their edge key and walk the equal runs, rather than hashing every one
|
|
// of them twice into an unordered_map. Same result, but the two expensive parts - forming the
|
|
// keys and ordering them - both parallelise, where a shared hash map cannot. The map also cost
|
|
// a second full pass of lookups purely to read back what the first pass had just inserted.
|
|
std::vector<std::pair<uint64_t, int>> he(tris.size() * 3); // (edge key, triangle * 3 + local edge)
|
|
tbb::parallel_for(tbb::blocked_range<size_t>(0, tris.size()),
|
|
[&](const tbb::blocked_range<size_t> &range) {
|
|
for (size_t ti = range.begin(); ti < range.end(); ++ti)
|
|
for (int e = 0; e < 3; ++e)
|
|
he[ti * 3 + size_t(e)] = {
|
|
edge_key(tris[ti].v[e], tris[ti].v[(e + 1) % 3]), int(ti) * 3 + e
|
|
};
|
|
});
|
|
tbb::parallel_sort(he.begin(), he.end());
|
|
// Runs of equal key are the half-edges of one edge: one is a boundary, two are neighbours,
|
|
// more is non-manifold. Serial, but it is a single linear pass over an already ordered array.
|
|
for (size_t i = 0; i < he.size();) {
|
|
size_t j = i + 1;
|
|
while (j < he.size() && he[j].first == he[i].first)
|
|
++j;
|
|
const size_t count = j - i;
|
|
if (count == 2) {
|
|
const int a = he[i].second, b = he[i + 1].second;
|
|
tris[size_t(a / 3)].nb[a % 3] = b / 3;
|
|
tris[size_t(b / 3)].nb[b % 3] = a / 3;
|
|
} else if (count > 2) {
|
|
for (size_t k = i; k < j; ++k)
|
|
tris[size_t(he[k].second / 3)].nb[he[k].second % 3] = NB_NONMANIFOLD;
|
|
}
|
|
// count == 1 keeps the NB_BOUNDARY it was initialised with.
|
|
i = j;
|
|
}
|
|
}
|
|
|
|
// Feature mode: per-vertex surface normal, and the sampled displacement height at each vertex.
|
|
// Heights are filled in lazily - on a big model only a small painted region is ever looked at, and
|
|
// a sampler call is a texture fetch (plus trig) per layer, so sampling every vertex of the whole
|
|
// mesh up front was pure waste. Both arrays grow in lockstep with `verts`.
|
|
std::vector<Vec3f> vnormal;
|
|
std::vector<float> vheight;
|
|
std::vector<uint8_t> vheight_valid;
|
|
if (feature_mode) {
|
|
vnormal.assign(verts.size(), Vec3f::Zero());
|
|
for (const Tri &t : tris) {
|
|
const Vec3f fn = (verts[t.v[1]] - verts[t.v[0]]).cross(verts[t.v[2]] - verts[t.v[0]]); // area-weighted
|
|
for (int i = 0; i < 3; ++i)
|
|
vnormal[t.v[i]] += fn;
|
|
}
|
|
for (Vec3f &n : vnormal) {
|
|
const float l = n.norm();
|
|
n = (l > 1e-12f) ? Vec3f(n / l) : Vec3f(Vec3f::UnitZ());
|
|
}
|
|
vheight.assign(verts.size(), 0.f);
|
|
vheight_valid.assign(verts.size(), 0);
|
|
}
|
|
auto height_of = [&](int v) -> float {
|
|
if (!vheight_valid[v]) {
|
|
vheight[v] = sampler(verts[v], vnormal[v]);
|
|
vheight_valid[v] = 1;
|
|
}
|
|
return vheight[v];
|
|
};
|
|
|
|
// Per-vertex filament index, sampled lazily and cached the same way the heights are. Needs the
|
|
// vertex normals, which feature mode also builds - so colour mode builds them when it is on alone.
|
|
std::vector<int> vcolor;
|
|
std::vector<uint8_t> vcolor_valid;
|
|
if (color_mode) {
|
|
if (vnormal.empty()) {
|
|
vnormal.assign(verts.size(), Vec3f::Zero());
|
|
for (const Tri &t : tris) {
|
|
const Vec3f fn = (verts[t.v[1]] - verts[t.v[0]]).cross(verts[t.v[2]] - verts[t.v[0]]);
|
|
for (int i = 0; i < 3; ++i)
|
|
vnormal[t.v[i]] += fn;
|
|
}
|
|
for (Vec3f &n : vnormal) {
|
|
const float l = n.norm();
|
|
n = (l > 1e-12f) ? Vec3f(n / l) : Vec3f(Vec3f::UnitZ());
|
|
}
|
|
}
|
|
vcolor.assign(verts.size(), -2); // -2 = not sampled yet; -1 = sampled, no colour there
|
|
vcolor_valid.assign(verts.size(), 0);
|
|
}
|
|
auto color_of = [&](int v) -> int {
|
|
if (!vcolor_valid[v]) {
|
|
vcolor[v] = color(verts[v], vnormal[v]);
|
|
vcolor_valid[v] = 1;
|
|
}
|
|
return vcolor[v];
|
|
};
|
|
|
|
// Sample the input mesh's own vertices up front, in parallel, for the region that is going to be
|
|
// refined. A sampler call is a texture fetch plus the projection's trigonometry per layer, and it
|
|
// is by far the most expensive thing here - but taken one at a time from inside the refinement
|
|
// loop it is also strictly serial. Every one of these vertices is read by the very first scoring
|
|
// pass anyway, so doing them together costs nothing extra and hands the work to every core.
|
|
//
|
|
// Only the region, and only the *initial* vertices: the laziness this replaces exists so that a
|
|
// small painted patch on a big model does not pay for the whole model (see height_of()), and that
|
|
// still holds. Midpoints created later stay lazy, because they do not exist yet.
|
|
if (feature_mode || color_mode) {
|
|
std::vector<uint8_t> wanted(verts.size(), 0);
|
|
for (const Tri &t : tris)
|
|
if (refine_region[t.src] != 0)
|
|
for (int i = 0; i < 3; ++i)
|
|
wanted[size_t(t.v[i])] = 1;
|
|
// Each index is touched by exactly one iteration, so the lazy caches can be filled without
|
|
// synchronisation - and every value is the one height_of()/color_of() would have produced.
|
|
tbb::parallel_for(tbb::blocked_range<size_t>(0, verts.size()),
|
|
[&](const tbb::blocked_range<size_t> &range) {
|
|
for (size_t v = range.begin(); v < range.end(); ++v) {
|
|
if (!wanted[v])
|
|
continue;
|
|
if (feature_mode) {
|
|
vheight[v] = sampler(verts[v], vnormal[v]);
|
|
vheight_valid[v] = 1;
|
|
}
|
|
if (color_mode) {
|
|
vcolor[v] = color(verts[v], vnormal[v]);
|
|
vcolor_valid[v] = 1;
|
|
}
|
|
}
|
|
});
|
|
if (multi_mode) {
|
|
// The level a step crosses: midway through the relief actually present on the region.
|
|
float lo = std::numeric_limits<float>::max(), hi = -lo;
|
|
for (size_t v = 0; v < verts.size(); ++v)
|
|
if (wanted[v]) { lo = std::min(lo, vheight[v]); hi = std::max(hi, vheight[v]); }
|
|
step_iso_mm = (lo < hi) ? 0.5f * (lo + hi) : 0.f;
|
|
step_dev_mm = (lo < hi) ? 0.4f * (hi - lo) : std::numeric_limits<float>::max();
|
|
}
|
|
}
|
|
|
|
auto elen_sq = [&](int a, int b) -> float { return (verts[a] - verts[b]).squaredNorm(); };
|
|
|
|
// The one edge of a triangle taken as its "longest": greatest squared length, exact ties broken by
|
|
// the smaller (sorted) vertex-index key. Both triangles sharing an edge compute the same key for
|
|
// it, so they can never disagree about which of them is longest - the property the conformality
|
|
// argument and the LEPP walk's termination both rest on.
|
|
auto longest_local = [&](int ti) -> int {
|
|
const Tri &t = tris[ti];
|
|
int best = 0;
|
|
float bl = elen_sq(t.v[0], t.v[1]);
|
|
uint64_t bk = edge_key(t.v[0], t.v[1]);
|
|
for (int e = 1; e < 3; ++e) {
|
|
const float l = elen_sq(t.v[e], t.v[(e + 1) % 3]);
|
|
const uint64_t k = edge_key(t.v[e], t.v[(e + 1) % 3]);
|
|
if (l > bl || (l == bl && k < bk)) {
|
|
bl = l;
|
|
bk = k;
|
|
best = e;
|
|
}
|
|
}
|
|
return best;
|
|
};
|
|
|
|
// How far the *displaced* surface departs from the flat triangle, sampled across the WHOLE
|
|
// triangle - the three edge midpoints and the centroid - not just one edge midpoint. Sampling the
|
|
// interior is what catches a hill that sits inside a triangle (the blind spot of an edge-only
|
|
// test). Cached per triangle: it can only change when the triangle is split, and then both
|
|
// children are fresh entries.
|
|
std::vector<float> tri_err;
|
|
if (feature_mode)
|
|
tri_err.assign(tris.size(), -1.f);
|
|
// True when this triangle straddles a colour boundary: its corners, its edge midpoints and its
|
|
// centroid do not all take the same filament. The midpoints and centroid matter for the same
|
|
// reason they do in detail_error() - a boundary can cross a triangle without separating any two of
|
|
// its corners. Cached per triangle; a split invalidates both children.
|
|
std::vector<uint8_t> tri_color_split; // 0 = unknown, 1 = straddles, 2 = uniform
|
|
if (color_mode)
|
|
tri_color_split.assign(tris.size(), 0);
|
|
auto straddles_color = [&](int ti) -> bool {
|
|
if (tri_color_split[ti] != 0)
|
|
return tri_color_split[ti] == 1;
|
|
const Tri &t = tris[ti];
|
|
const Vec3f pa = verts[t.v[0]], pb = verts[t.v[1]], pc = verts[t.v[2]];
|
|
const Vec3f na = vnormal[t.v[0]], nb = vnormal[t.v[1]], nc = vnormal[t.v[2]];
|
|
const int ca = color_of(t.v[0]);
|
|
bool split = color_of(t.v[1]) != ca || color_of(t.v[2]) != ca;
|
|
if (!split) {
|
|
static const float BARY[4][3] = { { 0.5f, 0.5f, 0.f }, { 0.f, 0.5f, 0.5f },
|
|
{ 0.5f, 0.f, 0.5f }, { 1.f / 3, 1.f / 3, 1.f / 3 } };
|
|
for (const auto &w : BARY) {
|
|
Vec3f n = w[0] * na + w[1] * nb + w[2] * nc;
|
|
const float nl = n.norm();
|
|
n = (nl > 1e-12f) ? Vec3f(n / nl) : na;
|
|
if (color(w[0] * pa + w[1] * pb + w[2] * pc, n) != ca) {
|
|
split = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
tri_color_split[ti] = split ? 1 : 2;
|
|
return split;
|
|
};
|
|
|
|
// The spacing at which the chord test samples a triangle. Features narrower than the resolution
|
|
// floor cannot be resolved by refinement anyway, so sampling any finer than that only costs time;
|
|
// sampling any coarser is what let thin features fall between the samples. Below the floor's own
|
|
// scale it is held at 0.05 mm, since finer is invisible on an FDM part.
|
|
const float sample_spacing = std::max(min_edge_length_mm, 0.05f);
|
|
|
|
auto detail_error = [&](int ti) -> float {
|
|
if (tri_err[ti] >= 0.f)
|
|
return tri_err[ti];
|
|
const Tri &t = tris[ti];
|
|
const Vec3f pa = verts[t.v[0]], pb = verts[t.v[1]], pc = verts[t.v[2]];
|
|
const Vec3f na = vnormal[t.v[0]], nb = vnormal[t.v[1]], nc = vnormal[t.v[2]];
|
|
const float ha = height_of(t.v[0]), hb = height_of(t.v[1]), hc = height_of(t.v[2]);
|
|
|
|
// A barycentric lattice over the whole triangle, dense enough that no feature wider than the
|
|
// sample spacing can hide between two samples. Four fixed samples (the three edge midpoints
|
|
// and the centroid) were the previous test, and on a 1 mm starting triangle they left 0.5 mm
|
|
// gaps - wider than a grid line or a knurl ridge - so whole features were never seen, the
|
|
// triangle scored zero, and it was never refined. The lattice is bounded at 8 subdivisions
|
|
// (45 points) so a large triangle does not cost thousands of samples; anything it misses at
|
|
// that size is caught once its children are small enough for the lattice to reach it.
|
|
const float longest = std::sqrt(std::max({ (pb - pa).squaredNorm(), (pc - pb).squaredNorm(), (pa - pc).squaredNorm() }));
|
|
const int n = std::clamp(int(std::ceil(longest / sample_spacing)), 2, 8);
|
|
float maxerr = 0.f;
|
|
// Whether the mid-level contour crosses an edge with a jump that is a step's worth, walked at
|
|
// the sample spacing (the lattice below is too coarse on a long edge: a 1 mm edge gets 8
|
|
// samples, and a 0.15 mm grid line slips between them). Such a triangle is the cutter's.
|
|
bool sharp_cross = false;
|
|
if (multi_mode) {
|
|
const Vec3f *P[3] = { &pa, &pb, &pc };
|
|
const Vec3f *Nn[3] = { &na, &nb, &nc };
|
|
const float H[3] = { ha, hb, hc };
|
|
for (int e = 0; e < 3; ++e) {
|
|
const Vec3f &p0 = *P[e], &p1 = *P[(e + 1) % 3], &n0 = *Nn[e], &n1 = *Nn[(e + 1) % 3];
|
|
const int m = std::clamp(int(std::ceil((p1 - p0).norm() / sample_spacing)), 2, 32);
|
|
float last = H[e];
|
|
for (int i = 1; i <= m; ++i) {
|
|
float h;
|
|
if (i == m) h = H[(e + 1) % 3];
|
|
else {
|
|
const float t = float(i) / float(m);
|
|
Vec3f nn = n0 + (n1 - n0) * t;
|
|
const float nl = nn.norm();
|
|
nn = (nl > 1e-12f) ? Vec3f(nn / nl) : n0;
|
|
h = sampler(p0 + (p1 - p0) * t, nn);
|
|
}
|
|
if ((h > step_iso_mm) != (last > step_iso_mm) && std::abs(h - last) > step_dev_mm) {
|
|
sharp_cross = true;
|
|
break;
|
|
}
|
|
last = h;
|
|
}
|
|
if (sharp_cross)
|
|
break;
|
|
}
|
|
}
|
|
for (int i = 0; i <= n; ++i)
|
|
for (int j = 0; j <= n - i; ++j) {
|
|
const int k = n - i - j;
|
|
const float wa = float(i) / float(n), wb = float(j) / float(n), wc = float(k) / float(n);
|
|
float actual;
|
|
if (i == n) actual = ha;
|
|
else if (j == n) actual = hb;
|
|
else if (k == n) actual = hc;
|
|
else {
|
|
Vec3f nn = wa * na + wb * nb + wc * nc;
|
|
const float nl = nn.norm();
|
|
nn = (nl > 1e-12f) ? Vec3f(nn / nl) : na;
|
|
actual = sampler(wa * pa + wb * pb + wc * pc, nn);
|
|
// Corners lie on the plane by construction; only the rest carry chord error.
|
|
maxerr = std::max(maxerr, std::abs(actual - (wa * ha + wb * hb + wc * hc)));
|
|
}
|
|
}
|
|
// A sharp step crossing the triangle is the cutter's job. Refinement cannot bring such a
|
|
// triangle under tolerance - a discontinuity has no chord - and would only carpet the step
|
|
// down to the floor, so its chord error is not counted. A feature lying entirely inside the
|
|
// triangle crosses no edge and still counts, which is what makes it surface until it does
|
|
// cross one.
|
|
if (multi_mode && sharp_cross)
|
|
maxerr = 0.f;
|
|
tri_err[ti] = maxerr;
|
|
return maxerr;
|
|
};
|
|
|
|
// How many times over its criteria a triangle is: <= 1 means "good enough, leave it alone", and
|
|
// the larger the value the more a split buys. Driving the heap with this is what makes a run that
|
|
// runs out of budget spend it on the worst offenders instead of wherever a sweep happened to
|
|
// reach. Triangles outside the region always score 0 - they are only ever touched by the conformal
|
|
// closure below, never refined on their own account.
|
|
auto priority = [&](int ti) -> float {
|
|
const Tri &t = tris[ti];
|
|
const uint8_t flags = refine_region[t.src];
|
|
if (flags == 0)
|
|
return 0.f;
|
|
const int le = longest_local(ti);
|
|
const float ll = elen_sq(t.v[le], t.v[(le + 1) % 3]);
|
|
if (ll <= min_floor_sq)
|
|
return 0.f; // at the resolution floor - also what stops a sharp texture step going forever
|
|
float p = 0.f;
|
|
if (flags & REFINE_PAINTED) {
|
|
p = (target_sq > 0.f) ? ll / target_sq : 0.f;
|
|
if (feature_mode) {
|
|
p = std::max(p, detail_error(ti) / chord_tolerance_mm);
|
|
}
|
|
// Colour is per facet, so a colour boundary can only be drawn where there are edges along
|
|
// it. Length target, floored by min_edge_length_mm above, exactly like the border band.
|
|
if (color_mode && straddles_color(ti))
|
|
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(): 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;
|
|
};
|
|
|
|
// What ordering the heap by. priority() alone saturates: a triangle straddling a hard step in the
|
|
// texture scores relief / tolerance however small it gets, so its children always come back to the
|
|
// top and the whole budget is spent carpeting step edges down to the floor while a large triangle
|
|
// with a moderate error a few millimetres away is never reached. Weighting by area makes the key
|
|
// the surface error a split actually removes, which is what a fixed budget should be spent on -
|
|
// and once the budget is gone, what remains is spread evenly instead of piled on one feature.
|
|
auto tri_area = [&](int ti) -> float {
|
|
const Tri &t = tris[ti];
|
|
return 0.5f * (verts[t.v[1]] - verts[t.v[0]]).cross(verts[t.v[2]] - verts[t.v[0]]).norm();
|
|
};
|
|
auto heap_key = [&](int ti, float p) -> float { return p * tri_area(ti); };
|
|
|
|
auto set_nb = [&](int ti, int u, int v, int val) {
|
|
if (ti < 0)
|
|
return;
|
|
Tri &t = tris[ti];
|
|
for (int e = 0; e < 3; ++e)
|
|
if ((t.v[e] == u && t.v[(e + 1) % 3] == v) || (t.v[e] == v && t.v[(e + 1) % 3] == u)) {
|
|
t.nb[e] = val;
|
|
return;
|
|
}
|
|
};
|
|
|
|
// Bisects triangle `ti` across its local edge `e`, which the caller has established is terminal.
|
|
// Both triangles on that edge are split in the one operation, around a single shared midpoint -
|
|
// which is exactly why a hanging node (and so a crack) can never appear. `ti` and the opposite
|
|
// triangle are each reused as one of their own children, so only two back-pointers in the
|
|
// surrounding mesh need repointing. Triangles whose geometry changed are left in `touched`.
|
|
std::vector<int> touched;
|
|
auto bisect = [&](int ti, int e) -> bool {
|
|
const int a = tris[ti].v[e], b = tris[ti].v[(e + 1) % 3], c = tris[ti].v[(e + 2) % 3];
|
|
const int n = tris[ti].nb[e];
|
|
// Locate the shared edge from the far side before touching anything: bailing out half way
|
|
// through would be the one way this could leave a crack.
|
|
int f = -1;
|
|
if (n >= 0) {
|
|
for (int k = 0; k < 3; ++k) {
|
|
const int u = tris[n].v[k], w = tris[n].v[(k + 1) % 3];
|
|
if ((u == a && w == b) || (u == b && w == a)) {
|
|
f = k;
|
|
break;
|
|
}
|
|
}
|
|
if (f < 0) {
|
|
tris[ti].nb[e] = NB_NONMANIFOLD; // inconsistent adjacency: refuse to split across it
|
|
return false;
|
|
}
|
|
}
|
|
|
|
const int m = int(verts.size());
|
|
verts.push_back(0.5f * (verts[a] + verts[b]));
|
|
if (feature_mode || color_mode) {
|
|
const Vec3f mn = vnormal[a] + vnormal[b];
|
|
const float ml = mn.norm();
|
|
vnormal.push_back(ml > 1e-12f ? Vec3f(mn / ml) : vnormal[a]);
|
|
}
|
|
if (feature_mode) {
|
|
vheight.push_back(0.f);
|
|
vheight_valid.push_back(0);
|
|
}
|
|
if (color_mode) {
|
|
vcolor.push_back(-2);
|
|
vcolor_valid.push_back(0);
|
|
}
|
|
|
|
// Near side: ti becomes (a, m, c), the new triangle is (m, b, c). Both keep the original
|
|
// a->b->c winding.
|
|
const int nb_bc = tris[ti].nb[(e + 1) % 3];
|
|
const int nb_ca = tris[ti].nb[(e + 2) % 3];
|
|
const int src = tris[ti].src;
|
|
const int t2 = int(tris.size());
|
|
tris.push_back(Tri{ { m, b, c }, { NB_BOUNDARY, nb_bc, ti }, src });
|
|
{
|
|
Tri &t1 = tris[ti];
|
|
t1.v[0] = a; t1.v[1] = m; t1.v[2] = c;
|
|
t1.nb[0] = NB_BOUNDARY; t1.nb[1] = t2; t1.nb[2] = nb_ca;
|
|
}
|
|
set_nb(nb_bc, b, c, t2); // that outer neighbour borders the second child now, not ti
|
|
if (feature_mode) {
|
|
tri_err.push_back(-1.f);
|
|
tri_err[ti] = -1.f;
|
|
}
|
|
if (color_mode) {
|
|
tri_color_split.push_back(0);
|
|
tri_color_split[ti] = 0;
|
|
}
|
|
touched.assign({ ti, t2 });
|
|
|
|
if (n < 0) {
|
|
return true; // boundary edge: nothing on the far side to split
|
|
}
|
|
|
|
// Far side, same shape: n becomes (p, m, d), the new triangle is (m, q, d).
|
|
const int p = tris[n].v[f], q = tris[n].v[(f + 1) % 3], d = tris[n].v[(f + 2) % 3];
|
|
const int nb_qd = tris[n].nb[(f + 1) % 3];
|
|
const int nb_dp = tris[n].nb[(f + 2) % 3];
|
|
const int nsrc = tris[n].src;
|
|
const int n2 = int(tris.size());
|
|
tris.push_back(Tri{ { m, q, d }, { NB_BOUNDARY, nb_qd, n }, nsrc });
|
|
{
|
|
Tri &n1 = tris[n];
|
|
n1.v[0] = p; n1.v[1] = m; n1.v[2] = d;
|
|
n1.nb[0] = NB_BOUNDARY; n1.nb[1] = n2; n1.nb[2] = nb_dp;
|
|
}
|
|
set_nb(nb_qd, q, d, n2);
|
|
if (feature_mode) {
|
|
tri_err.push_back(-1.f);
|
|
tri_err[n] = -1.f;
|
|
}
|
|
if (color_mode) {
|
|
tri_color_split.push_back(0);
|
|
tri_color_split[n] = 0;
|
|
}
|
|
|
|
// Stitch the two sides back together: whichever far child holds `a` borders the near child
|
|
// that holds `a`. (Which one that is depends on how n happens to be wound.)
|
|
const int side_a = (p == a) ? n : n2;
|
|
const int side_b = (p == a) ? n2 : n;
|
|
tris[ti].nb[0] = side_a; // near child (a, m, c), edge (a, m)
|
|
tris[t2].nb[0] = side_b; // near child (m, b, c), edge (m, b)
|
|
set_nb(side_a, a, m, ti);
|
|
set_nb(side_b, b, m, t2);
|
|
touched.assign({ ti, t2, n, n2 });
|
|
return true;
|
|
};
|
|
|
|
// Worst-first. Entries go stale as their triangle is split; a stale entry is harmless - it is
|
|
// re-scored on pop and dropped or re-pushed. The ordering is a budget-allocation heuristic only:
|
|
// neither correctness nor conformality depends on it.
|
|
std::priority_queue<std::pair<float, int>> queue;
|
|
{
|
|
// Scoring the starting mesh means a detail_error() per triangle - four more sampler calls each
|
|
// - so it is worth spreading, even though the refinement that follows cannot be. Each entry is
|
|
// written by one iteration only, and the caches those calls fill (tri_err, tri_color_split) are
|
|
// likewise per triangle, so there is nothing shared to guard. The heap is then built from the
|
|
// finished array in index order, which is exactly the order the serial loop pushed in.
|
|
std::vector<float> initial(tris.size(), 0.f);
|
|
tbb::parallel_for(tbb::blocked_range<size_t>(0, tris.size()),
|
|
[&](const tbb::blocked_range<size_t> &range) {
|
|
for (size_t ti = range.begin(); ti < range.end(); ++ti)
|
|
initial[ti] = priority(int(ti));
|
|
});
|
|
for (int ti = 0; ti < int(tris.size()); ++ti)
|
|
if (initial[size_t(ti)] > 1.f)
|
|
queue.emplace(heap_key(ti, initial[size_t(ti)]), ti);
|
|
}
|
|
|
|
// Every iteration either drops one satisfied triangle from the queue or performs exactly one
|
|
// bisection, and bisections are capped by the triangle budget, so this always terminates.
|
|
// Progress is reported against the triangle budget, which is what the loop is bounded by. Polled
|
|
// rather than pushed on every bisection: a refinement spends its budget in hundreds of thousands
|
|
// of them, and every hook call wakes the UI's idle loop to repaint the notification.
|
|
const int start_tris = int(tris.size());
|
|
const int budget_tris = std::max(1, max_triangles - start_tris);
|
|
int next_poll = start_tris;
|
|
while (!queue.empty() && int(tris.size()) + 2 <= max_triangles) {
|
|
if (progress && int(tris.size()) >= next_poll) {
|
|
next_poll = int(tris.size()) + std::max(1024, budget_tris / 100);
|
|
if (!progress(std::clamp((int(tris.size()) - start_tris) * 100 / budget_tris, 0, 100)))
|
|
break; // still conformal - whole bisections only; the caller decides whether to keep it
|
|
}
|
|
const int ti = queue.top().second;
|
|
queue.pop();
|
|
if (priority(ti) <= 1.f)
|
|
continue; // stale: already refined past its criteria
|
|
|
|
// Longest-Edge Propagation Path: step to the neighbour across the current longest edge for as
|
|
// long as that neighbour has a strictly longer one, then bisect the terminal edge we land on.
|
|
// Length strictly increases along the path, so it cannot cycle.
|
|
int cur = ti, split_edge = -1;
|
|
for (size_t guard = 0; guard <= tris.size(); ++guard) {
|
|
const int le = longest_local(cur);
|
|
const int nbr = tris[cur].nb[le];
|
|
if (nbr == NB_NONMANIFOLD)
|
|
break; // cannot split across it without tearing the mesh: give up on this path
|
|
if (nbr == NB_BOUNDARY) {
|
|
split_edge = le; // boundary longest edge -> terminal
|
|
break;
|
|
}
|
|
const int nle = longest_local(nbr);
|
|
if (edge_key(tris[nbr].v[nle], tris[nbr].v[(nle + 1) % 3]) ==
|
|
edge_key(tris[cur].v[le], tris[cur].v[(le + 1) % 3])) {
|
|
split_edge = le; // mutual longest edge -> terminal
|
|
break;
|
|
}
|
|
cur = nbr;
|
|
}
|
|
if (split_edge < 0 || !bisect(cur, split_edge))
|
|
continue; // ti sits in a non-manifold neighbourhood and cannot be refined safely
|
|
|
|
for (const int t : touched)
|
|
if (const float p = priority(t); p > 1.f)
|
|
queue.emplace(heap_key(t, p), t);
|
|
// The bisection may have been a step on the way to ti rather than ti itself, in which case ti
|
|
// is not in `touched` and has to go back on the heap to be walked again.
|
|
if (cur != ti)
|
|
if (const float p = priority(ti); p > 1.f)
|
|
queue.emplace(heap_key(ti, p), ti);
|
|
}
|
|
|
|
return emit();
|
|
}
|
|
|
|
namespace {
|
|
|
|
// Ear clipping of a simple polygon given counter-clockwise in 2D; `pts` are indexed by `poly`. Small
|
|
// polygons only (a triangle clipped by a few contour polylines), so the plain O(n^2) form is enough.
|
|
// A vertex that lies on a candidate ear's boundary does not block it, and when no ear can be found
|
|
// (numerically degenerate input) the flattest vertex is dropped rather than fanning blindly, which
|
|
// on a concave region would put triangles outside it; the zero-area triangle keeps the edges paired.
|
|
void ear_clip(const std::vector<Vec2f> &pts, std::vector<int> poly, const std::function<void(int, int, int)> &emit)
|
|
{
|
|
auto cross2 = [](const Vec2f &o, const Vec2f &a, const Vec2f &b) {
|
|
return (a.x() - o.x()) * (b.y() - o.y()) - (a.y() - o.y()) * (b.x() - o.x());
|
|
};
|
|
// A corner flatter than this is collinear: several vertices along one edge of the triangle. Such
|
|
// a corner is never an ear - the zero-area triangle would lie on the edge, where the neighbour,
|
|
// seeing the same points from its side, may emit the same one.
|
|
float flat = 0.f;
|
|
{
|
|
Vec2f lo = pts[size_t(poly[0])], hi = lo;
|
|
for (int id : poly) { lo = lo.cwiseMin(pts[size_t(id)]); hi = hi.cwiseMax(pts[size_t(id)]); }
|
|
flat = 1e-5f * (hi - lo).squaredNorm();
|
|
}
|
|
while (poly.size() > 3) {
|
|
const size_t n = poly.size();
|
|
bool found = false;
|
|
for (size_t i = 0; i < n && !found; ++i) {
|
|
const int ia = poly[(i + n - 1) % n], ib = poly[i], ic = poly[(i + 1) % n];
|
|
const Vec2f &a = pts[size_t(ia)], &b = pts[size_t(ib)], &c = pts[size_t(ic)];
|
|
const float area2 = cross2(a, b, c);
|
|
if (area2 <= flat)
|
|
continue; // reflex or flat corner
|
|
const float eps = 1e-6f * area2;
|
|
bool clean = true;
|
|
size_t on_base = 0;
|
|
for (size_t j = 0; j < n && clean; ++j) {
|
|
const int ij = poly[j];
|
|
if (ij == ia || ij == ib || ij == ic)
|
|
continue;
|
|
const Vec2f &p = pts[size_t(ij)];
|
|
clean = !(cross2(a, b, p) > eps && cross2(b, c, p) > eps && cross2(c, a, p) > eps);
|
|
// On the ear's base, between its ends: a run of vertices along the base, as when
|
|
// several sit on one edge of the triangle.
|
|
if (std::abs(cross2(c, a, p)) <= flat && (p - c).dot(a - c) > 0.f && (p - a).dot(c - a) > 0.f)
|
|
++on_base;
|
|
}
|
|
if (!clean)
|
|
continue;
|
|
if (on_base > 0) {
|
|
// The base carries other vertices. When those are all that is left of the polygon,
|
|
// in order from c round to a, fan them from b and finish; otherwise the ear is not
|
|
// clear of the rest of the polygon.
|
|
if (on_base + 3 != n)
|
|
continue;
|
|
for (size_t j = 0; j + 1 < n - 1; ++j) {
|
|
const int u = poly[(i + 1 + j) % n], v = poly[(i + 2 + j) % n];
|
|
emit(ib, u, v);
|
|
}
|
|
poly.clear();
|
|
return;
|
|
}
|
|
emit(ia, ib, ic);
|
|
poly.erase(poly.begin() + long(i));
|
|
found = true;
|
|
}
|
|
if (!found) {
|
|
// Clip the smallest convex corner anyway, or failing that the flattest: a thin triangle
|
|
// keeps every edge accounted for, and a convex one at least stays on the polygon's side.
|
|
size_t pick = 0;
|
|
float best = std::numeric_limits<float>::max();
|
|
bool convex_found = false;
|
|
for (size_t i = 0; i < n; ++i) {
|
|
const Vec2f &a = pts[size_t(poly[(i + n - 1) % n])], &b = pts[size_t(poly[i])], &c = pts[size_t(poly[(i + 1) % n])];
|
|
const float m = cross2(a, b, c);
|
|
if (m > flat && (!convex_found || m < best)) { best = m; pick = i; convex_found = true; }
|
|
else if (!convex_found && std::abs(m) < best) { best = std::abs(m); pick = i; }
|
|
}
|
|
emit(poly[(pick + n - 1) % n], poly[pick], poly[(pick + 1) % n]);
|
|
poly.erase(poly.begin() + long(pick));
|
|
}
|
|
}
|
|
if (poly.size() == 3)
|
|
emit(poly[0], poly[1], poly[2]);
|
|
}
|
|
|
|
// Douglas-Peucker on an open polyline; endpoints always survive.
|
|
void simplify_polyline(const std::vector<Vec2f> &in, float tol, std::vector<Vec2f> &out)
|
|
{
|
|
out.clear();
|
|
if (in.size() < 3) { out = in; return; }
|
|
std::vector<uint8_t> keep(in.size(), 0);
|
|
keep.front() = keep.back() = 1;
|
|
std::vector<std::pair<size_t, size_t>> stack = { { 0, in.size() - 1 } };
|
|
while (!stack.empty()) {
|
|
const auto [i0, i1] = stack.back();
|
|
stack.pop_back();
|
|
if (i1 <= i0 + 1)
|
|
continue;
|
|
const Vec2f d = in[i1] - in[i0];
|
|
const float len = d.norm();
|
|
float best = -1.f;
|
|
size_t bi = i0;
|
|
for (size_t i = i0 + 1; i < i1; ++i) {
|
|
const Vec2f r = in[i] - in[i0];
|
|
const float dist = (len > 1e-12f) ? std::abs(d.x() * r.y() - d.y() * r.x()) / len : r.norm();
|
|
if (dist > best) { best = dist; bi = i; }
|
|
}
|
|
if (best > tol) {
|
|
keep[bi] = 1;
|
|
stack.push_back({ i0, bi });
|
|
stack.push_back({ bi, i1 });
|
|
}
|
|
}
|
|
for (size_t i = 0; i < in.size(); ++i)
|
|
if (keep[i]) out.push_back(in[i]);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
namespace {
|
|
|
|
// The cutter's view of one crossing of the mid-level contour with a mesh edge.
|
|
struct Crossing
|
|
{
|
|
Vec3f p, n;
|
|
float t; // parameter from the edge's lower vertex id toward the higher
|
|
int a = -1, b = -1; // the edge, lower vertex id first
|
|
float sin_min = 1.f; // sine of the smallest angle a contour makes with the edge
|
|
float dt_max = std::numeric_limits<float>::max(); // room along the edge before a copy could
|
|
// pass the copies of the contour's next vertex
|
|
bool used = false; // some cut triangle's contour ends here
|
|
bool no_seam = false; // a contour joins it straight to its neighbour on the edge: nothing between
|
|
bool sharp = false;
|
|
float thick = 0.f; // narrower side of the feature at the crossing, along the gradient
|
|
int single = -1, lo = -1, hi = -1;
|
|
int near_a = -1, near_b = -1; // the copy on a's side and on b's, when doubled
|
|
};
|
|
struct Edge
|
|
{
|
|
std::vector<Crossing> xs; // sorted by t
|
|
};
|
|
|
|
inline uint64_t step_edge_key(int a, int b)
|
|
{
|
|
if (a > b) std::swap(a, b);
|
|
return (uint64_t(uint32_t(a)) << 32) | uint32_t(b);
|
|
}
|
|
|
|
// What cut_mesh_at_steps() learns about the mesh before it cuts anything: the vertex heights (and the
|
|
// nudged vertex positions), the crossings on every painted edge, and the verdict `ok` - whether this
|
|
// is a step texture worth cutting at all. The verdict is what the prepare path probes on the coarse
|
|
// mesh before it chooses how to refine, so refinement and cut agree on which textures are stepped.
|
|
struct StepScan
|
|
{
|
|
bool ok = false;
|
|
std::vector<Vec3f> pos, vnormal;
|
|
std::vector<float> vh;
|
|
float iso = 0.f, range = 0.f;
|
|
std::unordered_map<uint64_t, Edge> edges;
|
|
|
|
std::pair<Vec3f, Vec3f> sample_between(int a, int b, float t) const
|
|
{
|
|
const Vec3f p = pos[size_t(a)] + (pos[size_t(b)] - pos[size_t(a)]) * t;
|
|
Vec3f n = vnormal[size_t(a)] + (vnormal[size_t(b)] - vnormal[size_t(a)]) * t;
|
|
const float l = n.norm();
|
|
n = (l > 1e-12f) ? Vec3f(n / l) : vnormal[size_t(a)];
|
|
return std::make_pair(p, n);
|
|
}
|
|
bool side_of(float h) const { return h > iso; }
|
|
};
|
|
|
|
StepScan scan_steps(const indexed_triangle_set &mesh, const std::vector<uint8_t> ®ion,
|
|
const HeightFieldSampler &sampler, float step_width_mm, float seam_gap_mm,
|
|
float min_feature_mm, bool nudge)
|
|
{
|
|
StepScan scan;
|
|
const size_t nv = mesh.vertices.size(), nt = mesh.indices.size();
|
|
if (!sampler || region.size() != nt || step_width_mm <= 0.f)
|
|
return scan;
|
|
|
|
// 1. Heights at the vertices of the cuttable triangles, along the same normals the bake will use.
|
|
// Vertex positions are worked on in `pos`: a vertex that happens to sit within a texel of a step
|
|
// is nudged off it below, and everything from the edge march to the output uses the nudged place.
|
|
std::vector<Vec3f> &pos = scan.pos;
|
|
std::vector<Vec3f> &vnormal = scan.vnormal;
|
|
pos = mesh.vertices;
|
|
vnormal = texture_displacement_vertex_normals(mesh);
|
|
std::vector<uint8_t> wanted(nv, 0);
|
|
for (size_t t = 0; t < nt; ++t)
|
|
if (region[t] != 0)
|
|
for (int k = 0; k < 3; ++k)
|
|
wanted[size_t(mesh.indices[t][k])] = 1;
|
|
std::vector<float> &vh = scan.vh;
|
|
vh.assign(nv, 0.f);
|
|
tbb::parallel_for(tbb::blocked_range<size_t>(0, nv), [&](const tbb::blocked_range<size_t> &r) {
|
|
for (size_t v = r.begin(); v < r.end(); ++v)
|
|
if (wanted[v])
|
|
vh[v] = sampler(pos[v], vnormal[v]);
|
|
});
|
|
float hmin = std::numeric_limits<float>::max(), hmax = -hmin;
|
|
for (size_t v = 0; v < nv; ++v)
|
|
if (wanted[v]) { hmin = std::min(hmin, vh[v]); hmax = std::max(hmax, vh[v]); }
|
|
const float range = scan.range = hmax - hmin;
|
|
if (!(range > 1e-6f))
|
|
return scan;
|
|
const float iso = scan.iso = 0.5f * (hmin + hmax);
|
|
// Only a texture that is mostly steps is cut: one whose heights sit at two levels with little in
|
|
// between (a grid, a knurl, a logo). On a smooth or noisy relief the mid-level contour runs through
|
|
// every triangle without being a step anywhere, and cutting along it would only multiply the
|
|
// triangles; the chord-based refinement is the right tool there. Judged from the vertex heights,
|
|
// which sample the whole painted surface.
|
|
{
|
|
size_t near_level = 0, total = 0;
|
|
for (size_t v = 0; v < nv; ++v)
|
|
if (wanted[v]) { ++total; near_level += std::abs(vh[v] - iso) > 0.35f * range; }
|
|
if (total == 0 || double(near_level) < 0.6 * double(total))
|
|
return scan;
|
|
}
|
|
|
|
// 1b. A vertex inside the blend of a sharp step - the texture's bilinear ramp is a texel wide - would
|
|
// be displaced to a height between the two sides, and every triangle at it would ramp. Such a vertex
|
|
// is nudged along the surface away from the step, down its own side's slope, until it samples a
|
|
// pure value; a step's width or so. Only where the surface is flat around it: a vertex on a crease
|
|
// of the model (a box edge) stays where it is. Skipped by a probe, which only wants the verdict.
|
|
if (nudge) {
|
|
std::vector<uint8_t> creased(nv, 0);
|
|
std::vector<Vec3f> first_normal(nv, Vec3f::Zero());
|
|
std::vector<float> shortest(nv, std::numeric_limits<float>::max()); // shortest edge at the vertex
|
|
for (size_t t = 0; t < nt; ++t) {
|
|
const auto &tri = mesh.indices[t];
|
|
const Vec3f &a = pos[size_t(tri[0])];
|
|
Vec3f fn = (pos[size_t(tri[1])] - a).cross(pos[size_t(tri[2])] - a);
|
|
const float fl = fn.norm();
|
|
if (fl < 1e-12f) continue;
|
|
fn /= fl;
|
|
for (int k = 0; k < 3; ++k) {
|
|
Vec3f &f0 = first_normal[size_t(tri[k])];
|
|
if (f0.isZero()) f0 = fn;
|
|
else if (f0.dot(fn) < 0.985f) creased[size_t(tri[k])] = 1; // ~10 degrees
|
|
const float el = (pos[size_t(tri[(k + 1) % 3])] - pos[size_t(tri[k])]).norm();
|
|
shortest[size_t(tri[k])] = std::min(shortest[size_t(tri[k])], el);
|
|
shortest[size_t(tri[(k + 1) % 3])] = std::min(shortest[size_t(tri[(k + 1) % 3])], el);
|
|
}
|
|
}
|
|
tbb::parallel_for(tbb::blocked_range<size_t>(0, nv), [&](const tbb::blocked_range<size_t> &r) {
|
|
for (size_t v = r.begin(); v < r.end(); ++v) {
|
|
if (!wanted[v] || creased[v] || std::abs(vh[v] - iso) > 0.47f * range)
|
|
continue;
|
|
const Vec3f &n = vnormal[v];
|
|
const Vec3f ax = (std::abs(n.x()) < 0.9f) ? Vec3f::UnitX() : Vec3f::UnitY();
|
|
const Vec3f t1 = n.cross(ax).normalized(), t2 = n.cross(t1).normalized();
|
|
const float d = step_width_mm;
|
|
const float gx = sampler(pos[v] + t1 * d, n) - sampler(pos[v] - t1 * d, n);
|
|
const float gy = sampler(pos[v] + t2 * d, n) - sampler(pos[v] - t2 * d, n);
|
|
Vec3f g = t1 * gx + t2 * gy;
|
|
const float gl = g.norm();
|
|
if (gl < 0.25f * range)
|
|
continue; // not at a step: a slope, which the chord test handles
|
|
g /= gl;
|
|
// Down the slope for a low vertex, up it for a high one, in steps of half a texel, and
|
|
// never by more than a fraction of the shortest edge at the vertex: on a finely refined
|
|
// mesh a texel-sized move would fold the triangles around it.
|
|
const Vec3f dir = (vh[v] > iso) ? g : Vec3f(-g);
|
|
Vec3f best = pos[v];
|
|
float bh = vh[v];
|
|
const float limit = std::min(2.f * step_width_mm, 0.3f * shortest[v]);
|
|
for (int i = 1; i <= 8; ++i) {
|
|
const float dist = 0.25f * step_width_mm * float(i);
|
|
if (dist > limit)
|
|
break;
|
|
const Vec3f q = pos[v] + dir * dist;
|
|
const float h = sampler(q, n);
|
|
if ((h > iso) != (vh[v] > iso))
|
|
break; // crossed the step: the nudge would change the vertex's side
|
|
best = q; bh = h;
|
|
if (std::abs(h - iso) > 0.48f * range)
|
|
break;
|
|
}
|
|
if (std::abs(bh - iso) > std::abs(vh[v] - iso)) { pos[v] = best; vh[v] = bh; }
|
|
}
|
|
});
|
|
}
|
|
|
|
const auto sample_between = [&scan](int a, int b, float t) { return scan.sample_between(a, b, t); };
|
|
const auto side_of = [&scan](float h) { return scan.side_of(h); };
|
|
|
|
// 2. Crossings on the edges, found on the field itself - on a 1 mm edge over a texture with 30 um
|
|
// texels, interpolating the endpoint heights could put a step anywhere along the edge. Both
|
|
// triangles on an edge see the same crossings, which is what keeps the cut conformal.
|
|
auto &edges = scan.edges;
|
|
edges.reserve(nt);
|
|
|
|
const auto find_crossings = [&](int a, int b, Edge &edge) {
|
|
if (a > b) std::swap(a, b);
|
|
const float len = (pos[size_t(b)] - pos[size_t(a)]).norm();
|
|
const int steps = std::clamp(int(std::ceil(len / (0.5f * step_width_mm))), 4, 128);
|
|
float t0 = 0.f, h0 = vh[size_t(a)] - iso;
|
|
for (int i = 1; i <= steps; ++i) {
|
|
const float ti = float(i) / float(steps);
|
|
const auto [pp, nn] = sample_between(a, b, ti);
|
|
const float hi_ = sampler(pp, nn) - iso;
|
|
if (h0 * hi_ < 0.f || (hi_ == 0.f && h0 != 0.f)) {
|
|
float lo_t = t0, hi_t = ti, lo_h = h0;
|
|
for (int k = 0; k < 10; ++k) {
|
|
const float tm = 0.5f * (lo_t + hi_t);
|
|
const auto [pm, nm] = sample_between(a, b, tm);
|
|
if (lo_h * (sampler(pm, nm) - iso) <= 0.f) hi_t = tm; else { lo_t = tm; }
|
|
}
|
|
Crossing c;
|
|
c.a = a; c.b = b;
|
|
// Never exactly on a vertex: that would make a zero-area triangle of the split.
|
|
c.t = std::clamp(0.5f * (lo_t + hi_t), 1e-3f, 1.f - 1e-3f);
|
|
std::tie(c.p, c.n) = sample_between(a, b, c.t);
|
|
// Sharp means the field changes by at least half its range across `step_width_mm` in
|
|
// the steepest tangent direction - a step, not a slope that merely passes mid-level.
|
|
const Vec3f ax = (std::abs(c.n.x()) < 0.9f) ? Vec3f::UnitX() : Vec3f::UnitY();
|
|
const Vec3f t1v = c.n.cross(ax).normalized(), t2v = c.n.cross(t1v).normalized();
|
|
const float d = 0.5f * step_width_mm;
|
|
const float gx = sampler(c.p + t1v * d, c.n) - sampler(c.p - t1v * d, c.n);
|
|
const float gy = sampler(c.p + t2v * d, c.n) - sampler(c.p - t2v * d, c.n);
|
|
const float gl = std::sqrt(gx * gx + gy * gy);
|
|
c.sharp = gl >= 0.5f * range;
|
|
// How wide the feature is on either side of the step, along the gradient: the
|
|
// distance until the field returns to mid-level, in steps of half the step width, up
|
|
// to `thick_max`. A texture whose features are only a few texels wide has no pure
|
|
// interior for a seam copy to land in and is left to refinement (see the gate below).
|
|
{
|
|
const float thick_max = 4.f * step_width_mm;
|
|
c.thick = thick_max;
|
|
if (gl > 1e-12f) {
|
|
const Vec3f g = (t1v * gx + t2v * gy) / gl;
|
|
for (int sgn = -1; sgn <= 1; sgn += 2) {
|
|
const bool high = sgn > 0; // uphill along the gradient
|
|
for (int i = 1; i <= 8; ++i) {
|
|
const float dist = 0.5f * step_width_mm * float(i);
|
|
if (dist >= c.thick)
|
|
break;
|
|
if (side_of(sampler(c.p + g * (float(sgn) * dist), c.n)) != high) {
|
|
c.thick = std::min(c.thick, dist);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
edge.xs.push_back(c);
|
|
}
|
|
t0 = ti; h0 = hi_;
|
|
}
|
|
// A pair of crossings closer than the smallest feature the mesh is meant to carry is a feature
|
|
// too thin to print, and one closer than the seam gap has no room for two seams: dropping both
|
|
// leaves the surface flat there, at the side around it.
|
|
const float thinnest = std::max(min_feature_mm, seam_gap_mm);
|
|
for (size_t i = 0; i + 1 < edge.xs.size();) {
|
|
if ((edge.xs[i + 1].t - edge.xs[i].t) * len < thinnest)
|
|
edge.xs.erase(edge.xs.begin() + long(i), edge.xs.begin() + long(i) + 2);
|
|
else
|
|
++i;
|
|
}
|
|
// Parity: the endpoints' sides say whether the count must be odd or even; the march starts and
|
|
// ends on the vertex heights themselves, so a mismatch is a numerical accident. Drop the lot
|
|
// then - a triangle split on a wrong count is worse than a ramp there.
|
|
const bool odd_expected = side_of(vh[size_t(a)]) != side_of(vh[size_t(b)]);
|
|
if ((edge.xs.size() % 2 == 1) != odd_expected)
|
|
edge.xs.clear();
|
|
};
|
|
|
|
// Edges to examine, in a fixed order so the crossings can be found in parallel.
|
|
std::vector<uint64_t> edge_keys;
|
|
for (size_t t = 0; t < nt; ++t) {
|
|
if (region[t] == 0)
|
|
continue;
|
|
const auto &tri = mesh.indices[t];
|
|
for (int e = 0; e < 3; ++e)
|
|
edge_keys.push_back(step_edge_key(tri[e], tri[(e + 1) % 3]));
|
|
}
|
|
std::sort(edge_keys.begin(), edge_keys.end());
|
|
edge_keys.erase(std::unique(edge_keys.begin(), edge_keys.end()), edge_keys.end());
|
|
std::vector<Edge> found(edge_keys.size());
|
|
tbb::parallel_for(tbb::blocked_range<size_t>(0, edge_keys.size()), [&](const tbb::blocked_range<size_t> &r) {
|
|
for (size_t i = r.begin(); i < r.end(); ++i)
|
|
find_crossings(int(edge_keys[i] >> 32), int(uint32_t(edge_keys[i])), found[i]);
|
|
});
|
|
size_t crossings = 0, sharp = 0;
|
|
double thick_sum = 0.;
|
|
for (size_t i = 0; i < edge_keys.size(); ++i) {
|
|
for (const Crossing &c : found[i].xs) { ++crossings; sharp += c.sharp; thick_sum += c.thick; }
|
|
edges.emplace(edge_keys[i], std::move(found[i]));
|
|
}
|
|
// The mid-level contour has to be a step nearly everywhere it is crossed, or this is not a step
|
|
// texture: a noisy relief that happens to sit at two levels is crossed all over, mostly gently.
|
|
if (crossings == 0 || double(sharp) < 0.8 * double(crossings))
|
|
return scan;
|
|
// And the features have to be wider than the step itself. A texture whose blobs are only a few
|
|
// texels across (noise, a fine grain) is bimodal and sharp at every crossing, yet has no pure
|
|
// interior for the seam copies to land in, and its contour runs through every triangle: cutting it
|
|
// doubles the triangle count for walls the same size as the interpolation blur. Judged from the
|
|
// mean feature thickness at the crossings, measured in step widths (2.25 = four to five texels).
|
|
if (thick_sum < 2.25 * double(step_width_mm) * double(crossings))
|
|
return scan;
|
|
|
|
scan.ok = true;
|
|
return scan;
|
|
|
|
}
|
|
|
|
} // namespace
|
|
|
|
bool texture_has_steps_to_cut(const indexed_triangle_set &mesh, const std::vector<uint8_t> ®ion,
|
|
const HeightFieldSampler &sampler, float step_width_mm, float seam_gap_mm,
|
|
float min_feature_mm)
|
|
{
|
|
return scan_steps(mesh, region, sampler, step_width_mm, seam_gap_mm, min_feature_mm, /*nudge*/ false).ok;
|
|
}
|
|
|
|
indexed_triangle_set cut_mesh_at_steps(const indexed_triangle_set &mesh, const std::vector<uint8_t> ®ion,
|
|
const HeightFieldSampler &sampler, float step_width_mm,
|
|
float seam_gap_mm, float min_feature_mm, std::vector<int> *out_source,
|
|
size_t *out_cut_count)
|
|
{
|
|
const size_t nv = mesh.vertices.size(), nt = mesh.indices.size();
|
|
if (out_cut_count)
|
|
*out_cut_count = 0;
|
|
const auto passthrough = [&]() {
|
|
if (out_source) {
|
|
out_source->resize(nt);
|
|
std::iota(out_source->begin(), out_source->end(), 0);
|
|
}
|
|
return mesh;
|
|
};
|
|
if (!sampler || region.size() != nt || step_width_mm <= 0.f)
|
|
return passthrough();
|
|
|
|
StepScan scan = scan_steps(mesh, region, sampler, step_width_mm, seam_gap_mm, min_feature_mm, /*nudge*/ true);
|
|
if (!scan.ok)
|
|
return passthrough();
|
|
std::vector<Vec3f> &pos = scan.pos;
|
|
const std::vector<Vec3f> &vnormal = scan.vnormal;
|
|
std::vector<float> &vh = scan.vh;
|
|
const float iso = scan.iso, range = scan.range;
|
|
auto &edges = scan.edges;
|
|
const auto sample_between = [&scan](int a, int b, float t) { return scan.sample_between(a, b, t); };
|
|
const auto side_of = [&scan](float h) { return scan.side_of(h); };
|
|
const auto edge_key = [](int a, int b) { return step_edge_key(a, b); };
|
|
(void) vnormal; (void) range; (void) iso;
|
|
|
|
// 3. Per triangle: the perimeter as a loop of corners and crossings, and the contour inside the
|
|
// triangle traced from a local raster of the field (marching squares at half the step width), as
|
|
// polylines from one crossing to another. The trace is what makes a corner of the pattern come out
|
|
// as a corner instead of a chord clipping it, and what pairs the crossings up - no guessing. A
|
|
// polyline is kept only when its two ends land on distinct crossings the edge march found;
|
|
// otherwise the triangle is split at its crossings without a seam (the neighbours' cuts still meet
|
|
// no T-junction) and the surface ramps there. Unpainted neighbours of a crossed edge are split the
|
|
// same way.
|
|
struct Chain
|
|
{
|
|
int i = -1, j = -1; // crossing indices in the loop, from i to j
|
|
std::vector<Vec3f> pts; // interior vertices, from i toward j
|
|
std::vector<int> lo, hi; // output copies of the interior vertices (same when single)
|
|
float clearance = std::numeric_limits<float>::max(); // distance to the nearest other contour
|
|
};
|
|
struct Loop
|
|
{
|
|
std::vector<int> ids; // perimeter entries: corner vertex ids (>= 0) or -(1 + crossing index)
|
|
std::vector<Crossing *> xs; // crossings in perimeter order
|
|
std::vector<int> xpos; // their positions in ids
|
|
std::vector<Chain> chains;
|
|
std::vector<int> partner; // per crossing: the crossing its chain leads to
|
|
std::vector<int> chain_of;
|
|
std::vector<int> no_seam; // crossings joined to their neighbour on the edge with nothing between
|
|
Vec3f N = Vec3f::Zero();
|
|
bool ok = false;
|
|
};
|
|
const auto walk_edge = [&](int a, int b, std::vector<Crossing *> &out) {
|
|
auto it = edges.find(edge_key(a, b));
|
|
if (it == edges.end()) return;
|
|
Edge &edge = it->second;
|
|
if (a < b) for (auto &c : edge.xs) out.push_back(&c);
|
|
else for (auto ci = edge.xs.rbegin(); ci != edge.xs.rend(); ++ci) out.push_back(&*ci);
|
|
};
|
|
std::vector<Loop> loops(nt);
|
|
for (size_t t = 0; t < nt; ++t) {
|
|
const auto &tri = mesh.indices[t];
|
|
Loop &L = loops[t];
|
|
for (int e = 0; e < 3; ++e) {
|
|
L.ids.push_back(tri[e]);
|
|
std::vector<Crossing *> on_edge;
|
|
walk_edge(tri[e], tri[(e + 1) % 3], on_edge);
|
|
for (Crossing *c : on_edge) {
|
|
L.xpos.push_back(int(L.ids.size()));
|
|
L.ids.push_back(-(1 + int(L.xs.size())));
|
|
L.xs.push_back(c);
|
|
}
|
|
}
|
|
const Vec3f &a0 = pos[size_t(tri[0])];
|
|
L.N = (pos[size_t(tri[1])] - a0).cross(pos[size_t(tri[2])] - a0);
|
|
const float nl = L.N.norm();
|
|
if (nl > 1e-12f) L.N /= nl;
|
|
}
|
|
|
|
const float cell = 0.5f * step_width_mm;
|
|
const auto trace = [&](size_t t) {
|
|
Loop &L = loops[t];
|
|
const int n = int(L.xs.size());
|
|
const auto &tri = mesh.indices[t];
|
|
if (n == 0 || region[t] == 0)
|
|
return;
|
|
if (n % 2 != 0 || L.N.squaredNorm() < 0.5f)
|
|
return;
|
|
// Local frame on the triangle's plane; the loop runs counter-clockwise in it.
|
|
const Vec3f &A = pos[size_t(tri[0])], &B = pos[size_t(tri[1])], &C = pos[size_t(tri[2])];
|
|
const Vec3f U = (B - A).normalized(), V = L.N.cross(U);
|
|
const auto to2 = [&](const Vec3f &p) { return Vec2f((p - A).dot(U), (p - A).dot(V)); };
|
|
const auto to3 = [&](const Vec2f &q) { return Vec3f(A + U * q.x() + V * q.y()); };
|
|
const Vec2f a2 = to2(A), b2 = to2(B), c2 = to2(C);
|
|
const float det = (b2.x() - a2.x()) * (c2.y() - a2.y()) - (c2.x() - a2.x()) * (b2.y() - a2.y());
|
|
if (std::abs(det) < 1e-12f)
|
|
return;
|
|
|
|
// A barycentric lattice over the triangle, dense enough that its boundary rows are at least as
|
|
// fine as the edge march. Its boundary points take their side from the march - the lower
|
|
// corner's side, flipped at every crossing passed - so the contour traced through the lattice
|
|
// ends exactly at the crossings the neighbours share; no clipping, no matching. A conflict
|
|
// between that and the far corner's own side (an edge whose crossings the march dropped for
|
|
// parity) leaves the triangle uncut.
|
|
const float longest = std::sqrt(std::max({ (b2 - a2).squaredNorm(), (c2 - b2).squaredNorm(), (a2 - c2).squaredNorm() }));
|
|
const int N = std::clamp(int(std::ceil(longest / cell)), 4, 96);
|
|
const float h = longest / float(N);
|
|
const int W = N + 1;
|
|
// Lattice point (j, k): A + (B - A) j/N + (C - A) k/N, j + k <= N. Edge 0 (A->B) is k == 0,
|
|
// edge 1 (B->C) is j + k == N, edge 2 (C->A) is j == 0.
|
|
const auto at2 = [&](int j, int k) { return Vec2f(a2 + (b2 - a2) * (float(j) / float(N)) + (c2 - a2) * (float(k) / float(N))); };
|
|
std::vector<float> f(size_t(W) * size_t(W), 0.f);
|
|
std::vector<int8_t> sign(size_t(W) * size_t(W), 0);
|
|
const auto id = [&](int j, int k) { return size_t(k) * size_t(W) + size_t(j); };
|
|
for (int k = 0; k <= N; ++k)
|
|
for (int j = 0; j + k <= N; ++j) {
|
|
const float wb = float(j) / float(N), wc = float(k) / float(N), wa = 1.f - wb - wc;
|
|
Vec3f nn = wa * vnormal[size_t(tri[0])] + wb * vnormal[size_t(tri[1])] + wc * vnormal[size_t(tri[2])];
|
|
const float nl = nn.norm();
|
|
nn = (nl > 1e-12f) ? Vec3f(nn / nl) : L.N;
|
|
const float v = sampler(A + (B - A) * wb + (C - A) * wc, nn) - iso;
|
|
f[id(j, k)] = v;
|
|
sign[id(j, k)] = v > 0.f ? 1 : -1;
|
|
}
|
|
// Boundary sides from the march. Each edge walks from its first corner in loop order; the
|
|
// crossings on it are in that order in L.xs. Position along edge e of lattice point m/N.
|
|
const auto edge_point = [&](int e, int m) -> std::pair<int, int> { // (j, k)
|
|
return e == 0 ? std::make_pair(m, 0) : e == 1 ? std::make_pair(N - m, m) : std::make_pair(0, N - m);
|
|
};
|
|
// The crossings of edge e, with their parameter from the edge's first corner in loop order.
|
|
std::vector<std::vector<std::pair<float, int>>> edge_x(3);
|
|
{
|
|
int e = -1, k = 0;
|
|
for (int idv : L.ids) {
|
|
if (idv >= 0) { ++e; continue; }
|
|
const Crossing &c = *L.xs[size_t(k)];
|
|
// c.t runs from the lower vertex id; the loop walks tri[e] -> tri[(e + 1) % 3].
|
|
const float u = (tri[e] < tri[(e + 1) % 3]) ? c.t : 1.f - c.t;
|
|
edge_x[size_t(e)].push_back({ u, k });
|
|
++k;
|
|
}
|
|
}
|
|
for (int e = 0; e < 3; ++e) {
|
|
int8_t s = side_of(vh[size_t(tri[e])]) ? 1 : -1;
|
|
size_t next = 0;
|
|
for (int m = 0; m <= N; ++m) {
|
|
const float u = float(m) / float(N);
|
|
while (next < edge_x[size_t(e)].size() && edge_x[size_t(e)][next].first < u) { s = -s; ++next; }
|
|
const auto [j, k] = edge_point(e, m);
|
|
sign[id(j, k)] = s;
|
|
// Keep the value on the march's side of zero, so interpolation toward the interior
|
|
// does not put a crossing where the march has none - and clear of zero, so a contour
|
|
// that does pass between this point and the next row is not put on the boundary.
|
|
if ((f[id(j, k)] > 0.f) != (s > 0))
|
|
f[id(j, k)] = float(s) * std::max(std::abs(f[id(j, k)]), 0.05f * range);
|
|
}
|
|
if ((s > 0) != side_of(vh[size_t(tri[(e + 1) % 3])]))
|
|
return; // parity conflict on this edge
|
|
}
|
|
|
|
// Marching triangles. A contour point lives on a lattice edge, keyed by its lower point and
|
|
// direction: 0 (j,k)-(j+1,k), 1 (j,k)-(j,k+1), 2 (j+1,k)-(j,k+1). A boundary lattice edge with
|
|
// a sign change carries exactly the march crossing in its interval, and is that crossing.
|
|
std::vector<Vec2f> pts;
|
|
std::vector<int> pt_x; // crossing index, or -1 for an interior point
|
|
std::unordered_map<uint32_t, int> point_of;
|
|
std::vector<std::array<int, 2>> link;
|
|
bool bad = false;
|
|
const auto point_on = [&](int j, int k, int dir) -> int {
|
|
const uint32_t key = (uint32_t(j) << 16) | (uint32_t(k) << 2) | uint32_t(dir);
|
|
auto it = point_of.find(key);
|
|
if (it != point_of.end())
|
|
return it->second;
|
|
// Endpoints (j0, k0) and (jb, kb).
|
|
const int j0 = dir == 2 ? j + 1 : j, k0 = k;
|
|
const int jb = dir == 0 ? j + 1 : j, kb = dir == 0 ? k : k + 1;
|
|
int xk = -1;
|
|
Vec2f p;
|
|
int e = -1;
|
|
float u0 = 0.f, u1 = 0.f;
|
|
if (k0 == 0 && kb == 0) { e = 0; u0 = float(j0) / float(N); u1 = float(jb) / float(N); }
|
|
else if (j0 + k0 == N && jb + kb == N) { e = 1; u0 = float(k0) / float(N); u1 = float(kb) / float(N); }
|
|
else if (j0 == 0 && jb == 0) { e = 2; u0 = float(N - k0) / float(N); u1 = float(N - kb) / float(N); }
|
|
if (e >= 0) {
|
|
const float lo = std::min(u0, u1), hi = std::max(u0, u1);
|
|
for (const auto &[u, k_] : edge_x[size_t(e)])
|
|
if (u >= lo && u < hi) { if (xk >= 0) bad = true; xk = k_; }
|
|
if (xk < 0) { bad = true; p = 0.5f * (at2(j0, k0) + at2(jb, kb)); }
|
|
else p = to2(L.xs[size_t(xk)]->p);
|
|
} else {
|
|
const float fa = f[id(j0, k0)], fb = f[id(jb, kb)];
|
|
const float s = (fa != fb) ? std::clamp(fa / (fa - fb), 0.f, 1.f) : 0.5f;
|
|
p = at2(j0, k0) + (at2(jb, kb) - at2(j0, k0)) * s;
|
|
}
|
|
const int pid = int(pts.size());
|
|
pts.push_back(p);
|
|
pt_x.push_back(xk);
|
|
link.push_back({ -1, -1 });
|
|
point_of.emplace(key, pid);
|
|
return pid;
|
|
};
|
|
const auto connect = [&](int p, int q) {
|
|
for (int *slot : { &link[size_t(p)][0], &link[size_t(p)][1] })
|
|
if (*slot < 0) { *slot = q; break; }
|
|
for (int *slot : { &link[size_t(q)][0], &link[size_t(q)][1] })
|
|
if (*slot < 0) { *slot = p; break; }
|
|
};
|
|
// Up triangle (j,k),(j+1,k),(j,k+1): edges dir 0 at (j,k), dir 1 at (j,k), dir 2 at (j,k).
|
|
// Down triangle (j+1,k),(j+1,k+1),(j,k+1): dir 1 at (j+1,k), dir 0 at (j,k+1), dir 2 at (j,k).
|
|
for (int k = 0; k < N; ++k)
|
|
for (int j = 0; j + k < N; ++j) {
|
|
{
|
|
const int8_t s0_ = sign[id(j, k)], s1_ = sign[id(j + 1, k)], s2_ = sign[id(j, k + 1)];
|
|
if (!(s0_ == s1_ && s1_ == s2_)) {
|
|
std::vector<int> ends;
|
|
if (s0_ != s1_) ends.push_back(point_on(j, k, 0));
|
|
if (s0_ != s2_) ends.push_back(point_on(j, k, 1));
|
|
if (s1_ != s2_) ends.push_back(point_on(j, k, 2));
|
|
if (ends.size() == 2) connect(ends[0], ends[1]);
|
|
}
|
|
}
|
|
if (j + k + 1 < N) {
|
|
const int8_t s0_ = sign[id(j + 1, k)], s1_ = sign[id(j + 1, k + 1)], s2_ = sign[id(j, k + 1)];
|
|
if (!(s0_ == s1_ && s1_ == s2_)) {
|
|
std::vector<int> ends;
|
|
if (s0_ != s1_) ends.push_back(point_on(j + 1, k, 1));
|
|
if (s1_ != s2_) ends.push_back(point_on(j, k + 1, 0));
|
|
if (s0_ != s2_) ends.push_back(point_on(j, k, 2));
|
|
if (ends.size() == 2) connect(ends[0], ends[1]);
|
|
}
|
|
}
|
|
}
|
|
if (bad)
|
|
return;
|
|
|
|
// Chains from every crossing to the crossing it leads to. Closed loops never touch the
|
|
// boundary and are left alone - refinement resolves a feature that sits entirely inside a
|
|
// triangle.
|
|
std::vector<int> matched(static_cast<size_t>(n), -1);
|
|
std::vector<Chain> chains;
|
|
std::vector<std::vector<Vec2f>> raws; // the traced polyline of each chain, crossing to crossing
|
|
std::vector<uint8_t> seen(pts.size(), 0);
|
|
for (size_t s = 0; s < pts.size(); ++s) {
|
|
if (seen[s] || pt_x[s] < 0)
|
|
continue;
|
|
std::vector<Vec2f> full;
|
|
int prev = -1, cur = int(s);
|
|
while (cur >= 0 && !seen[size_t(cur)]) {
|
|
seen[size_t(cur)] = 1;
|
|
full.push_back(pts[size_t(cur)]);
|
|
if (pt_x[size_t(cur)] >= 0 && cur != int(s))
|
|
break;
|
|
const int nx0 = link[size_t(cur)][0], nx1 = link[size_t(cur)][1];
|
|
const int next = (nx0 != prev) ? nx0 : nx1;
|
|
prev = cur; cur = next;
|
|
}
|
|
if (cur < 0 || pt_x[size_t(cur)] < 0 || cur == int(s))
|
|
return; // a contour that starts at a crossing and ends nowhere: the lattice is inconsistent
|
|
Chain ch;
|
|
ch.i = pt_x[s];
|
|
ch.j = pt_x[size_t(cur)];
|
|
if (matched[size_t(ch.i)] >= 0 || matched[size_t(ch.j)] >= 0 || ch.i == ch.j)
|
|
return;
|
|
matched[size_t(ch.i)] = ch.j;
|
|
matched[size_t(ch.j)] = ch.i;
|
|
chains.push_back(std::move(ch));
|
|
raws.push_back(std::move(full));
|
|
}
|
|
// How close each contour comes to another: two sides of a thin feature. Simplifying either by
|
|
// the usual tolerance, or offsetting its copies by the usual gap, could then push it across
|
|
// the other, so both are scaled to the clearance. Segment-to-segment over every pair.
|
|
{
|
|
const auto seg_dist = [](const Vec2f &a, const Vec2f &b, const Vec2f &c, const Vec2f &d) {
|
|
const auto pt_seg = [](const Vec2f &q, const Vec2f &u, const Vec2f &v) {
|
|
const Vec2f uv = v - u;
|
|
const float t = std::clamp((q - u).dot(uv) / std::max(uv.squaredNorm(), 1e-12f), 0.f, 1.f);
|
|
return (q - (u + uv * t)).norm();
|
|
};
|
|
return std::min({ pt_seg(a, c, d), pt_seg(b, c, d), pt_seg(c, a, b), pt_seg(d, a, b) });
|
|
};
|
|
for (size_t x = 0; x < raws.size(); ++x)
|
|
for (size_t y = x + 1; y < raws.size(); ++y) {
|
|
float best = std::min(chains[x].clearance, chains[y].clearance);
|
|
for (size_t i = 0; i + 1 < raws[x].size(); ++i)
|
|
for (size_t j = 0; j + 1 < raws[y].size(); ++j)
|
|
best = std::min(best, seg_dist(raws[x][i], raws[x][i + 1], raws[y][j], raws[y][j + 1]));
|
|
chains[x].clearance = std::min(chains[x].clearance, best);
|
|
chains[y].clearance = std::min(chains[y].clearance, best);
|
|
}
|
|
}
|
|
for (size_t c = 0; c < chains.size(); ++c) {
|
|
Chain &ch = chains[c];
|
|
const std::vector<Vec2f> &full = raws[c];
|
|
const float tol = std::min(h, 0.25f * ch.clearance);
|
|
const float gap = std::min(seam_gap_mm, 0.5f * ch.clearance);
|
|
std::vector<Vec2f> simple;
|
|
simplify_polyline(full, tol, simple);
|
|
// A vertex within the seam gap of the previous one, or of the far end, leaves its copies no
|
|
// room to clear the blend of the step, and a corner that close to an edge is within the
|
|
// seam's own width anyway: it is merged into its neighbour.
|
|
{
|
|
std::vector<Vec2f> spaced;
|
|
spaced.push_back(simple.front());
|
|
for (size_t m = 1; m + 1 < simple.size(); ++m)
|
|
if ((simple[m] - spaced.back()).norm() >= gap && (simple[m] - simple.back()).norm() >= gap)
|
|
spaced.push_back(simple[m]);
|
|
spaced.push_back(simple.back());
|
|
simple = std::move(spaced);
|
|
}
|
|
const int m_loop = int(L.ids.size());
|
|
const bool adjacent = (L.xpos[size_t(ch.j)] - L.xpos[size_t(ch.i)] + m_loop) % m_loop == 1 ||
|
|
(L.xpos[size_t(ch.i)] - L.xpos[size_t(ch.j)] + m_loop) % m_loop == 1;
|
|
if (simple.size() == 2 && full.size() > 2 && adjacent) {
|
|
// Never down to a bare chord between two crossings next to each other on one edge: the
|
|
// shallow pocket between them would be joined along the edge itself, and the regions on
|
|
// either side would overlap. The raw point farthest from the chord stays.
|
|
const Vec2f d = full.back() - full.front();
|
|
const float dl = std::max(d.norm(), 1e-9f);
|
|
size_t bi = full.size() / 2;
|
|
float bd = 0.1f * tol; // below this the contour is straight: take its middle point
|
|
for (size_t m = 1; m + 1 < full.size(); ++m) {
|
|
const Vec2f r = full[m] - full.front();
|
|
const float dist = std::abs(d.x() * r.y() - d.y() * r.x()) / dl;
|
|
if (dist > bd) { bd = dist; bi = m; }
|
|
}
|
|
simple.insert(simple.begin() + 1, full[bi]);
|
|
}
|
|
// Hairpins - the contour doubling back on itself with less than the seam gap between the
|
|
// two sides, the tip of a grain line thinner than the gap - would give a region polygon
|
|
// whose two sides cross. The spike vertex goes, and the feature is flattened there, as a
|
|
// thin feature on an edge is.
|
|
for (bool again = true; again;) {
|
|
again = false;
|
|
for (size_t m = 1; m + 1 < simple.size(); ++m) {
|
|
const Vec2f d1 = simple[m] - simple[m - 1], d2 = simple[m + 1] - simple[m];
|
|
const float l1 = d1.norm(), l2 = d2.norm();
|
|
if (l1 < 1e-9f || l2 < 1e-9f) { simple.erase(simple.begin() + long(m)); again = true; break; }
|
|
if (d1.dot(d2) / (l1 * l2) > -0.5f)
|
|
continue; // turns less than 120 degrees: a corner, not a hairpin
|
|
// Distance between the two sides: the shorter side's far end to the other side.
|
|
const Vec2f &tip = simple[m];
|
|
const auto dist_to = [&](const Vec2f &q, const Vec2f &a, const Vec2f &b) {
|
|
const Vec2f ab = b - a;
|
|
const float t = std::clamp((q - a).dot(ab) / std::max(ab.squaredNorm(), 1e-12f), 0.f, 1.f);
|
|
return (q - (a + ab * t)).norm();
|
|
};
|
|
const float between = (l1 < l2) ? dist_to(simple[m - 1], tip, simple[m + 1]) : dist_to(simple[m + 1], simple[m - 1], tip);
|
|
if (between < seam_gap_mm) {
|
|
simple.erase(simple.begin() + long(m));
|
|
again = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
for (size_t m = 1; m + 1 < simple.size(); ++m)
|
|
ch.pts.push_back(to3(simple[m]));
|
|
if (ch.pts.empty() && adjacent) {
|
|
// Two crossings next to each other on one edge and the contour, once simplified, going
|
|
// straight from one to the other: an empty pocket. There is nothing to wall off - a wall
|
|
// along the edge would be a zero-area strip, and the neighbour would lay its own on top
|
|
// - so neither crossing gets a seam. Recorded here and applied once the tracing is done.
|
|
L.no_seam.push_back(ch.i);
|
|
L.no_seam.push_back(ch.j);
|
|
}
|
|
}
|
|
for (int k = 0; k < n; ++k)
|
|
if (matched[size_t(k)] < 0)
|
|
return;
|
|
L.chains = std::move(chains);
|
|
L.partner = std::move(matched);
|
|
L.chain_of.assign(static_cast<size_t>(n), -1);
|
|
for (size_t c = 0; c < L.chains.size(); ++c) {
|
|
L.chain_of[size_t(L.chains[c].i)] = int(c);
|
|
L.chain_of[size_t(L.chains[c].j)] = int(c);
|
|
}
|
|
L.ok = true;
|
|
};
|
|
tbb::parallel_for(tbb::blocked_range<size_t>(0, nt), [&](const tbb::blocked_range<size_t> &r) {
|
|
for (size_t t = r.begin(); t < r.end(); ++t)
|
|
trace(t);
|
|
});
|
|
size_t cut_count = 0;
|
|
for (size_t t = 0; t < nt; ++t) {
|
|
cut_count += loops[t].ok ? 1 : 0;
|
|
if (loops[t].ok)
|
|
for (int k : loops[t].no_seam)
|
|
loops[t].xs[size_t(k)]->no_seam = true;
|
|
}
|
|
|
|
// 4. Which crossings a contour reaches, and how obliquely: the copies of a doubled crossing are moved
|
|
// apart along the edge, so the shallower the contour meets the edge the further apart they need to
|
|
// be to sit the same distance clear of it.
|
|
for (size_t t = 0; t < nt; ++t) {
|
|
Loop &L = loops[t];
|
|
if (!L.ok)
|
|
continue;
|
|
for (auto &ch : L.chains) {
|
|
Crossing &p = *L.xs[size_t(ch.i)], &q = *L.xs[size_t(ch.j)];
|
|
p.used = q.used = true;
|
|
const Vec3f p_next = ch.pts.empty() ? q.p : ch.pts.front();
|
|
const Vec3f q_prev = ch.pts.empty() ? p.p : ch.pts.back();
|
|
for (auto [c, toward] : { std::make_pair(&p, p_next), std::make_pair(&q, q_prev) }) {
|
|
Vec3f d = toward - c->p;
|
|
const float dl = d.norm();
|
|
if (dl < 1e-12f) continue;
|
|
const Vec3f ed = (pos[size_t(c->b)] - pos[size_t(c->a)]).normalized();
|
|
c->sin_min = std::min(c->sin_min, ed.cross(d / dl).norm());
|
|
// Only the part of a move along the edge that runs along the contour brings the copy
|
|
// toward the next vertex's copies. With no interior vertex the chord's two ends share
|
|
// it; otherwise the interior copies, placed first, say how much room there is.
|
|
if (ch.pts.empty())
|
|
c->dt_max = std::min(c->dt_max, 0.45f * dl / std::max(std::abs(ed.dot(d / dl)), 1e-3f));
|
|
}
|
|
}
|
|
}
|
|
|
|
// 5. Output vertices: the originals, then one or two per crossing and per interior contour vertex.
|
|
// A sharp crossing that a contour reaches is doubled, into two vertices on the edge either side of
|
|
// it, each on its own side of the contour; a triangle on that edge that is not cut itself simply
|
|
// carries both on its perimeter, one after the other, so the cut neighbour's wall still meets a
|
|
// closed surface. Keeping the copies on the edge is what keeps every triangle's split planar and
|
|
// inside the triangle. A crossing that is not sharp, or that no contour reaches, stays one vertex
|
|
// and the surface ramps there.
|
|
indexed_triangle_set out;
|
|
out.vertices = pos;
|
|
const float half_gap = 0.5f * seam_gap_mm;
|
|
// Interior contour vertices: doubled when the contour's ends are, offset across the contour along
|
|
// the bisector of the two segments meeting there, toward the high side; mitre limited, and held
|
|
// inside the triangle - a copy pushed across an edge would fold the region over the neighbour.
|
|
for (size_t t = 0; t < nt; ++t) {
|
|
Loop &L = loops[t];
|
|
if (!L.ok)
|
|
continue;
|
|
const auto &tri = mesh.indices[t];
|
|
const Vec3f &A = pos[size_t(tri[0])], &B = pos[size_t(tri[1])], &C = pos[size_t(tri[2])];
|
|
const Vec3f U = (B - A).normalized(), V = L.N.cross(U);
|
|
const Vec2f a2(0.f, 0.f), b2((B - A).dot(U), (B - A).dot(V)), c2((C - A).dot(U), (C - A).dot(V));
|
|
const float det = (b2.x() - a2.x()) * (c2.y() - a2.y()) - (c2.x() - a2.x()) * (b2.y() - a2.y());
|
|
const auto bary2 = [&](const Vec3f &p) {
|
|
const Vec2f q((p - A).dot(U), (p - A).dot(V));
|
|
const float wb = ((q.x() - a2.x()) * (c2.y() - a2.y()) - (c2.x() - a2.x()) * (q.y() - a2.y())) / det;
|
|
const float wc = ((b2.x() - a2.x()) * (q.y() - a2.y()) - (q.x() - a2.x()) * (b2.y() - a2.y())) / det;
|
|
return Vec3f(1.f - wb - wc, wb, wc);
|
|
};
|
|
// The largest fraction of the offset that keeps the copy inside, with a little to spare.
|
|
const auto inside_fraction = [&](const Vec3f &p, const Vec3f &off) {
|
|
const Vec3f w0 = bary2(p), w1 = bary2(p + off);
|
|
float lambda = 1.f;
|
|
for (int i = 0; i < 3; ++i)
|
|
if (w1[i] < 0.f && w0[i] > w1[i])
|
|
lambda = std::min(lambda, w0[i] / (w0[i] - w1[i]));
|
|
return 0.9f * std::max(lambda, 0.f);
|
|
};
|
|
const bool s0 = side_of(vh[size_t(mesh.indices[t][0])]);
|
|
for (auto &ch : L.chains) {
|
|
const Crossing &p = *L.xs[size_t(ch.i)], &q = *L.xs[size_t(ch.j)];
|
|
const bool doubled = p.sharp && p.used && !p.no_seam && q.sharp && q.used && !q.no_seam;
|
|
// The arc right after crossing ch.i lies on one side; which way that is from the contour
|
|
// says where "high" is. The arc's side: the loop's first corner's, flipped once per
|
|
// crossing passed, so after crossing index k it is !s0 for even k and s0 for odd.
|
|
const bool arc_high = (ch.i % 2 == 0) ? !s0 : s0;
|
|
const int after_pos = L.xpos[size_t(ch.i)] + 1;
|
|
const int entry = L.ids[size_t(after_pos) % L.ids.size()];
|
|
const Vec3f ref = entry >= 0 ? pos[size_t(entry)] : L.xs[size_t(-entry - 1)]->p;
|
|
const size_t m = ch.pts.size();
|
|
ch.lo.resize(m); ch.hi.resize(m);
|
|
// The left of the walk from i to j is one side of the contour all along it; whether that
|
|
// side is high is settled once, at the first segment, where the arc is a known reference.
|
|
// The direction the contour leaves the crossing in, taken over a usable length: the first
|
|
// interior vertex may sit right next to it.
|
|
Vec3f d_first = q.p - p.p;
|
|
for (size_t k = 0; k < m; ++k)
|
|
if ((ch.pts[k] - p.p).norm() > 0.25f * seam_gap_mm) { d_first = ch.pts[k] - p.p; break; }
|
|
d_first.normalize();
|
|
const bool left_high = (L.N.cross(d_first).dot(ref - p.p) > 0.f) == arc_high;
|
|
for (size_t k = 0; k < m; ++k) {
|
|
if (!doubled) {
|
|
ch.lo[k] = ch.hi[k] = int(out.vertices.size());
|
|
out.vertices.push_back(ch.pts[k]);
|
|
continue;
|
|
}
|
|
const Vec3f prev = (k == 0) ? p.p : ch.pts[k - 1];
|
|
const Vec3f next = (k + 1 == m) ? q.p : ch.pts[k + 1];
|
|
Vec3f d1 = (ch.pts[k] - prev).normalized(), d2 = (next - ch.pts[k]).normalized();
|
|
Vec3f n1 = L.N.cross(d1), n2 = L.N.cross(d2);
|
|
Vec3f bis = n1 + n2;
|
|
float bl = bis.norm();
|
|
Vec3f off;
|
|
if (bl < 1e-6f) off = n1;
|
|
else {
|
|
bis /= bl;
|
|
// Mitre: the offset polyline stays `half_gap` from both segments, up to twice that.
|
|
const float cos_half = std::max(bis.dot(n1), 0.5f);
|
|
off = bis / cos_half;
|
|
}
|
|
if (!left_high) off = -off;
|
|
// No further along either segment than half its length, so copies never cross their
|
|
// neighbours'; across the segments the offset is free.
|
|
const float ol = off.norm();
|
|
const float reach = std::min({ half_gap, 0.45f * ch.clearance,
|
|
0.45f * (ch.pts[k] - prev).norm() / std::max(std::abs(off.dot(d1)) / ol, 1e-3f),
|
|
0.45f * (next - ch.pts[k]).norm() / std::max(std::abs(off.dot(d2)) / ol, 1e-3f) });
|
|
const Vec3f off_lo = -off * (reach * inside_fraction(ch.pts[k], -off * reach));
|
|
const Vec3f off_hi = off * (reach * inside_fraction(ch.pts[k], off * reach));
|
|
// As for the crossings: each copy at the first of a few positions outward that samples
|
|
// pure, or the vertex stays single.
|
|
const auto place = [&](const Vec3f &o, bool high) -> int {
|
|
for (int step = 1; step <= 4; ++step) {
|
|
const Vec3f q = ch.pts[k] + o * (0.5f * float(step));
|
|
if (step > 2 && inside_fraction(ch.pts[k], q - ch.pts[k]) < 0.999f) break;
|
|
const float h = sampler(q, L.N) - iso;
|
|
if ((h > 0.f) == high && std::abs(h) >= 0.3f * range) {
|
|
out.vertices.push_back(q);
|
|
return int(out.vertices.size()) - 1;
|
|
}
|
|
}
|
|
return -1;
|
|
};
|
|
const int il = place(off_lo, false), ih = place(off_hi, true);
|
|
if (il < 0 || ih < 0) {
|
|
out.vertices.resize(out.vertices.size() - (il >= 0 ? 1 : 0) - (ih >= 0 ? 1 : 0));
|
|
ch.lo[k] = ch.hi[k] = int(out.vertices.size());
|
|
out.vertices.push_back(ch.pts[k]);
|
|
continue;
|
|
}
|
|
ch.lo[k] = il; ch.hi[k] = ih;
|
|
}
|
|
// Room for the end crossings' copies along the edge: the nearer of the first interior
|
|
// vertex's copies, measured along the contour, less a margin, over the edge's share of
|
|
// that direction.
|
|
if (m > 0)
|
|
for (auto [c, k, other] : { std::make_tuple(&p, size_t(0), q.p), std::make_tuple(&q, m - 1, p.p) }) {
|
|
(void) other;
|
|
Crossing &cr = const_cast<Crossing &>(*c);
|
|
const Vec3f d = ch.pts[k] - cr.p;
|
|
const float dl = d.norm();
|
|
if (dl < 1e-9f) continue;
|
|
const Vec3f dn = d / dl;
|
|
const float u_lo = (out.vertices[size_t(ch.lo[k])] - cr.p).dot(dn);
|
|
const float u_hi = (out.vertices[size_t(ch.hi[k])] - cr.p).dot(dn);
|
|
const Vec3f ed = (pos[size_t(cr.b)] - pos[size_t(cr.a)]).normalized();
|
|
cr.dt_max = std::min(cr.dt_max, 0.9f * std::max(std::min(u_lo, u_hi), 0.f) / std::max(std::abs(ed.dot(dn)), 1e-3f));
|
|
}
|
|
}
|
|
}
|
|
|
|
for (auto &kv : edges) {
|
|
Edge &edge = kv.second;
|
|
for (size_t i = 0; i < edge.xs.size(); ++i) {
|
|
Crossing &c = edge.xs[i];
|
|
if (!(c.sharp && c.used) || c.no_seam) {
|
|
c.single = int(out.vertices.size());
|
|
out.vertices.push_back(c.p);
|
|
continue;
|
|
}
|
|
const float len = (pos[size_t(c.b)] - pos[size_t(c.a)]).norm();
|
|
// Room along the edge: a third of the way to the previous and the next crossing or vertex,
|
|
// and less than half the shortest contour segment leaving the crossing, so the copies of
|
|
// the contour's next vertex cannot cross these.
|
|
const float t_prev = (i == 0) ? 0.f : edge.xs[i - 1].t;
|
|
const float t_next = (i + 1 == edge.xs.size()) ? 1.f : edge.xs[i + 1].t;
|
|
float dt = half_gap / std::max(c.sin_min, 0.15f) / std::max(len, 1e-6f);
|
|
dt = std::min({ dt, (c.t - t_prev) / 3.f, (t_next - c.t) / 3.f, c.dt_max / std::max(len, 1e-6f) });
|
|
if (!(dt > 1e-6f)) {
|
|
c.single = int(out.vertices.size());
|
|
out.vertices.push_back(c.p);
|
|
continue;
|
|
}
|
|
// The side of the edge before this crossing: the lower vertex's, flipped once per crossing
|
|
// passed on the way.
|
|
const bool before_high = side_of(vh[size_t(c.a)]) != (i % 2 == 1);
|
|
// Each copy has to sample a pure value of its own side - inside the step's blend it would be
|
|
// displaced to a height between the sides, and one such vertex tilts every triangle at it.
|
|
// The copy is placed at the first of a few positions along the edge, from dt outward, that
|
|
// samples pure; when none does the crossing stays one vertex and the surface ramps across
|
|
// that edge only.
|
|
// The search never goes past what the neighbours allow: less than half way to the next
|
|
// crossing along the edge (so two crossings' copies never meet), most of the way to the
|
|
// edge's vertex, and not past dt_max toward the contour's next vertex.
|
|
const float lo_share = (i == 0) ? 0.9f : 0.49f, hi_share = (i + 1 == edge.xs.size()) ? 0.9f : 0.49f;
|
|
const float t_lo_lim = c.t - std::min(lo_share * (c.t - t_prev), c.dt_max / std::max(len, 1e-6f));
|
|
const float t_hi_lim = c.t + std::min(hi_share * (t_next - c.t), c.dt_max / std::max(len, 1e-6f));
|
|
const auto place = [&](float sgn, bool high) -> int {
|
|
for (int step = 1; step <= 6; ++step) {
|
|
const float tt = c.t + sgn * dt * (0.5f * float(step));
|
|
if (tt < t_lo_lim || tt > t_hi_lim) break;
|
|
const auto [q, qn] = sample_between(c.a, c.b, tt);
|
|
const float h = sampler(q, qn) - iso;
|
|
if ((h > 0.f) == high && std::abs(h) >= 0.3f * range) {
|
|
out.vertices.push_back(q);
|
|
return int(out.vertices.size()) - 1;
|
|
}
|
|
}
|
|
return -1;
|
|
};
|
|
const int na = place(-1.f, before_high), nb = place(+1.f, !before_high);
|
|
if (na < 0 || nb < 0) {
|
|
out.vertices.resize(out.vertices.size() - (na >= 0 ? 1 : 0) - (nb >= 0 ? 1 : 0));
|
|
c.single = int(out.vertices.size());
|
|
out.vertices.push_back(c.p);
|
|
continue;
|
|
}
|
|
c.near_a = na; c.near_b = nb;
|
|
c.lo = before_high ? c.near_b : c.near_a;
|
|
c.hi = before_high ? c.near_a : c.near_b;
|
|
}
|
|
}
|
|
// 6. Output triangles: each region of a cut triangle ear-clipped on its own side's copies, a wall
|
|
// strip per contour wound with the surface, everything else passed through or plainly split.
|
|
out.indices.reserve(nt * 2);
|
|
std::vector<int> source;
|
|
source.reserve(nt * 2);
|
|
const auto emit = [&](int a, int b, int c, int src) { out.indices.emplace_back(a, b, c); source.push_back(src); };
|
|
const auto copy_for = [&](const Crossing &c, bool high) { return c.single >= 0 ? c.single : (high ? c.hi : c.lo); };
|
|
// Triangulates a triangle that carries extra vertices on its edges without a zero-area sliver: the
|
|
// first extra vertex found is joined to the opposite corner, which splits the triangle in two that
|
|
// carry fewer extras each, and so on. A fan from a corner cannot do this - the extras on that
|
|
// corner's own edges are collinear with it. Each side is the corner followed by the extras on the
|
|
// way to the next corner.
|
|
const std::function<void(std::array<std::vector<int>, 3>, int)> split_tri =
|
|
[&](std::array<std::vector<int>, 3> side, int src) {
|
|
for (int e = 0; e < 3; ++e)
|
|
if (side[size_t(e)].size() > 1) {
|
|
const auto &S = side[size_t(e)];
|
|
const int a = S[0], x = S[1], c = side[size_t((e + 2) % 3)][0];
|
|
std::vector<int> rest(S.begin() + 1, S.end()); // x and the extras after it
|
|
split_tri({ std::vector<int>{ a }, std::vector<int>{ x }, side[size_t((e + 2) % 3)] }, src);
|
|
split_tri({ std::move(rest), side[size_t((e + 1) % 3)], std::vector<int>{ c } }, src);
|
|
return;
|
|
}
|
|
emit(side[0][0], side[1][0], side[2][0], src);
|
|
};
|
|
// A wall strip between the low and high copies of a contour, from crossing p to crossing q.
|
|
const auto emit_wall = [&](const Crossing &p, const Chain &ch, const Crossing &q, const Vec3f &N, int src) {
|
|
std::vector<int> lo, hi;
|
|
lo.push_back(copy_for(p, false)); hi.push_back(copy_for(p, true));
|
|
for (size_t k = 0; k < ch.pts.size(); ++k) { lo.push_back(ch.lo[k]); hi.push_back(ch.hi[k]); }
|
|
lo.push_back(copy_for(q, false)); hi.push_back(copy_for(q, true));
|
|
auto tri_up = [&](int i, int j, int k) {
|
|
if (i == j || j == k || k == i) return;
|
|
const Vec3f &A = out.vertices[size_t(i)], &B = out.vertices[size_t(j)], &C = out.vertices[size_t(k)];
|
|
if ((B - A).cross(C - A).dot(N) >= 0.f) emit(i, j, k, src); else emit(i, k, j, src);
|
|
};
|
|
for (size_t k = 0; k + 1 < lo.size(); ++k) {
|
|
tri_up(lo[k], lo[k + 1], hi[k + 1]);
|
|
tri_up(lo[k], hi[k + 1], hi[k]);
|
|
}
|
|
};
|
|
|
|
for (size_t t = 0; t < nt; ++t) {
|
|
const auto &tri = mesh.indices[t];
|
|
const Loop &L = loops[t];
|
|
const int n = int(L.xs.size());
|
|
if (n == 0) { emit(tri[0], tri[1], tri[2], int(t)); continue; }
|
|
const size_t m = L.ids.size();
|
|
const bool s0 = side_of(vh[size_t(tri[0])]);
|
|
|
|
if (L.ok) {
|
|
// Every crossing starts one region: the run of the loop after it, on the side the field has
|
|
// there, following any other contour it meets back to the loop, until it returns to the
|
|
// start. Each region is tracked per (crossing, side) so the one between two contours is
|
|
// emitted once, not once per contour. The region's vertices are collected in the triangle's
|
|
// plane for ear clipping, since a traced contour can make it concave.
|
|
const Vec3f &A = pos[size_t(tri[0])], &B = pos[size_t(tri[1])];
|
|
const Vec3f U = (B - A).normalized(), V = L.N.cross(U);
|
|
std::vector<Vec2f> pts2;
|
|
std::vector<int> ids2;
|
|
const auto push = [&](int id) {
|
|
const Vec3f &p = out.vertices[size_t(id)];
|
|
pts2.emplace_back((p - A).dot(U), (p - A).dot(V));
|
|
ids2.push_back(id);
|
|
};
|
|
// Interior vertices of chain c, walking from crossing `from`, on side S.
|
|
const auto push_chain = [&](int c, int from, bool S) {
|
|
const Chain &ch = L.chains[size_t(c)];
|
|
const auto &cp = S ? ch.hi : ch.lo;
|
|
if (from == ch.i) for (size_t k = 0; k < cp.size(); ++k) push(cp[k]);
|
|
else for (size_t k = cp.size(); k-- > 0;) push(cp[k]);
|
|
};
|
|
const auto side_after = [&](int k) { return (k % 2 == 0) ? !s0 : s0; };
|
|
std::vector<std::array<bool, 2>> consumed(static_cast<size_t>(n), { false, false });
|
|
for (int k = 0; k < n; ++k) {
|
|
const bool S = side_after(k);
|
|
if (consumed[size_t(k)][S])
|
|
continue;
|
|
pts2.clear(); ids2.clear();
|
|
push(copy_for(*L.xs[size_t(k)], S));
|
|
consumed[size_t(k)][S] = true;
|
|
size_t i = size_t(L.xpos[size_t(k)] + 1) % m;
|
|
for (size_t guard = 0; guard < 2 * m; ++guard) {
|
|
const int id = L.ids[i];
|
|
if (id >= 0) { push(id); i = (i + 1) % m; continue; }
|
|
const int c = -id - 1;
|
|
consumed[size_t(c)][S] = true;
|
|
push(copy_for(*L.xs[size_t(c)], S));
|
|
if (c == L.partner[size_t(k)])
|
|
break;
|
|
// Follow the contour from c to its partner, then continue the loop after it.
|
|
const int pc = L.partner[size_t(c)];
|
|
push_chain(L.chain_of[size_t(c)], c, S);
|
|
consumed[size_t(pc)][S] = true;
|
|
push(copy_for(*L.xs[size_t(pc)], S));
|
|
if (pc == L.partner[size_t(k)])
|
|
break;
|
|
i = size_t(L.xpos[size_t(pc)] + 1) % m;
|
|
}
|
|
// Close the polygon along the region's own contour back to k, in reverse.
|
|
push_chain(L.chain_of[size_t(k)], L.partner[size_t(k)], S);
|
|
std::vector<int> poly(ids2.size());
|
|
std::iota(poly.begin(), poly.end(), 0);
|
|
ear_clip(pts2, poly, [&](int a, int b, int c) { emit(ids2[size_t(a)], ids2[size_t(b)], ids2[size_t(c)], int(t)); });
|
|
}
|
|
for (const auto &ch : L.chains)
|
|
emit_wall(*L.xs[size_t(ch.i)], ch, *L.xs[size_t(ch.j)], L.N, int(t));
|
|
continue;
|
|
}
|
|
// Not cut: split at the crossings so the neighbours' cuts meet no T-junction. A doubled crossing
|
|
// contributes both copies, the one nearer the vertex the walk comes from first.
|
|
std::array<std::vector<int>, 3> side;
|
|
int e = -1;
|
|
for (size_t i = 0; i < m; ++i) {
|
|
if (L.ids[i] >= 0) { ++e; side[size_t(e)].push_back(L.ids[i]); continue; }
|
|
const Crossing &c = *L.xs[size_t(-L.ids[i] - 1)];
|
|
if (c.single >= 0) { side[size_t(e)].push_back(c.single); continue; }
|
|
const bool from_a = tri[e] == c.a;
|
|
side[size_t(e)].push_back(from_a ? c.near_a : c.near_b);
|
|
side[size_t(e)].push_back(from_a ? c.near_b : c.near_a);
|
|
}
|
|
split_tri(std::move(side), int(t));
|
|
}
|
|
// Safety net: a texture whose features are of the seam's own scale can fold copies over each
|
|
// other in spite of everything above. When more than a trace of the output faces the wrong way,
|
|
// the cut is not trusted and the mesh goes out as it came in.
|
|
{
|
|
size_t inverted = 0;
|
|
for (size_t i = 0; i < out.indices.size(); ++i) {
|
|
const auto &f = out.indices[i];
|
|
const auto &g = mesh.indices[size_t(source[i])];
|
|
const Vec3f n = (out.vertices[size_t(f[1])] - out.vertices[size_t(f[0])]).cross(out.vertices[size_t(f[2])] - out.vertices[size_t(f[0])]);
|
|
const Vec3f ns = (mesh.vertices[size_t(g[1])] - mesh.vertices[size_t(g[0])]).cross(mesh.vertices[size_t(g[2])] - mesh.vertices[size_t(g[0])]);
|
|
inverted += n.dot(ns) < 0.f && n.norm() > 1e-7f;
|
|
}
|
|
if (double(inverted) > 0.002 * double(out.indices.size()))
|
|
return passthrough();
|
|
}
|
|
if (out_source) *out_source = std::move(source);
|
|
if (out_cut_count) *out_cut_count = cut_count;
|
|
return out;
|
|
}
|
|
|
|
} // namespace Slic3r
|