mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-11 11:07:40 +00:00
Add new bake pipeline
This commit is contained in:
@@ -464,6 +464,22 @@ set(lisbslic3r_sources
|
|||||||
TextConfiguration.hpp
|
TextConfiguration.hpp
|
||||||
TextureDisplacement.cpp
|
TextureDisplacement.cpp
|
||||||
TextureDisplacement.hpp
|
TextureDisplacement.hpp
|
||||||
|
TextureBake/TextureBakeIndex.cpp
|
||||||
|
TextureBake/TextureBakeIndex.hpp
|
||||||
|
TextureBake/TextureBakeSubdivide.cpp
|
||||||
|
TextureBake/TextureBakeSubdivide.hpp
|
||||||
|
TextureBake/TextureBakeRegularize.cpp
|
||||||
|
TextureBake/TextureBakeRegularize.hpp
|
||||||
|
TextureBake/TextureBakeDisplace.cpp
|
||||||
|
TextureBake/TextureBakeDisplace.hpp
|
||||||
|
TextureBake/TextureBakeDecimate.cpp
|
||||||
|
TextureBake/TextureBakeDecimate.hpp
|
||||||
|
TextureBake/TextureBakeRepair.cpp
|
||||||
|
TextureBake/TextureBakeRepair.hpp
|
||||||
|
TextureBake/TextureBakePipeline.cpp
|
||||||
|
TextureBake/TextureBakePipeline.hpp
|
||||||
|
TextureBake/TextureBakeMesh.cpp
|
||||||
|
TextureBake/TextureBakeMesh.hpp
|
||||||
Thread.cpp
|
Thread.cpp
|
||||||
Thread.hpp
|
Thread.hpp
|
||||||
Time.cpp
|
Time.cpp
|
||||||
|
|||||||
478
src/libslic3r/TextureBake/TextureBakeDecimate.cpp
Normal file
478
src/libslic3r/TextureBake/TextureBakeDecimate.cpp
Normal file
@@ -0,0 +1,478 @@
|
|||||||
|
#include "TextureBakeDecimate.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
|
#include <cmath>
|
||||||
|
#include <limits>
|
||||||
|
#include <queue>
|
||||||
|
|
||||||
|
namespace Slic3r {
|
||||||
|
namespace TextureBake {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Symmetric 4x4 quadric, as its 10 upper-triangle values.
|
||||||
|
struct Quadric
|
||||||
|
{
|
||||||
|
std::array<double, 10> q{};
|
||||||
|
|
||||||
|
void add_plane(double a, double b, double c, double d)
|
||||||
|
{
|
||||||
|
q[0] += a * a; q[1] += a * b; q[2] += a * c; q[3] += a * d;
|
||||||
|
q[4] += b * b; q[5] += b * c; q[6] += b * d;
|
||||||
|
q[7] += c * c; q[8] += c * d;
|
||||||
|
q[9] += d * d;
|
||||||
|
}
|
||||||
|
void operator+=(const Quadric &o)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < 10; ++i)
|
||||||
|
q[size_t(i)] += o.q[size_t(i)];
|
||||||
|
}
|
||||||
|
double eval(double x, double y, double z) const
|
||||||
|
{
|
||||||
|
return q[0] * x * x + 2 * q[1] * x * y + 2 * q[2] * x * z + 2 * q[3] * x +
|
||||||
|
q[4] * y * y + 2 * q[5] * y * z + 2 * q[6] * y +
|
||||||
|
q[7] * z * z + 2 * q[8] * z + q[9];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
double eval_sum(const std::vector<Quadric> &qs, int v1, int v2, const Vec3d &p)
|
||||||
|
{
|
||||||
|
return qs[size_t(v1)].eval(p.x(), p.y(), p.z()) + qs[size_t(v2)].eval(p.x(), p.y(), p.z());
|
||||||
|
}
|
||||||
|
|
||||||
|
// The position minimising the summed quadric, if the system is well conditioned enough to trust.
|
||||||
|
bool solve_q(const std::vector<Quadric> &qs, int v1, int v2, Vec3d &out)
|
||||||
|
{
|
||||||
|
const auto &A = qs[size_t(v1)].q;
|
||||||
|
const auto &B = qs[size_t(v2)].q;
|
||||||
|
const double a00 = A[0] + B[0], a01 = A[1] + B[1], a02 = A[2] + B[2];
|
||||||
|
const double a11 = A[4] + B[4], a12 = A[5] + B[5], a22 = A[7] + B[7];
|
||||||
|
const double b0 = -(A[3] + B[3]), b1 = -(A[6] + B[6]), b2 = -(A[8] + B[8]);
|
||||||
|
|
||||||
|
const double det = a00 * (a11 * a22 - a12 * a12) - a01 * (a01 * a22 - a12 * a02) +
|
||||||
|
a02 * (a01 * a12 - a11 * a02);
|
||||||
|
const double max_el = std::max({ std::abs(a00), std::abs(a01), std::abs(a02), std::abs(a11),
|
||||||
|
std::abs(a12), std::abs(a22) });
|
||||||
|
// Scaled with the matrix, so it means the same at any model scale.
|
||||||
|
const double threshold = max_el * max_el * max_el * 1e-10;
|
||||||
|
if (std::abs(det) < std::max(threshold, 1e-30))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
const double inv = 1.0 / det;
|
||||||
|
out.x() = inv * (b0 * (a11 * a22 - a12 * a12) - a01 * (b1 * a22 - a12 * b2) + a02 * (b1 * a12 - a11 * b2));
|
||||||
|
out.y() = inv * (a00 * (b1 * a22 - a12 * b2) - b0 * (a01 * a22 - a12 * a02) + a02 * (a01 * b2 - b1 * a02));
|
||||||
|
out.z() = inv * (a00 * (a11 * b2 - b1 * a12) - a01 * (a01 * b2 - b1 * a02) + b0 * (a01 * a12 - a11 * a02));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Vec3d face_normal_unit(const std::vector<Vec3d> &pos, int a, int b, int c)
|
||||||
|
{
|
||||||
|
const Vec3d n = (pos[size_t(b)] - pos[size_t(a)]).cross(pos[size_t(c)] - pos[size_t(a)]);
|
||||||
|
const double len = n.norm();
|
||||||
|
return (len > 0.0) ? Vec3d(n / len) : Vec3d::Zero();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Versions are captured at push time; a mismatch on pop means a later collapse invalidated the entry.
|
||||||
|
// Lazy deletion, far cheaper than removing entries eagerly.
|
||||||
|
struct HeapEntry
|
||||||
|
{
|
||||||
|
double cost;
|
||||||
|
int v1, v2;
|
||||||
|
uint32_t ver1, ver2;
|
||||||
|
Vec3d p;
|
||||||
|
bool operator>(const HeapEntry &o) const { return cost > o.cost; }
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
DecimateResult decimate(const TriSoup &geometry, size_t target_triangles, bool harvest_flat,
|
||||||
|
double harvest_tol, const std::vector<uint8_t> &locked_faces,
|
||||||
|
const DecimateProgressFn &on_progress)
|
||||||
|
{
|
||||||
|
DecimateResult result;
|
||||||
|
const size_t n = geometry.pos.size();
|
||||||
|
if (n < 3) {
|
||||||
|
result.geometry = geometry;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The finest grid. Anything coarser fuses distinct fine-feature vertices on a displaced mesh,
|
||||||
|
// leaving it non-manifold before decimation starts and producing open edges afterwards.
|
||||||
|
QuantizedPointMap vert_map(WELD_GRID_DECIMATION, std::min(n, size_t(1) << 22));
|
||||||
|
std::vector<Vec3d> pos;
|
||||||
|
std::vector<int> remap(n);
|
||||||
|
for (size_t i = 0; i < n; ++i) {
|
||||||
|
const int idx = vert_map.get_or_set(geometry.pos[i], int(pos.size()));
|
||||||
|
if (vert_map.inserted())
|
||||||
|
pos.push_back(geometry.pos[i].cast<double>());
|
||||||
|
remap[i] = idx;
|
||||||
|
}
|
||||||
|
const size_t vert_count = pos.size();
|
||||||
|
const size_t face_count = n / 3;
|
||||||
|
std::vector<int> faces(face_count * 3);
|
||||||
|
for (size_t i = 0; i < n; ++i)
|
||||||
|
faces[i] = remap[i];
|
||||||
|
|
||||||
|
if (face_count <= target_triangles && !harvest_flat) {
|
||||||
|
result.geometry = geometry;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// An edge with a locked endpoint never reaches the heap.
|
||||||
|
std::vector<uint8_t> locked_vert;
|
||||||
|
size_t locked_face_count = 0;
|
||||||
|
if (!locked_faces.empty()) {
|
||||||
|
locked_vert.assign(vert_count, 0);
|
||||||
|
for (size_t f = 0; f < face_count && f < locked_faces.size(); ++f) {
|
||||||
|
if (!locked_faces[f])
|
||||||
|
continue;
|
||||||
|
++locked_face_count;
|
||||||
|
for (int k = 0; k < 3; ++k)
|
||||||
|
locked_vert[size_t(faces[f * 3 + size_t(k)])] = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// With the locked faces alone at the target, chasing it would grind the free region to its guard
|
||||||
|
// limit for nothing - harvest only, and say so.
|
||||||
|
const bool locked_over_budget =
|
||||||
|
!locked_vert.empty() && face_count > target_triangles && locked_face_count >= target_triangles;
|
||||||
|
result.locked_over_budget = locked_over_budget;
|
||||||
|
if (locked_over_budget && !harvest_flat) {
|
||||||
|
result.geometry = geometry;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<Quadric> quadrics(vert_count);
|
||||||
|
for (size_t f = 0; f < face_count; ++f) {
|
||||||
|
const int a = faces[f * 3], b = faces[f * 3 + 1], c = faces[f * 3 + 2];
|
||||||
|
if (a < 0)
|
||||||
|
continue;
|
||||||
|
const Vec3d nrm = face_normal_unit(pos, a, b, c);
|
||||||
|
if (nrm.isZero())
|
||||||
|
continue;
|
||||||
|
const double d = -nrm.dot(pos[size_t(a)]);
|
||||||
|
for (const int v : { a, b, c })
|
||||||
|
quadrics[size_t(v)].add_plane(nrm.x(), nrm.y(), nrm.z(), d);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Two penalty planes per endpoint on a sharp interior edge, each perpendicular to one adjacent
|
||||||
|
// face and containing the edge, constraining the vertex to the crease line.
|
||||||
|
{
|
||||||
|
struct EdgeRec { int va, vb, f0, f1; uint8_t count; };
|
||||||
|
std::vector<EdgeRec> edges;
|
||||||
|
QuantizedPointMap edge_idx(1.0, std::min(face_count * 3, size_t(1) << 22));
|
||||||
|
for (size_t f = 0; f < face_count; ++f) {
|
||||||
|
if (faces[f * 3] < 0)
|
||||||
|
continue;
|
||||||
|
for (int e = 0; e < 3; ++e) {
|
||||||
|
const int va = faces[f * 3 + size_t(e)];
|
||||||
|
const int vb = faces[f * 3 + size_t((e + 1) % 3)];
|
||||||
|
const int lo = std::min(va, vb), hi = std::max(va, vb);
|
||||||
|
const int ei = edge_idx.get_or_set_key(lo, hi, 0, int(edges.size()));
|
||||||
|
if (edge_idx.inserted())
|
||||||
|
edges.push_back({ lo, hi, int(f), -1, 1 });
|
||||||
|
else if (edges[size_t(ei)].count == 1) {
|
||||||
|
edges[size_t(ei)].f1 = int(f);
|
||||||
|
edges[size_t(ei)].count = 2;
|
||||||
|
} else
|
||||||
|
// Non-manifold; never feeds a crease.
|
||||||
|
edges[size_t(ei)].count = 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const double sqrt_w = std::sqrt(DECIMATE_CREASE_WEIGHT);
|
||||||
|
for (const EdgeRec &er : edges) {
|
||||||
|
if (er.count != 2)
|
||||||
|
continue; // boundary or non-manifold
|
||||||
|
const Vec3d n0 = face_normal_unit(pos, faces[size_t(er.f0) * 3], faces[size_t(er.f0) * 3 + 1],
|
||||||
|
faces[size_t(er.f0) * 3 + 2]);
|
||||||
|
const Vec3d n1 = face_normal_unit(pos, faces[size_t(er.f1) * 3], faces[size_t(er.f1) * 3 + 1],
|
||||||
|
faces[size_t(er.f1) * 3 + 2]);
|
||||||
|
if (n0.dot(n1) >= DECIMATE_CREASE_COS)
|
||||||
|
continue; // smooth enough to be no crease
|
||||||
|
|
||||||
|
const Vec3d e = pos[size_t(er.vb)] - pos[size_t(er.va)];
|
||||||
|
const double elen = e.norm();
|
||||||
|
if (elen <= 0.0)
|
||||||
|
continue;
|
||||||
|
const Vec3d ed = e / elen;
|
||||||
|
for (const Vec3d &fn : { n0, n1 }) {
|
||||||
|
Vec3d pn = fn.cross(ed);
|
||||||
|
const double plen = pn.norm();
|
||||||
|
if (plen < 1e-10)
|
||||||
|
continue; // edge parallel to the face normal
|
||||||
|
pn /= plen;
|
||||||
|
const double d = -pn.dot(pos[size_t(er.va)]);
|
||||||
|
// sqrt(w) on the inputs gives w times the accumulated products.
|
||||||
|
for (const int v : { er.va, er.vb })
|
||||||
|
quadrics[size_t(v)].add_plane(pn.x() * sqrt_w, pn.y() * sqrt_w, pn.z() * sqrt_w,
|
||||||
|
d * sqrt_w);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vertex-face incidence as intrusive linked lists of slots over flat arrays.
|
||||||
|
const size_t S = face_count * 3;
|
||||||
|
std::vector<int> vf_head(vert_count, -1), slot_face(S), slot_vert(S), slot_next(S, -1),
|
||||||
|
slot_prev(S, -1), face_slot(S, -1);
|
||||||
|
for (size_t f = 0; f < face_count; ++f)
|
||||||
|
for (int k = 0; k < 3; ++k) {
|
||||||
|
const int s = int(f) * 3 + k;
|
||||||
|
const int v = faces[size_t(s)];
|
||||||
|
slot_face[size_t(s)] = int(f);
|
||||||
|
slot_vert[size_t(s)] = v;
|
||||||
|
slot_next[size_t(s)] = vf_head[size_t(v)];
|
||||||
|
slot_prev[size_t(s)] = -1;
|
||||||
|
if (vf_head[size_t(v)] >= 0)
|
||||||
|
slot_prev[size_t(vf_head[size_t(v)])] = s;
|
||||||
|
vf_head[size_t(v)] = s;
|
||||||
|
face_slot[size_t(s)] = s;
|
||||||
|
}
|
||||||
|
const auto unlink_slot = [&](int s) {
|
||||||
|
const int p = slot_prev[size_t(s)], nx = slot_next[size_t(s)];
|
||||||
|
if (p >= 0) slot_next[size_t(p)] = nx;
|
||||||
|
else vf_head[size_t(slot_vert[size_t(s)])] = nx;
|
||||||
|
if (nx >= 0) slot_prev[size_t(nx)] = p;
|
||||||
|
};
|
||||||
|
const auto move_slot = [&](int s, int nv) {
|
||||||
|
unlink_slot(s);
|
||||||
|
slot_next[size_t(s)] = vf_head[size_t(nv)];
|
||||||
|
slot_prev[size_t(s)] = -1;
|
||||||
|
if (vf_head[size_t(nv)] >= 0)
|
||||||
|
slot_prev[size_t(vf_head[size_t(nv)])] = s;
|
||||||
|
vf_head[size_t(nv)] = s;
|
||||||
|
slot_vert[size_t(s)] = nv;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<uint8_t> active(vert_count, 1);
|
||||||
|
std::vector<uint32_t> version(vert_count, 0);
|
||||||
|
std::vector<uint32_t> nb_stamp(vert_count, 0), lk_stamp(vert_count, 0);
|
||||||
|
uint32_t epoch = 1, lk_epoch = 1;
|
||||||
|
size_t active_faces = face_count;
|
||||||
|
|
||||||
|
std::priority_queue<HeapEntry, std::vector<HeapEntry>, std::greater<HeapEntry>> heap;
|
||||||
|
|
||||||
|
const auto push_edge = [&](int v1, int v2) {
|
||||||
|
Vec3d p;
|
||||||
|
if (!solve_q(quadrics, v1, v2, p)) {
|
||||||
|
const Vec3d mid = (pos[size_t(v1)] + pos[size_t(v2)]) * 0.5;
|
||||||
|
const double e1 = eval_sum(quadrics, v1, v2, pos[size_t(v1)]);
|
||||||
|
const double e2 = eval_sum(quadrics, v1, v2, pos[size_t(v2)]);
|
||||||
|
const double em = eval_sum(quadrics, v1, v2, mid);
|
||||||
|
const double emin = std::min({ e1, e2, em });
|
||||||
|
const double etol = emin * 1e-2 + 1e-12;
|
||||||
|
// The midpoint when the three are near-equal, i.e. flat: it moves adjacent triangles
|
||||||
|
// least, so fewer normal flips and no stalling on coplanar geometry.
|
||||||
|
if (em <= emin + etol) p = mid;
|
||||||
|
else if (e1 <= e2) p = pos[size_t(v1)];
|
||||||
|
else p = pos[size_t(v2)];
|
||||||
|
}
|
||||||
|
// Where quadric costs are all near zero, shorter edges first keeps triangle quality up.
|
||||||
|
const double len2 = (pos[size_t(v2)] - pos[size_t(v1)]).squaredNorm();
|
||||||
|
heap.push({ eval_sum(quadrics, v1, v2, p) + len2 * 1e-8, v1, v2, version[size_t(v1)],
|
||||||
|
version[size_t(v2)], p });
|
||||||
|
};
|
||||||
|
|
||||||
|
{
|
||||||
|
QuantizedPointMap seed_seen(1.0, std::min(face_count * 3, size_t(1) << 22));
|
||||||
|
for (size_t f = 0; f < face_count; ++f) {
|
||||||
|
if (faces[f * 3] < 0)
|
||||||
|
continue;
|
||||||
|
for (int e = 0; e < 3; ++e) {
|
||||||
|
const int va = faces[f * 3 + size_t(e)];
|
||||||
|
const int vb = faces[f * 3 + size_t((e + 1) % 3)];
|
||||||
|
if (!locked_vert.empty() && (locked_vert[size_t(va)] || locked_vert[size_t(vb)]))
|
||||||
|
continue;
|
||||||
|
seed_seen.get_or_set_key(std::min(va, vb), std::max(va, vb), 0, 1);
|
||||||
|
if (seed_seen.inserted())
|
||||||
|
push_edge(va, vb);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 0 means a stale entry, 1 a boundary edge, 2 or more safe.
|
||||||
|
const auto shared_face_count = [&](int v1, int v2) {
|
||||||
|
int count = 0;
|
||||||
|
for (int s = vf_head[size_t(v1)]; s >= 0; s = slot_next[size_t(s)]) {
|
||||||
|
const int f = slot_face[size_t(s)];
|
||||||
|
if (faces[size_t(f) * 3] < 0)
|
||||||
|
continue;
|
||||||
|
for (int k = 0; k < 3; ++k)
|
||||||
|
if (faces[size_t(f) * 3 + size_t(k)] == v2) {
|
||||||
|
if (++count >= 2)
|
||||||
|
return 2;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Safe only when the sole common neighbours of the endpoints are the apexes of the faces the edge
|
||||||
|
// already shares; any other would pile a third triangle onto an edge after the collapse.
|
||||||
|
const auto has_link_violation = [&](int v1, int v2, uint32_t ep) {
|
||||||
|
for (int s = vf_head[size_t(v1)]; s >= 0; s = slot_next[size_t(s)]) {
|
||||||
|
const int f = slot_face[size_t(s)];
|
||||||
|
if (faces[size_t(f) * 3] < 0)
|
||||||
|
continue;
|
||||||
|
for (int k = 0; k < 3; ++k)
|
||||||
|
if (const int x = faces[size_t(f) * 3 + size_t(k)]; x != v1)
|
||||||
|
lk_stamp[size_t(x)] = ep;
|
||||||
|
}
|
||||||
|
int shared = 0;
|
||||||
|
for (int s = vf_head[size_t(v1)]; s >= 0; s = slot_next[size_t(s)]) {
|
||||||
|
const int f = slot_face[size_t(s)];
|
||||||
|
if (faces[size_t(f) * 3] < 0)
|
||||||
|
continue;
|
||||||
|
const int a = faces[size_t(f) * 3], b = faces[size_t(f) * 3 + 1], c = faces[size_t(f) * 3 + 2];
|
||||||
|
if (a == v2 || b == v2 || c == v2) {
|
||||||
|
++shared;
|
||||||
|
const int apex = (a != v1 && a != v2) ? a : (b != v1 && b != v2) ? b : c;
|
||||||
|
lk_stamp[size_t(apex)] = ep + 1; // a legal shared-face apex
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (shared > 2)
|
||||||
|
return true; // already non-manifold
|
||||||
|
for (int s = vf_head[size_t(v2)]; s >= 0; s = slot_next[size_t(s)]) {
|
||||||
|
const int f = slot_face[size_t(s)];
|
||||||
|
if (faces[size_t(f) * 3] < 0)
|
||||||
|
continue;
|
||||||
|
for (int k = 0; k < 3; ++k) {
|
||||||
|
const int x = faces[size_t(f) * 3 + size_t(k)];
|
||||||
|
if (x != v2 && x != v1 && lk_stamp[size_t(x)] == ep)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Squared-dot, so no square root or division. Faces containing the other endpoint are the ones
|
||||||
|
// being removed, so they are skipped.
|
||||||
|
const auto check_flipped = [&](int vc, int vo, const Vec3d &np) {
|
||||||
|
for (int s = vf_head[size_t(vc)]; s >= 0; s = slot_next[size_t(s)]) {
|
||||||
|
const size_t f = size_t(slot_face[size_t(s)]);
|
||||||
|
if (faces[f * 3] < 0)
|
||||||
|
continue;
|
||||||
|
const int fa = faces[f * 3], fb = faces[f * 3 + 1], fc = faces[f * 3 + 2];
|
||||||
|
if (fa == vo || fb == vo || fc == vo)
|
||||||
|
continue;
|
||||||
|
const Vec3d oa = pos[size_t(fa)], ob = pos[size_t(fb)], oc = pos[size_t(fc)];
|
||||||
|
const Vec3d on = (ob - oa).cross(oc - oa);
|
||||||
|
const Vec3d na = (fa == vc) ? np : oa;
|
||||||
|
const Vec3d nb = (fb == vc) ? np : ob;
|
||||||
|
const Vec3d nc = (fc == vc) ? np : oc;
|
||||||
|
const Vec3d nn = (nb - na).cross(nc - na);
|
||||||
|
const double raw = on.dot(nn);
|
||||||
|
if (raw < 0.0)
|
||||||
|
return true;
|
||||||
|
if (raw * raw < DECIMATE_FLIP_DOT * DECIMATE_FLIP_DOT * on.squaredNorm() * nn.squaredNorm())
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const size_t init_faces = active_faces;
|
||||||
|
const size_t to_remove = std::max<size_t>(1, init_faces > target_triangles
|
||||||
|
? init_faces - target_triangles : init_faces);
|
||||||
|
const double harvest_ceil = harvest_tol * harvest_tol;
|
||||||
|
bool reached_target = locked_over_budget;
|
||||||
|
double last_progress = 0.0;
|
||||||
|
|
||||||
|
while (!heap.empty()) {
|
||||||
|
if (active_faces <= target_triangles) {
|
||||||
|
if (!harvest_flat)
|
||||||
|
break;
|
||||||
|
reached_target = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const HeapEntry top = heap.top();
|
||||||
|
heap.pop();
|
||||||
|
// The popped entry is the cheapest left, so exceeding the tolerance ends the run.
|
||||||
|
if (reached_target && top.cost > harvest_ceil)
|
||||||
|
break;
|
||||||
|
|
||||||
|
const int v1 = top.v1, v2 = top.v2;
|
||||||
|
if (!active[size_t(v1)] || !active[size_t(v2)])
|
||||||
|
continue;
|
||||||
|
if (version[size_t(v1)] != top.ver1 || version[size_t(v2)] != top.ver2)
|
||||||
|
continue;
|
||||||
|
if (shared_face_count(v1, v2) < 2)
|
||||||
|
continue;
|
||||||
|
lk_epoch += 2; // +2 so ep and ep+1 cannot collide with the next call
|
||||||
|
if (has_link_violation(v1, v2, lk_epoch))
|
||||||
|
continue;
|
||||||
|
if (check_flipped(v1, v2, top.p) || check_flipped(v2, v1, top.p))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// v1 survives at the new position, v2 goes.
|
||||||
|
pos[size_t(v1)] = top.p;
|
||||||
|
quadrics[size_t(v1)] += quadrics[size_t(v2)];
|
||||||
|
++version[size_t(v1)];
|
||||||
|
|
||||||
|
for (int s = vf_head[size_t(v2)]; s >= 0;) {
|
||||||
|
const size_t f = size_t(slot_face[size_t(s)]);
|
||||||
|
const int s_next = slot_next[size_t(s)]; // read before the list is modified
|
||||||
|
if (faces[f * 3] >= 0) {
|
||||||
|
for (int k = 0; k < 3; ++k)
|
||||||
|
if (faces[f * 3 + size_t(k)] == v2) {
|
||||||
|
faces[f * 3 + size_t(k)] = v1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const int fa = faces[f * 3], fb = faces[f * 3 + 1], fc = faces[f * 3 + 2];
|
||||||
|
if (fa == fb || fb == fc || fa == fc) {
|
||||||
|
for (int k = 0; k < 3; ++k)
|
||||||
|
if (const int sk = face_slot[f * 3 + size_t(k)]; sk >= 0) {
|
||||||
|
unlink_slot(sk);
|
||||||
|
face_slot[f * 3 + size_t(k)] = -1;
|
||||||
|
}
|
||||||
|
faces[f * 3] = faces[f * 3 + 1] = faces[f * 3 + 2] = -1;
|
||||||
|
--active_faces;
|
||||||
|
} else
|
||||||
|
move_slot(s, v1);
|
||||||
|
}
|
||||||
|
s = s_next;
|
||||||
|
}
|
||||||
|
active[size_t(v2)] = 0;
|
||||||
|
|
||||||
|
++epoch;
|
||||||
|
for (int sv = vf_head[size_t(v1)]; sv >= 0; sv = slot_next[size_t(sv)]) {
|
||||||
|
const size_t f = size_t(slot_face[size_t(sv)]);
|
||||||
|
if (faces[f * 3] < 0)
|
||||||
|
continue;
|
||||||
|
for (int k = 0; k < 3; ++k) {
|
||||||
|
const int nb = faces[f * 3 + size_t(k)];
|
||||||
|
if (nb == v1 || nb_stamp[size_t(nb)] == epoch)
|
||||||
|
continue;
|
||||||
|
nb_stamp[size_t(nb)] = epoch;
|
||||||
|
// v1 is never locked - a locked edge never entered the heap.
|
||||||
|
if (active[size_t(nb)] && (locked_vert.empty() || !locked_vert[size_t(nb)]))
|
||||||
|
push_edge(v1, nb);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (on_progress) {
|
||||||
|
const double p = std::min(1.0, double(init_faces - active_faces) / double(to_remove));
|
||||||
|
if (p - last_progress > 0.005) {
|
||||||
|
last_progress = p;
|
||||||
|
if (!on_progress(p))
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rebuild from the surviving faces, with per-face normals.
|
||||||
|
TriSoup &out = result.geometry;
|
||||||
|
for (size_t f = 0; f < face_count; ++f) {
|
||||||
|
if (faces[f * 3] < 0)
|
||||||
|
continue;
|
||||||
|
const Vec3f a = pos[size_t(faces[f * 3])].cast<float>();
|
||||||
|
const Vec3f b = pos[size_t(faces[f * 3 + 1])].cast<float>();
|
||||||
|
const Vec3f c = pos[size_t(faces[f * 3 + 2])].cast<float>();
|
||||||
|
const Vec3f nrm = (b - a).cross(c - a).normalized();
|
||||||
|
out.pos.insert(out.pos.end(), { a, b, c });
|
||||||
|
out.nrm.insert(out.nrm.end(), { nrm, nrm, nrm });
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace TextureBake
|
||||||
|
} // namespace Slic3r
|
||||||
56
src/libslic3r/TextureBake/TextureBakeDecimate.hpp
Normal file
56
src/libslic3r/TextureBake/TextureBakeDecimate.hpp
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
// Quadric error metric decimation (Garland & Heckbert), with two additions that matter on a
|
||||||
|
// displaced mesh.
|
||||||
|
//
|
||||||
|
// Crease quadrics: an interior edge sharper than the threshold gets penalty planes at both endpoints,
|
||||||
|
// perpendicular to each adjacent face and through the edge, weighted so such edges collapse last or
|
||||||
|
// not at all. A texture's hard step keeps its geometry while the flat ground around it reduces.
|
||||||
|
//
|
||||||
|
// Flat-face harvesting: the loop keeps going past the triangle target while each collapse's error
|
||||||
|
// stays under an absolute bound, so flat faces that cost nothing to remove are not left behind.
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <functional>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "TextureBakeIndex.hpp"
|
||||||
|
|
||||||
|
namespace Slic3r {
|
||||||
|
namespace TextureBake {
|
||||||
|
|
||||||
|
// Reject a collapse deviating more than about 78 degrees from the old face normal.
|
||||||
|
static constexpr double DECIMATE_FLIP_DOT = 0.2;
|
||||||
|
// Edges sharper than 60 degrees are treated as creases.
|
||||||
|
static constexpr double DECIMATE_CREASE_COS = 0.5;
|
||||||
|
// Quadric penalty weight for a crease plane.
|
||||||
|
static constexpr double DECIMATE_CREASE_WEIGHT = 1e4;
|
||||||
|
|
||||||
|
// Upper bound in mm on the deviation a harvested collapse may introduce; the real one is smaller,
|
||||||
|
// since the cost sums squared distances over all incident faces.
|
||||||
|
//
|
||||||
|
// Absolute, not relative to the cost at which the target was crossed. A relative band fails in the
|
||||||
|
// case with the most to shed: when the target is reached with a large flat surplus left, the crossing
|
||||||
|
// cost is essentially zero, so the band is too and nothing is harvested.
|
||||||
|
static constexpr double DECIMATE_DEFAULT_HARVEST_TOL = 0.005;
|
||||||
|
|
||||||
|
// Returns false to cancel.
|
||||||
|
using DecimateProgressFn = std::function<bool(double fraction)>;
|
||||||
|
|
||||||
|
struct DecimateResult
|
||||||
|
{
|
||||||
|
TriSoup geometry;
|
||||||
|
// The locked faces alone met the target, so it was unreachable without touching preserved
|
||||||
|
// geometry.
|
||||||
|
bool locked_over_budget = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// `locked_faces`: one entry per input triangle; a vertex touching one may neither move nor be
|
||||||
|
// removed, which also pins the ring between the two regions.
|
||||||
|
DecimateResult decimate(const TriSoup &geometry, size_t target_triangles, bool harvest_flat = true,
|
||||||
|
double harvest_tol = DECIMATE_DEFAULT_HARVEST_TOL,
|
||||||
|
const std::vector<uint8_t> &locked_faces = {},
|
||||||
|
const DecimateProgressFn &on_progress = {});
|
||||||
|
|
||||||
|
} // namespace TextureBake
|
||||||
|
} // namespace Slic3r
|
||||||
268
src/libslic3r/TextureBake/TextureBakeDisplace.cpp
Normal file
268
src/libslic3r/TextureBake/TextureBakeDisplace.cpp
Normal file
@@ -0,0 +1,268 @@
|
|||||||
|
#include "TextureBakeDisplace.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
#include <limits>
|
||||||
|
|
||||||
|
namespace Slic3r {
|
||||||
|
namespace TextureBake {
|
||||||
|
|
||||||
|
TriSoup apply_displacement(const TriSoup &geometry, const HeightSampleFn &sample,
|
||||||
|
const DisplaceSettings &settings, const DisplaceBounds &bounds,
|
||||||
|
const DisplaceProgressFn &on_progress)
|
||||||
|
{
|
||||||
|
TriSoup out;
|
||||||
|
const size_t count = geometry.pos.size();
|
||||||
|
if (count == 0 || !sample)
|
||||||
|
return geometry;
|
||||||
|
|
||||||
|
out.pos.resize(count);
|
||||||
|
out.nrm.resize(count);
|
||||||
|
|
||||||
|
// Everything below is keyed by this id, which is what makes one vector per position expressible.
|
||||||
|
const bool need_id_positions = settings.boundary_falloff > 0.f;
|
||||||
|
QuantizedPointMap dedup(WELD_GRID_GEOMETRY, std::min(count, size_t(1) << 22));
|
||||||
|
std::vector<int> vertex_id(count);
|
||||||
|
std::vector<Vec3f> id_pos;
|
||||||
|
int next_id = 0;
|
||||||
|
for (size_t i = 0; i < count; ++i) {
|
||||||
|
const int id = dedup.get_or_set(geometry.pos[i], next_id);
|
||||||
|
if (dedup.inserted()) {
|
||||||
|
++next_id;
|
||||||
|
if (need_id_positions)
|
||||||
|
id_pos.push_back(geometry.pos[i]);
|
||||||
|
}
|
||||||
|
vertex_id[i] = id;
|
||||||
|
}
|
||||||
|
const size_t unique_count = size_t(next_id);
|
||||||
|
|
||||||
|
// Pass 1: area-weighted smooth normals per position, plus what masking and falloff need.
|
||||||
|
std::vector<Vec3d> smooth_nrm(unique_count, Vec3d::Zero());
|
||||||
|
std::vector<double> masked_area(unique_count, 0.0), total_area(unique_count, 0.0);
|
||||||
|
const bool have_weights = !geometry.exclude_weight.empty();
|
||||||
|
std::vector<uint8_t> user_excluded_face(have_weights ? count / 3 : 0, 0);
|
||||||
|
std::vector<uint8_t> excluded_pos(have_weights ? unique_count : 0, 0);
|
||||||
|
|
||||||
|
for (size_t t = 0; t + 2 < count; t += 3) {
|
||||||
|
const Vec3d a = geometry.pos[t].cast<double>();
|
||||||
|
const Vec3d face_n = (geometry.pos[t + 1].cast<double>() - a).cross(geometry.pos[t + 2].cast<double>() - a);
|
||||||
|
const double face_area = face_n.norm(); // twice the triangle area, so weighting is natural
|
||||||
|
const double nz = face_area > 1e-12 ? face_n.z() / face_area : 0.0;
|
||||||
|
const double face_angle = std::acos(std::min(1.0, std::abs(nz))) * (180.0 / M_PI);
|
||||||
|
const bool angle_masked =
|
||||||
|
nz < 0.0 ? (settings.bottom_angle_limit > 0.f && face_angle <= settings.bottom_angle_limit)
|
||||||
|
: (settings.top_angle_limit > 0.f && face_angle <= settings.top_angle_limit);
|
||||||
|
|
||||||
|
// Thresholded high, not at a half: merging by maximum leaves a face bordering an excluded one
|
||||||
|
// with two corners at 1.0, averaging about 0.67, which a half threshold would misread.
|
||||||
|
bool user_excluded = false;
|
||||||
|
if (have_weights) {
|
||||||
|
const float avg = (geometry.exclude_weight[t] + geometry.exclude_weight[t + 1] +
|
||||||
|
geometry.exclude_weight[t + 2]) / 3.f;
|
||||||
|
user_excluded = avg > 0.99f;
|
||||||
|
if (user_excluded)
|
||||||
|
user_excluded_face[t / 3] = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int v = 0; v < 3; ++v) {
|
||||||
|
const size_t vid = size_t(vertex_id[t + size_t(v)]);
|
||||||
|
if (user_excluded && have_weights)
|
||||||
|
excluded_pos[vid] = 1;
|
||||||
|
// Subdivision split vertices at sharp edges, so these are smooth across soft edges and
|
||||||
|
// sharp across hard ones - no faceting on round surfaces, no rounding of corners.
|
||||||
|
smooth_nrm[vid] += geometry.nrm[t + size_t(v)].cast<double>() * face_area;
|
||||||
|
if (angle_masked)
|
||||||
|
masked_area[vid] += face_area;
|
||||||
|
total_area[vid] += face_area;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The pre-normalisation magnitude over the total area says how much the neighbouring faces agree:
|
||||||
|
// near 1 they do, near 0 they cancelled, meaning a knife edge with no usable surface direction.
|
||||||
|
std::vector<double> reliability(unique_count, 0.0);
|
||||||
|
for (size_t id = 0; id < unique_count; ++id) {
|
||||||
|
const double len = smooth_nrm[id].norm();
|
||||||
|
reliability[id] = (len > 0.0 && total_area[id] > 0.0) ? len / total_area[id] : 0.0;
|
||||||
|
smooth_nrm[id] = (len > 0.0) ? Vec3d(smooth_nrm[id] / len) : Vec3d(0.0, 0.0, 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pass 1.5: the smoothed blend normal - see the header for why it is separate.
|
||||||
|
std::vector<Vec3d> blend_nrm = smooth_nrm;
|
||||||
|
if (settings.blend_normal_smoothing > 0 && unique_count > 0) {
|
||||||
|
// CSR adjacency over the welded graph, deliberately a multigraph: duplicates weight a pair by
|
||||||
|
// how often it shares an edge, so a well-connected surface couples more strongly.
|
||||||
|
std::vector<uint32_t> degree(unique_count, 0);
|
||||||
|
const auto add_degree = [&](int a, int b) {
|
||||||
|
if (a != b) { ++degree[size_t(a)]; ++degree[size_t(b)]; }
|
||||||
|
};
|
||||||
|
for (size_t t = 0; t + 2 < count; t += 3) {
|
||||||
|
const int a = vertex_id[t], b = vertex_id[t + 1], c = vertex_id[t + 2];
|
||||||
|
add_degree(a, b); add_degree(b, c); add_degree(c, a);
|
||||||
|
}
|
||||||
|
std::vector<uint32_t> csr_start(unique_count + 1, 0);
|
||||||
|
for (size_t id = 0; id < unique_count; ++id)
|
||||||
|
csr_start[id + 1] = csr_start[id] + degree[id];
|
||||||
|
std::vector<uint32_t> neighbors(csr_start[unique_count]);
|
||||||
|
std::vector<uint32_t> cursor(unique_count, 0);
|
||||||
|
const auto add_edge = [&](int a, int b) {
|
||||||
|
if (a == b)
|
||||||
|
return;
|
||||||
|
neighbors[csr_start[size_t(a)] + cursor[size_t(a)]++] = uint32_t(b);
|
||||||
|
neighbors[csr_start[size_t(b)] + cursor[size_t(b)]++] = uint32_t(a);
|
||||||
|
};
|
||||||
|
for (size_t t = 0; t + 2 < count; t += 3) {
|
||||||
|
const int a = vertex_id[t], b = vertex_id[t + 1], c = vertex_id[t + 2];
|
||||||
|
add_edge(a, b); add_edge(b, c); add_edge(c, a);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<Vec3d> cur = smooth_nrm, nxt(unique_count, Vec3d::Zero());
|
||||||
|
for (int iter = 0; iter < settings.blend_normal_smoothing; ++iter) {
|
||||||
|
for (size_t id = 0; id < unique_count; ++id) {
|
||||||
|
const uint32_t s = csr_start[id], e = csr_start[id + 1];
|
||||||
|
if (e == s) {
|
||||||
|
nxt[id] = cur[id];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Vec3d sum = Vec3d::Zero();
|
||||||
|
for (uint32_t k = s; k < e; ++k)
|
||||||
|
sum += cur[neighbors[k]];
|
||||||
|
sum /= double(e - s);
|
||||||
|
const double len = sum.norm();
|
||||||
|
// Cancelling neighbours mean a knife edge; keep what we had.
|
||||||
|
nxt[id] = (len > 1e-12) ? Vec3d(sum / len) : cur[id];
|
||||||
|
}
|
||||||
|
cur.swap(nxt);
|
||||||
|
}
|
||||||
|
blend_nrm = std::move(cur);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A boundary position borders both masked and unmasked faces, or sits on the exclusion seam.
|
||||||
|
// Every other position gets its distance to the nearest one, ramped to 1 at the falloff distance.
|
||||||
|
std::vector<double> falloff;
|
||||||
|
if (settings.boundary_falloff > 0.f && unique_count > 0) {
|
||||||
|
std::vector<Vec3f> boundary;
|
||||||
|
for (size_t id = 0; id < unique_count; ++id) {
|
||||||
|
const double frac = total_area[id] > 0.0 ? masked_area[id] / total_area[id] : 0.0;
|
||||||
|
const bool on_excl = !excluded_pos.empty() && excluded_pos[id] != 0;
|
||||||
|
if (on_excl || (frac > 0.0 && frac < 1.0))
|
||||||
|
boundary.push_back(id_pos[id]);
|
||||||
|
}
|
||||||
|
falloff.assign(unique_count, 1.0);
|
||||||
|
if (!boundary.empty()) {
|
||||||
|
// A uniform grid: the query is nearest-point only, so a tree costs more than it saves.
|
||||||
|
Vec3f lo = boundary.front(), hi = boundary.front();
|
||||||
|
for (const Vec3f &p : boundary) {
|
||||||
|
lo = lo.cwiseMin(p);
|
||||||
|
hi = hi.cwiseMax(p);
|
||||||
|
}
|
||||||
|
const Vec3f span = (hi - lo).cwiseMax(Vec3f(1e-6f, 1e-6f, 1e-6f));
|
||||||
|
const int res = std::clamp(int(std::ceil(std::cbrt(double(boundary.size())) * 2.0)), 4, 128);
|
||||||
|
const Vec3f cell = span / float(res);
|
||||||
|
const float cell_min = cell.minCoeff();
|
||||||
|
const auto cell_of = [&](const Vec3f &p) {
|
||||||
|
Vec3i32 c;
|
||||||
|
for (int k = 0; k < 3; ++k)
|
||||||
|
c[k] = std::clamp(int((p[k] - lo[k]) / span[k] * float(res)), 0, res - 1);
|
||||||
|
return c;
|
||||||
|
};
|
||||||
|
const auto cell_index = [&](int x, int y, int z) {
|
||||||
|
return size_t(z) * size_t(res) * size_t(res) + size_t(y) * size_t(res) + size_t(x);
|
||||||
|
};
|
||||||
|
std::vector<std::vector<int>> grid(size_t(res) * size_t(res) * size_t(res));
|
||||||
|
for (size_t i = 0; i < boundary.size(); ++i) {
|
||||||
|
const Vec3i32 c = cell_of(boundary[i]);
|
||||||
|
grid[cell_index(c.x(), c.y(), c.z())].push_back(int(i));
|
||||||
|
}
|
||||||
|
|
||||||
|
const double radius = double(settings.boundary_falloff);
|
||||||
|
for (size_t id = 0; id < unique_count; ++id) {
|
||||||
|
const Vec3f &p = id_pos[id];
|
||||||
|
const Vec3i32 c = cell_of(p);
|
||||||
|
double best = std::numeric_limits<double>::max();
|
||||||
|
// Anything in shell r is at least (r - 1) cells away, so once the best found is within
|
||||||
|
// that bound nothing closer can be hiding further out.
|
||||||
|
for (int r = 0; r < res; ++r) {
|
||||||
|
for (int dz = -r; dz <= r; ++dz)
|
||||||
|
for (int dy = -r; dy <= r; ++dy)
|
||||||
|
for (int dx = -r; dx <= r; ++dx) {
|
||||||
|
// The shell only; its interior was covered by a smaller r.
|
||||||
|
if (r > 0 && std::abs(dx) != r && std::abs(dy) != r && std::abs(dz) != r)
|
||||||
|
continue;
|
||||||
|
const int qx = c.x() + dx, qy = c.y() + dy, qz = c.z() + dz;
|
||||||
|
if (qx < 0 || qy < 0 || qz < 0 || qx >= res || qy >= res || qz >= res)
|
||||||
|
continue;
|
||||||
|
for (const int bi : grid[cell_index(qx, qy, qz)])
|
||||||
|
best = std::min(best, double((boundary[size_t(bi)] - p).norm()));
|
||||||
|
}
|
||||||
|
if (best <= double(r) * double(cell_min))
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
falloff[id] = (best == std::numeric_limits<double>::max() || radius <= 0.0)
|
||||||
|
? 1.0
|
||||||
|
: std::clamp(best / radius, 0.0, 1.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pass 2: one sample per unique position.
|
||||||
|
std::vector<double> grey(unique_count, 0.0);
|
||||||
|
std::vector<uint8_t> grey_set(unique_count, 0);
|
||||||
|
for (size_t i = 0; i < count; ++i) {
|
||||||
|
const size_t vid = size_t(vertex_id[i]);
|
||||||
|
if (grey_set[vid])
|
||||||
|
continue;
|
||||||
|
grey_set[vid] = 1;
|
||||||
|
grey[vid] = double(sample(geometry.pos[i], smooth_nrm[vid].cast<float>(),
|
||||||
|
blend_nrm[vid].cast<float>()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pass 3: move every copy of a position by the identical vector.
|
||||||
|
for (size_t i = 0; i < count; ++i) {
|
||||||
|
const Vec3f &p = geometry.pos[i];
|
||||||
|
const size_t vid = size_t(vertex_id[i]);
|
||||||
|
|
||||||
|
// Only angle masking uses the per-position blend, so an excluded face never dims its
|
||||||
|
// neighbours through a shared vertex.
|
||||||
|
const bool face_excluded = !user_excluded_face.empty() && user_excluded_face[i / 3] != 0;
|
||||||
|
// Pinned where an included face shares a position with an excluded one, sealing the boundary.
|
||||||
|
const bool sealed_boundary =
|
||||||
|
!face_excluded && !excluded_pos.empty() && excluded_pos[vid] != 0;
|
||||||
|
const double masked_frac = total_area[vid] > 0.0 ? masked_area[vid] / total_area[vid] : 0.0;
|
||||||
|
const double centered = settings.symmetric ? (grey[vid] - 0.5) : grey[vid];
|
||||||
|
const double ramp = falloff.empty() ? 1.0 : falloff[vid];
|
||||||
|
const double disp = (face_excluded || sealed_boundary)
|
||||||
|
? 0.0
|
||||||
|
: ramp * (1.0 - masked_frac) * centered * double(settings.amplitude);
|
||||||
|
|
||||||
|
Vec3d moved = p.cast<double>() + smooth_nrm[vid] * disp;
|
||||||
|
|
||||||
|
// Stop a partly masked vertex poking through the surface it borders.
|
||||||
|
if (masked_frac > 0.0) {
|
||||||
|
if (settings.bottom_angle_limit > 0.f && moved.z() < double(p.z())) moved.z() = double(p.z());
|
||||||
|
if (settings.top_angle_limit > 0.f && moved.z() > double(p.z())) moved.z() = double(p.z());
|
||||||
|
}
|
||||||
|
if (settings.no_downward_z && moved.z() < double(p.z()))
|
||||||
|
moved.z() = double(p.z());
|
||||||
|
// A vertex starting on the bottom plane stays there: otherwise a downward-facing face pulls
|
||||||
|
// *up* where the sample is below mid-grey, leaving bed-contact vertices at differing heights.
|
||||||
|
if (settings.no_downward_z && double(p.z()) <= double(bounds.min.z()) + 1e-5)
|
||||||
|
moved.z() = double(p.z());
|
||||||
|
|
||||||
|
out.pos[i] = moved.cast<float>();
|
||||||
|
|
||||||
|
if (on_progress && (i % 5000) == 0 && !on_progress(double(i) / double(count)))
|
||||||
|
return geometry; // cancelled: hand back the input untouched
|
||||||
|
}
|
||||||
|
|
||||||
|
// Per-face, not averaged across shared positions: averaging can flip an excluded face's normal
|
||||||
|
// when its neighbours moved outward.
|
||||||
|
for (size_t t = 0; t + 2 < count; t += 3) {
|
||||||
|
const Vec3f n = (out.pos[t + 1] - out.pos[t]).cross(out.pos[t + 2] - out.pos[t]).normalized();
|
||||||
|
out.nrm[t] = out.nrm[t + 1] = out.nrm[t + 2] = n;
|
||||||
|
}
|
||||||
|
out.exclude_weight = geometry.exclude_weight;
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace TextureBake
|
||||||
|
} // namespace Slic3r
|
||||||
68
src/libslic3r/TextureBake/TextureBakeDisplace.hpp
Normal file
68
src/libslic3r/TextureBake/TextureBakeDisplace.hpp
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
// Displacement along surface normals.
|
||||||
|
//
|
||||||
|
// The mesh is non-indexed, so at a shared edge two triangles hold the same position with different
|
||||||
|
// face normals; displacing each copy along its own normal sends them to different points and opens a
|
||||||
|
// crack. So one smooth (area-weighted) normal per unique position drives both the sample lookup and
|
||||||
|
// the displacement direction, every copy moves by the same vector, and the result is watertight by
|
||||||
|
// construction. Displaced normals are then smooth at hard edges, but the geometry is still faceted,
|
||||||
|
// so printed edges stay sharp.
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <functional>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "TextureBakeIndex.hpp"
|
||||||
|
|
||||||
|
namespace Slic3r {
|
||||||
|
namespace TextureBake {
|
||||||
|
|
||||||
|
// Height at a point, called once per unique welded position. `smooth_normal` is the vector the
|
||||||
|
// displacement will move along; `blend_normal` is that after smoothing, for projection blend weights.
|
||||||
|
using HeightSampleFn = std::function<float(const Vec3f &position, const Vec3f &smooth_normal,
|
||||||
|
const Vec3f &blend_normal)>;
|
||||||
|
|
||||||
|
struct DisplaceSettings
|
||||||
|
{
|
||||||
|
// Displacement height in mm, applied to the sampled value.
|
||||||
|
float amplitude = 0.4f;
|
||||||
|
|
||||||
|
// Sample around a mid-grey rest level rather than displacing outward only.
|
||||||
|
bool symmetric = false;
|
||||||
|
|
||||||
|
// Faces flatter than these (degrees from horizontal) are held back, leaving bed-contact and top
|
||||||
|
// surfaces alone. 0 disables that side.
|
||||||
|
float bottom_angle_limit = 5.f;
|
||||||
|
float top_angle_limit = 0.f;
|
||||||
|
|
||||||
|
// Never move a vertex below its original Z, so no new overhang. The sideways component is kept.
|
||||||
|
bool no_downward_z = false;
|
||||||
|
|
||||||
|
// Distance in mm over which displacement ramps up from a mask boundary. 0 leaves a hard edge.
|
||||||
|
float boundary_falloff = 0.f;
|
||||||
|
|
||||||
|
// Laplacian iterations on the blend normal only - the displacement direction must stay the exact
|
||||||
|
// smooth normal or copies of a position move differently and the mesh cracks. Inside a blend band
|
||||||
|
// the weight gradient is largest, so a few degrees of vertex-to-vertex jitter multiplies the
|
||||||
|
// difference between two unrelated height samples into visible seam noise. A no-op on an
|
||||||
|
// already-smooth surface.
|
||||||
|
int blend_normal_smoothing = 32;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Model extents; only the minimum Z is read, for the bottom-plane clamp.
|
||||||
|
struct DisplaceBounds
|
||||||
|
{
|
||||||
|
Vec3f min = Vec3f::Zero();
|
||||||
|
Vec3f max = Vec3f::Zero();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Returns false to cancel.
|
||||||
|
using DisplaceProgressFn = std::function<bool(double fraction)>;
|
||||||
|
|
||||||
|
TriSoup apply_displacement(const TriSoup &geometry, const HeightSampleFn &sample,
|
||||||
|
const DisplaceSettings &settings, const DisplaceBounds &bounds,
|
||||||
|
const DisplaceProgressFn &on_progress = {});
|
||||||
|
|
||||||
|
} // namespace TextureBake
|
||||||
|
} // namespace Slic3r
|
||||||
25
src/libslic3r/TextureBake/TextureBakeIndex.cpp
Normal file
25
src/libslic3r/TextureBake/TextureBakeIndex.cpp
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
#include "TextureBakeIndex.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
namespace Slic3r {
|
||||||
|
namespace TextureBake {
|
||||||
|
|
||||||
|
WeldResult weld_vertices(const std::vector<Vec3f> &positions, double quant)
|
||||||
|
{
|
||||||
|
WeldResult out;
|
||||||
|
QuantizedPointMap map(quant, std::min<size_t>(positions.size(), size_t(1) << 22));
|
||||||
|
out.vertex_id.resize(positions.size());
|
||||||
|
int next_id = 0;
|
||||||
|
for (size_t i = 0; i < positions.size(); ++i) {
|
||||||
|
const int id = map.get_or_set(positions[i], next_id);
|
||||||
|
if (map.inserted())
|
||||||
|
++next_id;
|
||||||
|
out.vertex_id[i] = id;
|
||||||
|
}
|
||||||
|
out.unique_count = next_id;
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace TextureBake
|
||||||
|
} // namespace Slic3r
|
||||||
164
src/libslic3r/TextureBake/TextureBakeIndex.hpp
Normal file
164
src/libslic3r/TextureBake/TextureBakeIndex.hpp
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
// Vertex welding for the texture bake pipeline. The pipeline works on non-indexed triangle soup, so
|
||||||
|
// a shared point exists once per incident triangle with float noise between the copies; welding maps
|
||||||
|
// each quantised position to one integer id.
|
||||||
|
//
|
||||||
|
// The three grids below are deliberately not unified - changing one at a call site changes
|
||||||
|
// watertightness. 100 um matches the precision files are written with; 10 um keeps small fillet
|
||||||
|
// vertices distinct (they merge at 100 um, giving needle artifacts after displacement) while still
|
||||||
|
// absorbing float noise; 1 um is what collapse positioning needs.
|
||||||
|
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "../Point.hpp"
|
||||||
|
|
||||||
|
namespace Slic3r {
|
||||||
|
namespace TextureBake {
|
||||||
|
|
||||||
|
static constexpr double WELD_GRID_EXPORT = 1e4; // 100 um
|
||||||
|
static constexpr double WELD_GRID_GEOMETRY = 1e5; // 10 um
|
||||||
|
static constexpr double WELD_GRID_DECIMATION = 1e6; // 1 um
|
||||||
|
|
||||||
|
// Round half toward positive infinity. Quantised coordinates hit exact halves often enough that the
|
||||||
|
// tie rule matters.
|
||||||
|
inline int64_t grid_round(double v) { return int64_t(std::floor(v + 0.5)); }
|
||||||
|
|
||||||
|
// Open-addressing table over flat arrays: no allocation per lookup, exact integer key comparison.
|
||||||
|
// Values must be non-negative; -1 is the empty sentinel and what get() returns on a miss.
|
||||||
|
class QuantizedPointMap
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit QuantizedPointMap(double quant, size_t expected = 256) : m_quant(quant)
|
||||||
|
{
|
||||||
|
size_t cap = 16;
|
||||||
|
const size_t target = std::max<size_t>(16, size_t(std::ceil(double(expected) / 0.6)));
|
||||||
|
while (cap < target)
|
||||||
|
cap *= 2;
|
||||||
|
alloc(cap);
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t size() const { return m_size; }
|
||||||
|
// Whether the last get_or_set() inserted rather than found.
|
||||||
|
bool inserted() const { return m_inserted; }
|
||||||
|
|
||||||
|
int get(float x, float y, float z)
|
||||||
|
{
|
||||||
|
return m_val[slot(grid_round(double(x) * m_quant), grid_round(double(y) * m_quant),
|
||||||
|
grid_round(double(z) * m_quant))];
|
||||||
|
}
|
||||||
|
int get(const Vec3f &p) { return get(p.x(), p.y(), p.z()); }
|
||||||
|
|
||||||
|
// The value already stored for this position's grid cell; if there is none, store `value` and
|
||||||
|
// return it. inserted() then says which of the two happened.
|
||||||
|
int get_or_set(float x, float y, float z, int value)
|
||||||
|
{
|
||||||
|
const int64_t qx = grid_round(double(x) * m_quant);
|
||||||
|
const int64_t qy = grid_round(double(y) * m_quant);
|
||||||
|
const int64_t qz = grid_round(double(z) * m_quant);
|
||||||
|
const size_t i = slot(qx, qy, qz);
|
||||||
|
if (m_val[i] != -1) {
|
||||||
|
m_inserted = false;
|
||||||
|
return m_val[i];
|
||||||
|
}
|
||||||
|
m_qx[i] = qx; m_qy[i] = qy; m_qz[i] = qz;
|
||||||
|
m_val[i] = value;
|
||||||
|
m_inserted = true;
|
||||||
|
if (++m_size > size_t(double(m_cap) * 0.7))
|
||||||
|
grow();
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
int get_or_set(const Vec3f &p, int value) { return get_or_set(p.x(), p.y(), p.z(), value); }
|
||||||
|
|
||||||
|
// The same table as a set of integer tuples (edge marking, midpoint cache). Quantisation is
|
||||||
|
// bypassed: routing ids through the float overloads loses precision above 2^24.
|
||||||
|
int get_key(int64_t a, int64_t b, int64_t c) { return m_val[slot(a, b, c)]; }
|
||||||
|
int get_or_set_key(int64_t a, int64_t b, int64_t c, int value)
|
||||||
|
{
|
||||||
|
const size_t i = slot(a, b, c);
|
||||||
|
if (m_val[i] != -1) {
|
||||||
|
m_inserted = false;
|
||||||
|
return m_val[i];
|
||||||
|
}
|
||||||
|
m_qx[i] = a; m_qy[i] = b; m_qz[i] = c;
|
||||||
|
m_val[i] = value;
|
||||||
|
m_inserted = true;
|
||||||
|
if (++m_size > size_t(double(m_cap) * 0.7))
|
||||||
|
grow();
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
void alloc(size_t cap)
|
||||||
|
{
|
||||||
|
m_cap = cap;
|
||||||
|
m_mask = cap - 1;
|
||||||
|
m_qx.assign(cap, 0);
|
||||||
|
m_qy.assign(cap, 0);
|
||||||
|
m_qz.assign(cap, 0);
|
||||||
|
m_val.assign(cap, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t slot(int64_t qx, int64_t qy, int64_t qz) const
|
||||||
|
{
|
||||||
|
uint32_t h = uint32_t(int32_t(qx) * int32_t(0x9E3779B1)) ^
|
||||||
|
uint32_t(int32_t(qy) * int32_t(0x85EBCA77)) ^
|
||||||
|
uint32_t(int32_t(qz) * int32_t(0xC2B2AE3D));
|
||||||
|
h ^= h >> 15;
|
||||||
|
size_t i = size_t(h) & m_mask;
|
||||||
|
// Equality is checked against the stored 64-bit keys, so truncating to 32 bits for the hash
|
||||||
|
// costs collisions at worst, never a wrong answer.
|
||||||
|
while (m_val[i] != -1) {
|
||||||
|
if (m_qx[i] == qx && m_qy[i] == qy && m_qz[i] == qz)
|
||||||
|
return i;
|
||||||
|
i = (i + 1) & m_mask;
|
||||||
|
}
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
|
||||||
|
void grow()
|
||||||
|
{
|
||||||
|
std::vector<int64_t> oqx = std::move(m_qx), oqy = std::move(m_qy), oqz = std::move(m_qz);
|
||||||
|
std::vector<int> oval = std::move(m_val);
|
||||||
|
const size_t ocap = m_cap;
|
||||||
|
alloc(ocap * 2);
|
||||||
|
for (size_t i = 0; i < ocap; ++i) {
|
||||||
|
if (oval[i] == -1)
|
||||||
|
continue;
|
||||||
|
const size_t s = slot(oqx[i], oqy[i], oqz[i]);
|
||||||
|
m_qx[s] = oqx[i]; m_qy[s] = oqy[i]; m_qz[s] = oqz[i];
|
||||||
|
m_val[s] = oval[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
double m_quant;
|
||||||
|
size_t m_cap = 0, m_mask = 0, m_size = 0;
|
||||||
|
bool m_inserted = false;
|
||||||
|
std::vector<int64_t> m_qx, m_qy, m_qz;
|
||||||
|
std::vector<int> m_val;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Three consecutive entries per triangle. The indexers turn this into shared vertices where a stage
|
||||||
|
// needs adjacency.
|
||||||
|
struct TriSoup
|
||||||
|
{
|
||||||
|
std::vector<Vec3f> pos;
|
||||||
|
std::vector<Vec3f> nrm; // parallel to pos
|
||||||
|
std::vector<float> exclude_weight; // parallel to pos; empty when nothing is excluded
|
||||||
|
|
||||||
|
size_t triangle_count() const { return pos.size() / 3; }
|
||||||
|
bool empty() const { return pos.empty(); }
|
||||||
|
};
|
||||||
|
|
||||||
|
// Assign each vertex the sequential id of its quantised position, first occurrence winning.
|
||||||
|
struct WeldResult
|
||||||
|
{
|
||||||
|
std::vector<int> vertex_id;
|
||||||
|
int unique_count = 0;
|
||||||
|
};
|
||||||
|
WeldResult weld_vertices(const std::vector<Vec3f> &positions, double quant);
|
||||||
|
|
||||||
|
} // namespace TextureBake
|
||||||
|
} // namespace Slic3r
|
||||||
62
src/libslic3r/TextureBake/TextureBakeMesh.cpp
Normal file
62
src/libslic3r/TextureBake/TextureBakeMesh.cpp
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
#include "TextureBakeMesh.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
namespace Slic3r {
|
||||||
|
namespace TextureBake {
|
||||||
|
|
||||||
|
TriSoup to_soup(const indexed_triangle_set &its, const std::vector<uint8_t> &face_excluded)
|
||||||
|
{
|
||||||
|
TriSoup out;
|
||||||
|
const size_t n = its.indices.size();
|
||||||
|
out.pos.resize(n * 3);
|
||||||
|
out.nrm.resize(n * 3);
|
||||||
|
const bool have_excl = face_excluded.size() == n;
|
||||||
|
if (have_excl)
|
||||||
|
out.exclude_weight.resize(n * 3);
|
||||||
|
|
||||||
|
for (size_t t = 0; t < n; ++t) {
|
||||||
|
const stl_triangle_vertex_indices &tri = its.indices[t];
|
||||||
|
const Vec3f a = its.vertices[size_t(tri[0])];
|
||||||
|
const Vec3f b = its.vertices[size_t(tri[1])];
|
||||||
|
const Vec3f c = its.vertices[size_t(tri[2])];
|
||||||
|
Vec3f nrm = (b - a).cross(c - a);
|
||||||
|
const float len = nrm.norm();
|
||||||
|
nrm = (len > 0.f) ? Vec3f(nrm / len) : Vec3f(0.f, 0.f, 1.f);
|
||||||
|
out.pos[t * 3] = a;
|
||||||
|
out.pos[t * 3 + 1] = b;
|
||||||
|
out.pos[t * 3 + 2] = c;
|
||||||
|
// Per-face on purpose: the accurate indexer derives smooth normals and splits at sharp edges
|
||||||
|
// itself, so averaged ones would pre-empt that.
|
||||||
|
out.nrm[t * 3] = out.nrm[t * 3 + 1] = out.nrm[t * 3 + 2] = nrm;
|
||||||
|
if (have_excl) {
|
||||||
|
const float w = face_excluded[t] ? 1.f : 0.f;
|
||||||
|
out.exclude_weight[t * 3] = out.exclude_weight[t * 3 + 1] = out.exclude_weight[t * 3 + 2] = w;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
indexed_triangle_set to_indexed_triangle_set(const TriSoup &soup)
|
||||||
|
{
|
||||||
|
indexed_triangle_set out;
|
||||||
|
const size_t n = soup.pos.size();
|
||||||
|
out.indices.reserve(n / 3);
|
||||||
|
QuantizedPointMap map(WELD_GRID_GEOMETRY, std::min(n, size_t(1) << 22));
|
||||||
|
std::vector<int> id(n);
|
||||||
|
for (size_t i = 0; i < n; ++i) {
|
||||||
|
id[i] = map.get_or_set(soup.pos[i], int(out.vertices.size()));
|
||||||
|
if (map.inserted())
|
||||||
|
out.vertices.push_back(soup.pos[i]);
|
||||||
|
}
|
||||||
|
for (size_t t = 0; t + 2 < n; t += 3) {
|
||||||
|
// Welded-together corners carry no area.
|
||||||
|
if (id[t] == id[t + 1] || id[t + 1] == id[t + 2] || id[t] == id[t + 2])
|
||||||
|
continue;
|
||||||
|
out.indices.emplace_back(id[t], id[t + 1], id[t + 2]);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace TextureBake
|
||||||
|
} // namespace Slic3r
|
||||||
19
src/libslic3r/TextureBake/TextureBakeMesh.hpp
Normal file
19
src/libslic3r/TextureBake/TextureBakeMesh.hpp
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
// Conversion between the pipeline's triangle soup and the indexed mesh used elsewhere. The pipeline
|
||||||
|
// stays on soup because each stage welds on its own grid, and those differences are load-bearing.
|
||||||
|
|
||||||
|
#include "TextureBakeIndex.hpp"
|
||||||
|
#include "../TriangleMesh.hpp"
|
||||||
|
|
||||||
|
namespace Slic3r {
|
||||||
|
namespace TextureBake {
|
||||||
|
|
||||||
|
// `face_excluded`: one entry per input triangle, becoming the soup's per-corner exclusion weight.
|
||||||
|
TriSoup to_soup(const indexed_triangle_set &its, const std::vector<uint8_t> &face_excluded = {});
|
||||||
|
|
||||||
|
// Welds at the geometry grid.
|
||||||
|
indexed_triangle_set to_indexed_triangle_set(const TriSoup &soup);
|
||||||
|
|
||||||
|
} // namespace TextureBake
|
||||||
|
} // namespace Slic3r
|
||||||
216
src/libslic3r/TextureBake/TextureBakePipeline.cpp
Normal file
216
src/libslic3r/TextureBake/TextureBakePipeline.cpp
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
#include "TextureBakePipeline.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
namespace Slic3r {
|
||||||
|
namespace TextureBake {
|
||||||
|
|
||||||
|
void clamp_below_bottom(TriSoup &geometry, float bottom_z)
|
||||||
|
{
|
||||||
|
for (size_t t = 0; t + 2 < geometry.pos.size(); t += 3) {
|
||||||
|
bool dirty = false;
|
||||||
|
for (int k = 0; k < 3; ++k)
|
||||||
|
if (geometry.pos[t + size_t(k)].z() < bottom_z) {
|
||||||
|
geometry.pos[t + size_t(k)].z() = bottom_z;
|
||||||
|
dirty = true;
|
||||||
|
}
|
||||||
|
if (!dirty)
|
||||||
|
continue;
|
||||||
|
Vec3f n = (geometry.pos[t + 1] - geometry.pos[t]).cross(geometry.pos[t + 2] - geometry.pos[t]);
|
||||||
|
const float len = n.norm();
|
||||||
|
n = (len > 0.f) ? Vec3f(n / len) : Vec3f(0.f, 0.f, 1.f);
|
||||||
|
geometry.nrm[t] = geometry.nrm[t + 1] = geometry.nrm[t + 2] = n;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t snap_bottom_to_flat(TriSoup &geometry, float bottom_z, double tol)
|
||||||
|
{
|
||||||
|
const size_t vert_count = geometry.pos.size();
|
||||||
|
const size_t tri_count = vert_count / 3;
|
||||||
|
if (tri_count == 0 || tol <= 0.0)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
// Weld at the finest grid: by this point copies of one position are bit-identical, because every
|
||||||
|
// earlier stage moved them by the same vector.
|
||||||
|
QuantizedPointMap weld(WELD_GRID_DECIMATION, std::min(vert_count, size_t(1) << 22));
|
||||||
|
std::vector<int> vid(vert_count);
|
||||||
|
int unique = 0;
|
||||||
|
for (size_t i = 0; i < vert_count; ++i) {
|
||||||
|
vid[i] = weld.get_or_set(geometry.pos[i], unique);
|
||||||
|
if (weld.inserted())
|
||||||
|
++unique;
|
||||||
|
}
|
||||||
|
// Incident corners per position, CSR style.
|
||||||
|
std::vector<uint32_t> start(size_t(unique) + 1, 0);
|
||||||
|
for (size_t i = 0; i < vert_count; ++i)
|
||||||
|
++start[size_t(vid[i]) + 1];
|
||||||
|
for (size_t id = 0; id < size_t(unique); ++id)
|
||||||
|
start[id + 1] += start[id];
|
||||||
|
std::vector<uint32_t> inc(vert_count), cursor(size_t(unique), 0);
|
||||||
|
for (size_t i = 0; i < vert_count; ++i)
|
||||||
|
inc[start[size_t(vid[i])] + cursor[size_t(vid[i])]++] = uint32_t(i);
|
||||||
|
|
||||||
|
const double fold_cos = std::cos(75.0 * M_PI / 180.0);
|
||||||
|
std::vector<uint8_t> dirty_tri(tri_count, 0);
|
||||||
|
|
||||||
|
for (size_t id = 0; id < size_t(unique); ++id) {
|
||||||
|
const float z = geometry.pos[inc[start[id]]].z();
|
||||||
|
if (z == bottom_z || std::abs(double(z) - double(bottom_z)) > tol)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Simulate the move: every incident triangle must keep positive area and must not fold.
|
||||||
|
bool ok = true;
|
||||||
|
for (uint32_t k = start[id]; k < start[id + 1] && ok; ++k) {
|
||||||
|
const size_t t = size_t(inc[k]) / 3;
|
||||||
|
Vec3f p[3];
|
||||||
|
for (int v = 0; v < 3; ++v) {
|
||||||
|
p[v] = geometry.pos[t * 3 + size_t(v)];
|
||||||
|
if (vid[t * 3 + size_t(v)] == int(id))
|
||||||
|
p[v].z() = bottom_z;
|
||||||
|
}
|
||||||
|
const Vec3d on = (geometry.pos[t * 3 + 1] - geometry.pos[t * 3])
|
||||||
|
.cross(geometry.pos[t * 3 + 2] - geometry.pos[t * 3]).cast<double>();
|
||||||
|
const Vec3d nn = (p[1] - p[0]).cross(p[2] - p[0]).cast<double>();
|
||||||
|
const double o2 = on.squaredNorm(), n2 = nn.squaredNorm();
|
||||||
|
if (n2 < 1e-20) { ok = false; break; } // would collapse to zero area
|
||||||
|
if (o2 < 1e-20) continue; // already degenerate, cannot judge a rotation
|
||||||
|
const double dot = on.dot(nn);
|
||||||
|
if (dot < 0.0 || dot * dot < fold_cos * fold_cos * o2 * n2)
|
||||||
|
ok = false;
|
||||||
|
}
|
||||||
|
if (!ok)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
for (uint32_t k = start[id]; k < start[id + 1]; ++k) {
|
||||||
|
geometry.pos[inc[k]].z() = bottom_z;
|
||||||
|
dirty_tri[size_t(inc[k]) / 3] = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t dirty = 0;
|
||||||
|
for (size_t t = 0; t < tri_count; ++t) {
|
||||||
|
if (!dirty_tri[t])
|
||||||
|
continue;
|
||||||
|
++dirty;
|
||||||
|
Vec3f n = (geometry.pos[t * 3 + 1] - geometry.pos[t * 3])
|
||||||
|
.cross(geometry.pos[t * 3 + 2] - geometry.pos[t * 3]);
|
||||||
|
const float len = n.norm();
|
||||||
|
n = (len > 0.f) ? Vec3f(n / len) : Vec3f(0.f, 0.f, 1.f);
|
||||||
|
geometry.nrm[t * 3] = geometry.nrm[t * 3 + 1] = geometry.nrm[t * 3 + 2] = n;
|
||||||
|
}
|
||||||
|
return dirty;
|
||||||
|
}
|
||||||
|
|
||||||
|
PipelineResult run_pipeline(const TriSoup &input, const HeightSampleFn &sample,
|
||||||
|
const PipelineSettings &settings, const DisplaceBounds &bounds,
|
||||||
|
PipelineMode mode, const std::vector<uint8_t> &face_excluded,
|
||||||
|
const PipelineProgressFn &on_progress)
|
||||||
|
{
|
||||||
|
PipelineResult result;
|
||||||
|
const auto report = [&](const char *stage, double f) {
|
||||||
|
return !on_progress || on_progress(stage, f);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (input.empty() || !sample) {
|
||||||
|
result.geometry = input;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Refine to the target edge length.
|
||||||
|
SubdivideResult sub = subdivide(
|
||||||
|
input, settings.refine_length, face_excluded, /* fast */ false, settings.safety_cap,
|
||||||
|
[&](double f, size_t, double) { return report("subdivide", f); });
|
||||||
|
result.safety_cap_hit = sub.safety_cap_hit;
|
||||||
|
if (!report("subdivide", 1.0)) {
|
||||||
|
result.canceled = true;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Dissolve the slivers refinement inherited, then recover the edges that lengthened.
|
||||||
|
if (settings.regularize) {
|
||||||
|
RegularizeOptions ropts = settings.regularize_opts;
|
||||||
|
ropts.preserve_excluded = settings.preserve_untextured;
|
||||||
|
RegularizeResult reg = regularize_mesh(sub.geometry, sub.face_parent_id,
|
||||||
|
settings.refine_length, ropts);
|
||||||
|
result.collapse_count = reg.collapse_count;
|
||||||
|
if (!report("regularize", 1.0)) {
|
||||||
|
result.canceled = true;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
if (reg.collapse_count > 0) {
|
||||||
|
// Excluded faces are carried on the soup itself, so the flag is re-derived rather than
|
||||||
|
// indexed across the collapse.
|
||||||
|
std::vector<uint8_t> excl;
|
||||||
|
if (!reg.geometry.exclude_weight.empty()) {
|
||||||
|
excl.assign(reg.geometry.triangle_count(), 0);
|
||||||
|
for (size_t t = 0; t < excl.size(); ++t)
|
||||||
|
excl[t] = reg.geometry.exclude_weight[t * 3] > 0.99f ? 1 : 0;
|
||||||
|
}
|
||||||
|
sub = subdivide(reg.geometry, settings.refine_length * settings.regularize_second_pass_mul,
|
||||||
|
excl, false, settings.safety_cap,
|
||||||
|
[&](double f, size_t, double) { return report("re-subdivide", f); });
|
||||||
|
result.safety_cap_hit = result.safety_cap_hit || sub.safety_cap_hit;
|
||||||
|
// The second pass renumbers faces, so the parent map has to be composed through it.
|
||||||
|
std::vector<int> composed(sub.face_parent_id.size());
|
||||||
|
for (size_t i = 0; i < composed.size(); ++i) {
|
||||||
|
const int mid = sub.face_parent_id[i];
|
||||||
|
composed[i] = (mid >= 0 && size_t(mid) < reg.face_parent_id.size())
|
||||||
|
? reg.face_parent_id[size_t(mid)] : -1;
|
||||||
|
}
|
||||||
|
sub.face_parent_id = std::move(composed);
|
||||||
|
} else {
|
||||||
|
sub.geometry = std::move(reg.geometry);
|
||||||
|
sub.face_parent_id = std::move(reg.face_parent_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Displace.
|
||||||
|
TriSoup displaced = apply_displacement(sub.geometry, sample, settings.displace, bounds,
|
||||||
|
[&](double f) { return report("displace", f); });
|
||||||
|
if (!report("displace", 1.0)) {
|
||||||
|
result.canceled = true;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Decimate - export only. A bake needs the face-parent map, which a collapse destroys.
|
||||||
|
std::vector<int> parent = std::move(sub.face_parent_id);
|
||||||
|
if (mode == PipelineMode::Export) {
|
||||||
|
const bool needs_decimation = displaced.triangle_count() > settings.max_triangles;
|
||||||
|
if (needs_decimation || settings.harvest_flat) {
|
||||||
|
std::vector<uint8_t> locked;
|
||||||
|
if (settings.preserve_untextured && !displaced.exclude_weight.empty()) {
|
||||||
|
locked.assign(displaced.triangle_count(), 0);
|
||||||
|
for (size_t t = 0; t < locked.size(); ++t)
|
||||||
|
locked[t] = displaced.exclude_weight[t * 3] > 0.99f ? 1 : 0;
|
||||||
|
}
|
||||||
|
DecimateResult dec = decimate(displaced, settings.max_triangles, settings.harvest_flat,
|
||||||
|
settings.harvest_tol, locked,
|
||||||
|
[&](double f) { return report("decimate", f); });
|
||||||
|
result.locked_over_budget = dec.locked_over_budget;
|
||||||
|
displaced = std::move(dec.geometry);
|
||||||
|
parent.clear(); // no longer meaningful
|
||||||
|
}
|
||||||
|
if (!report("decimate", 1.0)) {
|
||||||
|
result.canceled = true;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Flatten the bed-contact surface.
|
||||||
|
if (settings.displace.bottom_angle_limit > 0.f)
|
||||||
|
clamp_below_bottom(displaced, bounds.min.z());
|
||||||
|
if (settings.bottom_snap_tol > 0.0)
|
||||||
|
snap_bottom_to_flat(displaced, bounds.min.z(), settings.bottom_snap_tol);
|
||||||
|
|
||||||
|
// 6. Close the T-junctions decimation left behind. Only meaningful when it ran.
|
||||||
|
if (mode == PipelineMode::Export && parent.empty())
|
||||||
|
displaced = resolve_t_junctions(displaced);
|
||||||
|
|
||||||
|
result.geometry = std::move(displaced);
|
||||||
|
result.face_parent_id = std::move(parent);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace TextureBake
|
||||||
|
} // namespace Slic3r
|
||||||
98
src/libslic3r/TextureBake/TextureBakePipeline.hpp
Normal file
98
src/libslic3r/TextureBake/TextureBakePipeline.hpp
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
// The bake pipeline:
|
||||||
|
//
|
||||||
|
// subdivide -> [regularize -> re-subdivide] -> displace -> [decimate]
|
||||||
|
// -> bottom clamp -> bottom snap -> [resolve T-junctions]
|
||||||
|
//
|
||||||
|
// Regularization sits between two subdivisions on purpose: it dissolves the slivers refinement
|
||||||
|
// inherited, which lengthens some edges past the target, and the second pass brings those back.
|
||||||
|
// Before any subdivision it would have nothing to work on, since the slivers come from refining a
|
||||||
|
// needle; after a single pass it would leave the mesh coarser than asked for.
|
||||||
|
//
|
||||||
|
// Decimation and repair are export-only - decimation drops the output-to-input face mapping a bake
|
||||||
|
// needs to carry per-face data forward.
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <functional>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "TextureBakeDecimate.hpp"
|
||||||
|
#include "TextureBakeDisplace.hpp"
|
||||||
|
#include "TextureBakeIndex.hpp"
|
||||||
|
#include "TextureBakeRegularize.hpp"
|
||||||
|
#include "TextureBakeRepair.hpp"
|
||||||
|
#include "TextureBakeSubdivide.hpp"
|
||||||
|
|
||||||
|
namespace Slic3r {
|
||||||
|
namespace TextureBake {
|
||||||
|
|
||||||
|
enum class PipelineMode
|
||||||
|
{
|
||||||
|
// Keeps the face-parent mapping; skips decimation and repair.
|
||||||
|
Bake,
|
||||||
|
// The full sequence, including decimation and repair.
|
||||||
|
Export,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct PipelineSettings
|
||||||
|
{
|
||||||
|
// Target edge length for the refinement, in mm.
|
||||||
|
double refine_length = 1.0;
|
||||||
|
|
||||||
|
// Sliver removal between the two subdivision passes.
|
||||||
|
bool regularize = true;
|
||||||
|
RegularizeOptions regularize_opts;
|
||||||
|
// Slightly above the first pass, so it recovers the edges regularization lengthened instead of
|
||||||
|
// re-refining what it just merged.
|
||||||
|
double regularize_second_pass_mul = 1.1;
|
||||||
|
|
||||||
|
DisplaceSettings displace;
|
||||||
|
|
||||||
|
// Export mode only.
|
||||||
|
size_t max_triangles = 750'000;
|
||||||
|
bool harvest_flat = true;
|
||||||
|
double harvest_tol = DECIMATE_DEFAULT_HARVEST_TOL;
|
||||||
|
// Lock the untextured region against both regularization and decimation.
|
||||||
|
bool preserve_untextured = true;
|
||||||
|
|
||||||
|
// Snap vertices within this of the bottom plane onto it. 0 disables.
|
||||||
|
double bottom_snap_tol = 0.1;
|
||||||
|
|
||||||
|
int safety_cap = SUBDIVIDE_SAFETY_CAP;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Stage name and a fraction within it. Returning false cancels the run.
|
||||||
|
using PipelineProgressFn = std::function<bool(const char *stage, double fraction)>;
|
||||||
|
|
||||||
|
struct PipelineResult
|
||||||
|
{
|
||||||
|
TriSoup geometry;
|
||||||
|
// Output face -> input face. Empty in Export mode, where decimation invalidates it.
|
||||||
|
std::vector<int> face_parent_id;
|
||||||
|
bool safety_cap_hit = false;
|
||||||
|
bool locked_over_budget = false;
|
||||||
|
size_t collapse_count = 0;
|
||||||
|
bool canceled = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
PipelineResult run_pipeline(const TriSoup &input, const HeightSampleFn &sample,
|
||||||
|
const PipelineSettings &settings, const DisplaceBounds &bounds,
|
||||||
|
PipelineMode mode, const std::vector<uint8_t> &face_excluded = {},
|
||||||
|
const PipelineProgressFn &on_progress = {});
|
||||||
|
|
||||||
|
// Snap anything that ended below the model's original bottom back up to it.
|
||||||
|
void clamp_below_bottom(TriSoup &geometry, float bottom_z);
|
||||||
|
|
||||||
|
// Flatten the bed-contact surface by snapping positions within `tol` of the bottom plane onto it.
|
||||||
|
//
|
||||||
|
// Gated, not unconditional: an unconditional band snap also flattens the undersides of texture bumps
|
||||||
|
// near the base, folding them coplanar into the bottom face. Folded faces overlap the plate, so edges
|
||||||
|
// there pick up four incident faces - non-manifold edges and phantom shells on re-import. All copies
|
||||||
|
// of a position move together, and the move is rejected if any incident triangle would go degenerate
|
||||||
|
// or rotate more than about 75 degrees. A real bed-contact sliver rotates by a fraction of a degree
|
||||||
|
// and still snaps. Returns how many triangles moved.
|
||||||
|
size_t snap_bottom_to_flat(TriSoup &geometry, float bottom_z, double tol = 0.1);
|
||||||
|
|
||||||
|
} // namespace TextureBake
|
||||||
|
} // namespace Slic3r
|
||||||
380
src/libslic3r/TextureBake/TextureBakeRegularize.cpp
Normal file
380
src/libslic3r/TextureBake/TextureBakeRegularize.cpp
Normal file
@@ -0,0 +1,380 @@
|
|||||||
|
#include "TextureBakeRegularize.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
#include <limits>
|
||||||
|
#include <numeric>
|
||||||
|
|
||||||
|
namespace Slic3r {
|
||||||
|
namespace TextureBake {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Vertex-to-triangle lists as intrusive doubly linked lists of corner slots over flat arrays. Slot s
|
||||||
|
// is corner (triangle * 3 + k), owned by corners[s]. Deleted and moved corners are unlinked, so a
|
||||||
|
// collapse costs no allocation.
|
||||||
|
struct SlotLists
|
||||||
|
{
|
||||||
|
std::vector<int> head, next, prev;
|
||||||
|
|
||||||
|
void init(size_t vertex_count, size_t slot_count)
|
||||||
|
{
|
||||||
|
head.assign(vertex_count, -1);
|
||||||
|
next.assign(slot_count, -1);
|
||||||
|
prev.assign(slot_count, -1);
|
||||||
|
}
|
||||||
|
void link(int s, const std::vector<int> &corners)
|
||||||
|
{
|
||||||
|
const int v = corners[size_t(s)];
|
||||||
|
const int h = head[size_t(v)];
|
||||||
|
prev[size_t(s)] = -1;
|
||||||
|
next[size_t(s)] = h;
|
||||||
|
if (h != -1)
|
||||||
|
prev[size_t(h)] = s;
|
||||||
|
head[size_t(v)] = s;
|
||||||
|
}
|
||||||
|
void unlink(int s, const std::vector<int> &corners)
|
||||||
|
{
|
||||||
|
const int p = prev[size_t(s)], n = next[size_t(s)];
|
||||||
|
if (p != -1) next[size_t(p)] = n;
|
||||||
|
else head[size_t(corners[size_t(s)])] = n;
|
||||||
|
if (n != -1) prev[size_t(n)] = p;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
RegularizeResult regularize_mesh(const TriSoup &geometry, const std::vector<int> &face_parent_id,
|
||||||
|
double max_edge_length, const RegularizeOptions &opts)
|
||||||
|
{
|
||||||
|
RegularizeResult result;
|
||||||
|
const size_t tri_count = geometry.triangle_count();
|
||||||
|
if (tri_count == 0 || max_edge_length <= 0.0) {
|
||||||
|
result.geometry = geometry;
|
||||||
|
result.face_parent_id = face_parent_id;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
const double base_max_len_sq = (max_edge_length * opts.slack) * (max_edge_length * opts.slack);
|
||||||
|
const double aggr_max_len_sq =
|
||||||
|
(max_edge_length * opts.aggressive_slack) * (max_edge_length * opts.aggressive_slack);
|
||||||
|
const double extreme_aspect2 = opts.extreme_sliver_aspect * opts.extreme_sliver_aspect;
|
||||||
|
const double aspect_thr2 = opts.aspect_threshold * opts.aspect_threshold;
|
||||||
|
|
||||||
|
// Double precision: a collapse writes a midpoint back and later collapses read it, so rounding
|
||||||
|
// would accumulate.
|
||||||
|
QuantizedPointMap pos_map(WELD_GRID_GEOMETRY, std::min(tri_count * 3, size_t(1) << 22));
|
||||||
|
std::vector<Vec3d> vert;
|
||||||
|
std::vector<int> corners(tri_count * 3);
|
||||||
|
vert.reserve(tri_count);
|
||||||
|
for (size_t i = 0; i < tri_count * 3; ++i) {
|
||||||
|
const Vec3f &p = geometry.pos[i];
|
||||||
|
const int id = pos_map.get_or_set(p, int(vert.size()));
|
||||||
|
if (pos_map.inserted())
|
||||||
|
vert.push_back(p.cast<double>());
|
||||||
|
corners[i] = id;
|
||||||
|
}
|
||||||
|
const size_t vert_count = vert.size();
|
||||||
|
|
||||||
|
std::vector<Vec3d> tri_nrm(tri_count, Vec3d::Zero());
|
||||||
|
std::vector<uint8_t> tri_deleted(tri_count, 0);
|
||||||
|
result.face_parent_id = face_parent_id;
|
||||||
|
if (result.face_parent_id.size() != tri_count)
|
||||||
|
result.face_parent_id.assign(tri_count, 0);
|
||||||
|
|
||||||
|
const auto sq_dist = [&](int a, int b) { return (vert[size_t(a)] - vert[size_t(b)]).squaredNorm(); };
|
||||||
|
|
||||||
|
const auto recompute_face_normal = [&](size_t t) {
|
||||||
|
const Vec3d &a = vert[size_t(corners[t * 3])];
|
||||||
|
const Vec3d n = (vert[size_t(corners[t * 3 + 1])] - a).cross(vert[size_t(corners[t * 3 + 2])] - a);
|
||||||
|
const double len = n.norm();
|
||||||
|
tri_nrm[t] = (len > 0.0) ? Vec3d(n / len) : Vec3d::Zero();
|
||||||
|
};
|
||||||
|
for (size_t t = 0; t < tri_count; ++t)
|
||||||
|
recompute_face_normal(t);
|
||||||
|
|
||||||
|
// Never updated - the normal gate measures against these, so drift cannot compound across rounds.
|
||||||
|
const std::vector<Vec3d> orig_nrm = tri_nrm;
|
||||||
|
|
||||||
|
// Squared thinness, the longest edge over the shortest altitude:
|
||||||
|
// thinness = lmax / hmin = lmax^2 / (2 * area), so thinness^2 = lmax^4 / |AB x AC|^2
|
||||||
|
//
|
||||||
|
// Not lmax/lmin, which misses what matters here: three near-collinear points can have all edges
|
||||||
|
// similar, so an edge ratio reports about 2 and the gate skips a triangle with near-zero area
|
||||||
|
// whose corners sample three unrelated texels. An equilateral scores about 1.15.
|
||||||
|
const auto tri_aspect_sq = [&](size_t t) -> double {
|
||||||
|
const Vec3d &a = vert[size_t(corners[t * 3])];
|
||||||
|
const Vec3d ab = vert[size_t(corners[t * 3 + 1])] - a;
|
||||||
|
const Vec3d ac = vert[size_t(corners[t * 3 + 2])] - a;
|
||||||
|
const Vec3d bc = vert[size_t(corners[t * 3 + 2])] - vert[size_t(corners[t * 3 + 1])];
|
||||||
|
const double lmax2 = std::max({ ab.squaredNorm(), ac.squaredNorm(), bc.squaredNorm() });
|
||||||
|
const double cross2 = ab.cross(ac).squaredNorm();
|
||||||
|
return cross2 > 0.0 ? lmax2 * lmax2 / cross2 : std::numeric_limits<double>::infinity();
|
||||||
|
};
|
||||||
|
|
||||||
|
SlotLists slots;
|
||||||
|
slots.init(vert_count, tri_count * 3);
|
||||||
|
for (size_t s = 0; s < tri_count * 3; ++s)
|
||||||
|
slots.link(int(s), corners);
|
||||||
|
|
||||||
|
// O(1) membership without clearing a set per collapse.
|
||||||
|
std::vector<uint32_t> vert_stamp(vert_count, 0), tri_stamp(tri_count, 0);
|
||||||
|
uint32_t stamp_gen = 0;
|
||||||
|
|
||||||
|
// Both endpoints of a hard edge are barred from being collapse endpoints, preserving such
|
||||||
|
// corners exactly while leaving flat-face interiors free.
|
||||||
|
//
|
||||||
|
// Skipped when either triangle is an extreme sliver: a sliver's normal is dominated by where its
|
||||||
|
// far apex sits, so noise pivots it tens of degrees with no feature behind it, and freezing on
|
||||||
|
// that would lock the very chains this pass exists to dissolve. Genuine features are bordered by
|
||||||
|
// well-shaped triangles and are unaffected.
|
||||||
|
std::vector<uint8_t> frozen_vert(vert_count, 0);
|
||||||
|
{
|
||||||
|
std::vector<double> tri_thin2(tri_count);
|
||||||
|
for (size_t t = 0; t < tri_count; ++t)
|
||||||
|
tri_thin2[t] = tri_aspect_sq(t);
|
||||||
|
QuantizedPointMap edge_seen(1.0, std::min(tri_count * 3, size_t(1) << 22));
|
||||||
|
for (size_t t = 0; t < tri_count; ++t)
|
||||||
|
for (int e = 0; e < 3; ++e) {
|
||||||
|
const int u = corners[t * 3 + size_t(e)];
|
||||||
|
const int v = corners[t * 3 + size_t((e + 1) % 3)];
|
||||||
|
const int lo = std::min(u, v), hi = std::max(u, v);
|
||||||
|
const int other = edge_seen.get_or_set_key(lo, hi, 0, int(t));
|
||||||
|
if (edge_seen.inserted())
|
||||||
|
continue;
|
||||||
|
if (tri_thin2[t] > extreme_aspect2 || tri_thin2[size_t(other)] > extreme_aspect2)
|
||||||
|
continue;
|
||||||
|
if (tri_nrm[t].dot(tri_nrm[size_t(other)]) < opts.sharp_edge_cos) {
|
||||||
|
frozen_vert[size_t(u)] = 1;
|
||||||
|
frozen_vert[size_t(v)] = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exclusion freeze. The weight is constant across a face's corners, so the first one answers.
|
||||||
|
if (opts.preserve_excluded && !geometry.exclude_weight.empty())
|
||||||
|
for (size_t t = 0; t < tri_count; ++t)
|
||||||
|
if (geometry.exclude_weight[t * 3] > 0.99f)
|
||||||
|
for (int k = 0; k < 3; ++k)
|
||||||
|
frozen_vert[size_t(corners[t * 3 + size_t(k)])] = 1;
|
||||||
|
|
||||||
|
std::vector<int> wing_scratch, affected_scratch;
|
||||||
|
|
||||||
|
const auto third_vertex = [&](size_t t, int u, int v) {
|
||||||
|
const int a = corners[t * 3], b = corners[t * 3 + 1], c = corners[t * 3 + 2];
|
||||||
|
if (a != u && a != v) return a;
|
||||||
|
if (b != u && b != v) return b;
|
||||||
|
return c;
|
||||||
|
};
|
||||||
|
const auto triangles_sharing_edge = [&](int u, int v) -> std::vector<int> & {
|
||||||
|
wing_scratch.clear();
|
||||||
|
for (int s = slots.head[size_t(u)]; s != -1; s = slots.next[size_t(s)]) {
|
||||||
|
const size_t t = size_t(s) / 3;
|
||||||
|
if (tri_deleted[t])
|
||||||
|
continue;
|
||||||
|
if (corners[t * 3] == v || corners[t * 3 + 1] == v || corners[t * 3 + 2] == v)
|
||||||
|
wing_scratch.push_back(int(t));
|
||||||
|
}
|
||||||
|
return wing_scratch;
|
||||||
|
};
|
||||||
|
|
||||||
|
RegularizeRejectStats &stats = result.reject_stats;
|
||||||
|
|
||||||
|
const auto try_collapse = [&](int u, int v) -> bool {
|
||||||
|
if (u == v)
|
||||||
|
return false;
|
||||||
|
if (frozen_vert[size_t(u)] || frozen_vert[size_t(v)]) { ++stats.frozen; return false; }
|
||||||
|
|
||||||
|
// Two wings means a manifold interior edge.
|
||||||
|
std::vector<int> &wings = triangles_sharing_edge(u, v);
|
||||||
|
if (wings.size() != 2) { ++stats.wing_count; return false; }
|
||||||
|
const size_t w0 = size_t(wings[0]), w1 = size_t(wings[1]);
|
||||||
|
const int apex1 = third_vertex(w0, u, v), apex2 = third_vertex(w1, u, v);
|
||||||
|
if (apex1 == apex2) { ++stats.folded_apex; return false; }
|
||||||
|
|
||||||
|
// The edge cap loosens if *either* wing is extreme, since the re-subdivision recovers an
|
||||||
|
// over-long edge. The normal cap needs *both*, which is what protects fillets.
|
||||||
|
const double w1a = tri_aspect_sq(w0), w2a = tri_aspect_sq(w1);
|
||||||
|
const bool either_extreme = w1a > extreme_aspect2 || w2a > extreme_aspect2;
|
||||||
|
const bool both_extreme = w1a > extreme_aspect2 && w2a > extreme_aspect2;
|
||||||
|
const double eff_max_len_sq = either_extreme ? aggr_max_len_sq : base_max_len_sq;
|
||||||
|
const double eff_normal_cos =
|
||||||
|
both_extreme ? opts.aggressive_normal_delta_cos : opts.max_normal_delta_cos;
|
||||||
|
|
||||||
|
// A vertex sharing a triangle with both endpoints, other than the wing apexes, would go
|
||||||
|
// non-manifold. Stamp one side's neighbours, scan the other against them.
|
||||||
|
++stamp_gen;
|
||||||
|
for (int s = slots.head[size_t(v)]; s != -1; s = slots.next[size_t(s)]) {
|
||||||
|
const size_t t = size_t(s) / 3;
|
||||||
|
if (tri_deleted[t])
|
||||||
|
continue;
|
||||||
|
for (int k = 0; k < 3; ++k)
|
||||||
|
if (const int x = corners[t * 3 + size_t(k)]; x != v)
|
||||||
|
vert_stamp[size_t(x)] = stamp_gen;
|
||||||
|
}
|
||||||
|
for (int s = slots.head[size_t(u)]; s != -1; s = slots.next[size_t(s)]) {
|
||||||
|
const size_t t = size_t(s) / 3;
|
||||||
|
if (tri_deleted[t])
|
||||||
|
continue;
|
||||||
|
for (int k = 0; k < 3; ++k) {
|
||||||
|
const int x = corners[t * 3 + size_t(k)];
|
||||||
|
if (x != u && x != v && x != apex1 && x != apex2 && vert_stamp[size_t(x)] == stamp_gen) {
|
||||||
|
++stats.link_condition;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const Vec3d m = (vert[size_t(u)] + vert[size_t(v)]) * 0.5;
|
||||||
|
|
||||||
|
// Everything using either endpoint; the wings are being deleted.
|
||||||
|
++stamp_gen;
|
||||||
|
affected_scratch.clear();
|
||||||
|
for (const int endpoint : { u, v })
|
||||||
|
for (int s = slots.head[size_t(endpoint)]; s != -1; s = slots.next[size_t(s)]) {
|
||||||
|
const size_t t = size_t(s) / 3;
|
||||||
|
if (tri_deleted[t] || t == w0 || t == w1)
|
||||||
|
continue;
|
||||||
|
if (tri_stamp[t] != stamp_gen) {
|
||||||
|
tri_stamp[t] = stamp_gen;
|
||||||
|
affected_scratch.push_back(int(t));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate every affected triangle before touching anything.
|
||||||
|
for (const int ti : affected_scratch) {
|
||||||
|
const size_t t = size_t(ti);
|
||||||
|
Vec3d p[3];
|
||||||
|
for (int k = 0; k < 3; ++k) {
|
||||||
|
const int x = corners[t * 3 + size_t(k)];
|
||||||
|
p[k] = (x == u || x == v) ? m : vert[size_t(x)];
|
||||||
|
}
|
||||||
|
const double ab2 = (p[1] - p[0]).squaredNorm();
|
||||||
|
const double bc2 = (p[2] - p[1]).squaredNorm();
|
||||||
|
const double ca2 = (p[0] - p[2]).squaredNorm();
|
||||||
|
if (ab2 > eff_max_len_sq || bc2 > eff_max_len_sq || ca2 > eff_max_len_sq) {
|
||||||
|
++stats.edge_cap;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const Vec3d n = (p[1] - p[0]).cross(p[2] - p[0]);
|
||||||
|
const double nlen = n.norm();
|
||||||
|
if (nlen <= 0.0) { ++stats.degenerate; return false; }
|
||||||
|
if ((n / nlen).dot(orig_nrm[t]) < eff_normal_cos) { ++stats.normal_change; return false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply: move u to the merged position and redirect every reference to v.
|
||||||
|
vert[size_t(u)] = m;
|
||||||
|
for (const size_t w : { w0, w1 }) {
|
||||||
|
tri_deleted[w] = 1;
|
||||||
|
for (int k = 0; k < 3; ++k)
|
||||||
|
slots.unlink(int(w * 3) + k, corners);
|
||||||
|
}
|
||||||
|
// A non-wing triangle contains v exactly once, so moving its slots suffices.
|
||||||
|
for (int s = slots.head[size_t(v)]; s != -1;) {
|
||||||
|
const int ns = slots.next[size_t(s)];
|
||||||
|
slots.unlink(s, corners);
|
||||||
|
corners[size_t(s)] = u;
|
||||||
|
slots.link(s, corners);
|
||||||
|
recompute_face_normal(size_t(s) / 3);
|
||||||
|
s = ns;
|
||||||
|
}
|
||||||
|
for (int s = slots.head[size_t(u)]; s != -1; s = slots.next[size_t(s)]) {
|
||||||
|
const size_t t = size_t(s) / 3;
|
||||||
|
if (!tri_deleted[t])
|
||||||
|
recompute_face_normal(t);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
for (int round = 0; round < opts.maxrounds; ++round) {
|
||||||
|
// Rebuilt each round so earlier collapses inform the priorities.
|
||||||
|
std::vector<int> cand;
|
||||||
|
std::vector<double> cand_aspect;
|
||||||
|
for (size_t t = 0; t < tri_count; ++t) {
|
||||||
|
if (tri_deleted[t])
|
||||||
|
continue;
|
||||||
|
const int a = corners[t * 3], b = corners[t * 3 + 1], c = corners[t * 3 + 2];
|
||||||
|
if (std::min({ sq_dist(a, b), sq_dist(b, c), sq_dist(c, a) }) <= 0.0)
|
||||||
|
continue;
|
||||||
|
const double aspect2 = tri_aspect_sq(t);
|
||||||
|
if (aspect2 < aspect_thr2)
|
||||||
|
continue;
|
||||||
|
cand.push_back(int(t));
|
||||||
|
cand_aspect.push_back(aspect2);
|
||||||
|
}
|
||||||
|
// Worst first; ties keep ascending order so the pass is deterministic.
|
||||||
|
std::vector<int> order(cand.size());
|
||||||
|
std::iota(order.begin(), order.end(), 0);
|
||||||
|
std::stable_sort(order.begin(), order.end(),
|
||||||
|
[&](int x, int y) { return cand_aspect[size_t(x)] > cand_aspect[size_t(y)]; });
|
||||||
|
|
||||||
|
size_t round_collapses = 0;
|
||||||
|
for (const int oi : order) {
|
||||||
|
const size_t t = size_t(cand[size_t(oi)]);
|
||||||
|
if (tri_deleted[t])
|
||||||
|
continue;
|
||||||
|
const int a = corners[t * 3], b = corners[t * 3 + 1], c = corners[t * 3 + 2];
|
||||||
|
// All three edges, shortest first: a sliver straddling a seam has its shortest edge
|
||||||
|
// crossing it, which the normal gate refuses, while a long edge along one surface
|
||||||
|
// collapses safely. Trying only the shortest would leave those stuck.
|
||||||
|
struct Cand { double len2; int u, v; };
|
||||||
|
Cand e[3] = { { sq_dist(a, b), a, b }, { sq_dist(b, c), b, c }, { sq_dist(c, a), c, a } };
|
||||||
|
std::stable_sort(std::begin(e), std::end(e),
|
||||||
|
[](const Cand &x, const Cand &y) { return x.len2 < y.len2; });
|
||||||
|
if (try_collapse(e[0].u, e[0].v) || try_collapse(e[1].u, e[1].v) ||
|
||||||
|
try_collapse(e[2].u, e[2].v))
|
||||||
|
++round_collapses;
|
||||||
|
}
|
||||||
|
result.collapse_count += round_collapses;
|
||||||
|
if (round_collapses == 0)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drop deleted triangles and rebuild the soup.
|
||||||
|
const bool have_weights = !geometry.exclude_weight.empty();
|
||||||
|
std::vector<int> out_parent;
|
||||||
|
TriSoup &out = result.geometry;
|
||||||
|
for (size_t t = 0; t < tri_count; ++t) {
|
||||||
|
if (tri_deleted[t])
|
||||||
|
continue;
|
||||||
|
for (int k = 0; k < 3; ++k)
|
||||||
|
out.pos.push_back(vert[size_t(corners[t * 3 + size_t(k)])].cast<float>());
|
||||||
|
if (have_weights) {
|
||||||
|
// Constant across a face's corners.
|
||||||
|
const float w = geometry.exclude_weight[t * 3];
|
||||||
|
out.exclude_weight.insert(out.exclude_weight.end(), { w, w, w });
|
||||||
|
}
|
||||||
|
out_parent.push_back(result.face_parent_id[t]);
|
||||||
|
}
|
||||||
|
result.face_parent_id = std::move(out_parent);
|
||||||
|
|
||||||
|
// Rebuilt from the compacted geometry - the collapses moved vertices.
|
||||||
|
out.nrm.assign(out.pos.size(), Vec3f::Zero());
|
||||||
|
{
|
||||||
|
std::vector<Vec3d> accum(out.pos.size(), Vec3d::Zero());
|
||||||
|
QuantizedPointMap weld(WELD_GRID_GEOMETRY, out.pos.size());
|
||||||
|
std::vector<int> vid(out.pos.size());
|
||||||
|
int next = 0;
|
||||||
|
for (size_t i = 0; i < out.pos.size(); ++i) {
|
||||||
|
vid[i] = weld.get_or_set(out.pos[i], next);
|
||||||
|
if (weld.inserted())
|
||||||
|
++next;
|
||||||
|
}
|
||||||
|
std::vector<Vec3d> vn(size_t(next), Vec3d::Zero());
|
||||||
|
for (size_t t = 0; t * 3 < out.pos.size(); ++t) {
|
||||||
|
const Vec3d a = out.pos[t * 3].cast<double>();
|
||||||
|
const Vec3d n = (out.pos[t * 3 + 1].cast<double>() - a).cross(out.pos[t * 3 + 2].cast<double>() - a);
|
||||||
|
for (int k = 0; k < 3; ++k)
|
||||||
|
vn[size_t(vid[t * 3 + size_t(k)])] += n;
|
||||||
|
}
|
||||||
|
for (size_t i = 0; i < out.pos.size(); ++i) {
|
||||||
|
const Vec3d &n = vn[size_t(vid[i])];
|
||||||
|
const double l = n.norm();
|
||||||
|
out.nrm[i] = (l > 0.0) ? Vec3d(n / l).cast<float>() : Vec3f(0.f, 0.f, 1.f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace TextureBake
|
||||||
|
} // namespace Slic3r
|
||||||
73
src/libslic3r/TextureBake/TextureBakeRegularize.hpp
Normal file
73
src/libslic3r/TextureBake/TextureBakeRegularize.hpp
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
// Sliver removal by short-edge collapse.
|
||||||
|
//
|
||||||
|
// Subdivision turns tessellation needles into chains of slivers that are within the edge-length
|
||||||
|
// budget but still poor triangles. A sliver's three vertices land on three unrelated texels, so the
|
||||||
|
// relief picks up noise that is an artifact of the tessellation rather than of the image.
|
||||||
|
//
|
||||||
|
// A candidate's edge is collapsed to its midpoint only if it passes three gates: no affected
|
||||||
|
// triangle may exceed the target edge times a slack factor; every affected triangle must keep its
|
||||||
|
// face normal within a bound of its *original* direction (which is what stops curved surfaces being
|
||||||
|
// flattened); and the link condition must hold, or the result would be non-manifold. Boundary and
|
||||||
|
// non-manifold edges are skipped outright. Rounds repeat until one achieves nothing.
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "TextureBakeIndex.hpp"
|
||||||
|
|
||||||
|
namespace Slic3r {
|
||||||
|
namespace TextureBake {
|
||||||
|
|
||||||
|
struct RegularizeOptions
|
||||||
|
{
|
||||||
|
// Candidate threshold. Set to catch real slivers - chains measure in the hundreds - without
|
||||||
|
// sweeping up moderate fillet triangles, which sit between 2 and 5.
|
||||||
|
double aspect_threshold = 5.0;
|
||||||
|
|
||||||
|
// The base tier is loose on purpose: non-sliver boundary collapses must keep succeeding, since
|
||||||
|
// those give a chain the room to dissolve. A tight base leaves chains worse than before. The
|
||||||
|
// aggressive tier applies when at least one wing is an extreme sliver.
|
||||||
|
double slack = 3.0;
|
||||||
|
double aggressive_slack = 8.0;
|
||||||
|
|
||||||
|
// Thinness above which a wing counts as extreme: longest edge over shortest altitude.
|
||||||
|
double extreme_sliver_aspect = 8.0;
|
||||||
|
|
||||||
|
// Measured against each triangle's normal from before any collapse ran, so rounds of small
|
||||||
|
// allowed drift cannot compound into corner damage. Asymmetric two-tier: the loose bound needs
|
||||||
|
// *both* wings extreme, which matches a needle chain on a curved face but not a sliver beside a
|
||||||
|
// fillet, so fillets keep the tight bound.
|
||||||
|
double max_normal_delta_cos = 0.965925826289; // cos(15 degrees)
|
||||||
|
double aggressive_normal_delta_cos = 0.906307787037; // cos(25 degrees)
|
||||||
|
|
||||||
|
// Vertices on edges sharper than this are frozen, so hard features keep every original vertex.
|
||||||
|
double sharp_edge_cos = 0.866025403784; // cos(30 degrees)
|
||||||
|
|
||||||
|
int maxrounds = 8;
|
||||||
|
|
||||||
|
// Freeze excluded faces entirely, so untextured geometry is never modified.
|
||||||
|
bool preserve_excluded = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Which gate blocked a collapse - the only practical way to tell why a region failed to merge.
|
||||||
|
struct RegularizeRejectStats
|
||||||
|
{
|
||||||
|
size_t frozen = 0, wing_count = 0, link_condition = 0, edge_cap = 0, normal_change = 0,
|
||||||
|
degenerate = 0, folded_apex = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct RegularizeResult
|
||||||
|
{
|
||||||
|
TriSoup geometry;
|
||||||
|
std::vector<int> face_parent_id;
|
||||||
|
size_t collapse_count = 0;
|
||||||
|
RegularizeRejectStats reject_stats;
|
||||||
|
};
|
||||||
|
|
||||||
|
RegularizeResult regularize_mesh(const TriSoup &geometry, const std::vector<int> &face_parent_id,
|
||||||
|
double max_edge_length, const RegularizeOptions &opts = {});
|
||||||
|
|
||||||
|
} // namespace TextureBake
|
||||||
|
} // namespace Slic3r
|
||||||
215
src/libslic3r/TextureBake/TextureBakeRepair.cpp
Normal file
215
src/libslic3r/TextureBake/TextureBakeRepair.cpp
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
#include "TextureBakeRepair.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
|
#include <cmath>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <unordered_set>
|
||||||
|
|
||||||
|
namespace Slic3r {
|
||||||
|
namespace TextureBake {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
inline uint64_t edge_key(int a, int b)
|
||||||
|
{
|
||||||
|
const uint32_t lo = uint32_t(std::min(a, b)), hi = uint32_t(std::max(a, b));
|
||||||
|
return (uint64_t(lo) << 32) | uint64_t(hi);
|
||||||
|
}
|
||||||
|
|
||||||
|
// On the export grid a squared cross product is either 0 (collinear) or at least about 1e-16, the
|
||||||
|
// smallest real triangle being one grid unit per leg, so this separates the two cleanly.
|
||||||
|
constexpr double DEGENERATE_AREA_SQ = 1e-18;
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
EdgeDefects count_edge_defects(const TriSoup &geometry, double quant)
|
||||||
|
{
|
||||||
|
EdgeDefects out;
|
||||||
|
const size_t n = geometry.pos.size();
|
||||||
|
out.triangles = n / 3;
|
||||||
|
QuantizedPointMap vmap(quant, std::min(n, size_t(1) << 22));
|
||||||
|
std::vector<int> id(n);
|
||||||
|
int next = 0;
|
||||||
|
for (size_t i = 0; i < n; ++i) {
|
||||||
|
id[i] = vmap.get_or_set(geometry.pos[i], next);
|
||||||
|
if (vmap.inserted())
|
||||||
|
++next;
|
||||||
|
}
|
||||||
|
std::unordered_map<uint64_t, int> counts;
|
||||||
|
for (size_t t = 0; t + 2 < n; t += 3) {
|
||||||
|
const int a = id[t], b = id[t + 1], c = id[t + 2];
|
||||||
|
if (a == b || b == c || a == c)
|
||||||
|
continue;
|
||||||
|
const int tri[3] = { a, b, c };
|
||||||
|
for (int e = 0; e < 3; ++e)
|
||||||
|
++counts[edge_key(tri[e], tri[(e + 1) % 3])];
|
||||||
|
}
|
||||||
|
for (const auto &[key, c] : counts) {
|
||||||
|
(void) key;
|
||||||
|
if (c == 1) ++out.open;
|
||||||
|
else if (c > 2) ++out.non_manifold;
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t count_area_slivers(const TriSoup &geometry)
|
||||||
|
{
|
||||||
|
size_t n = 0;
|
||||||
|
for (size_t t = 0; t + 2 < geometry.pos.size(); t += 3) {
|
||||||
|
const Vec3d u = (geometry.pos[t + 1] - geometry.pos[t]).cast<double>();
|
||||||
|
const Vec3d v = (geometry.pos[t + 2] - geometry.pos[t]).cast<double>();
|
||||||
|
// The threshold a slicer applies: area below 1e-12 mm^2.
|
||||||
|
if (u.cross(v).squaredNorm() < 1e-24)
|
||||||
|
++n;
|
||||||
|
}
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
TriSoup resolve_t_junctions(const TriSoup &geometry, const RepairOptions &opts)
|
||||||
|
{
|
||||||
|
const size_t n_tri = geometry.triangle_count();
|
||||||
|
const double on_tol2 = opts.on_seg_tol * opts.on_seg_tol;
|
||||||
|
const double Q = opts.weld_quant;
|
||||||
|
|
||||||
|
// Snapped, not just welded: keeping unrounded coordinates lets a thin triangle pass the
|
||||||
|
// degeneracy test here and then collapse to collinear once the file is written, punching the very
|
||||||
|
// hole this pass prevents. Snapping makes the check see what will be written.
|
||||||
|
QuantizedPointMap vmap(Q, std::min(n_tri * 3, size_t(1) << 22));
|
||||||
|
std::vector<Vec3d> vert;
|
||||||
|
std::vector<int> vid(n_tri * 3);
|
||||||
|
for (size_t i = 0; i < n_tri * 3; ++i) {
|
||||||
|
const Vec3f &p = geometry.pos[i];
|
||||||
|
const int id = vmap.get_or_set(p, int(vert.size()));
|
||||||
|
if (vmap.inserted())
|
||||||
|
vert.emplace_back(double(grid_round(double(p.x()) * Q)) / Q,
|
||||||
|
double(grid_round(double(p.y()) * Q)) / Q,
|
||||||
|
double(grid_round(double(p.z()) * Q)) / Q);
|
||||||
|
vid[i] = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dropped: faces whose corners welded together, and needles - distinct but collinear on this
|
||||||
|
// grid. A needle reads as watertight yet is deleted downstream, and dropping it leaves exactly
|
||||||
|
// the on-edge-vertex topology the pass below closes.
|
||||||
|
std::vector<std::array<int, 3>> faces;
|
||||||
|
faces.reserve(n_tri);
|
||||||
|
for (size_t t = 0; t < n_tri; ++t) {
|
||||||
|
const int a = vid[t * 3], b = vid[t * 3 + 1], c = vid[t * 3 + 2];
|
||||||
|
if (a == b || b == c || a == c)
|
||||||
|
continue;
|
||||||
|
const Vec3d u = vert[size_t(b)] - vert[size_t(a)];
|
||||||
|
const Vec3d w = vert[size_t(c)] - vert[size_t(a)];
|
||||||
|
if (u.cross(w).squaredNorm() < DEGENERATE_AREA_SQ)
|
||||||
|
continue;
|
||||||
|
faces.push_back({ a, b, c });
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int iter = 0; iter < opts.max_iters; ++iter) {
|
||||||
|
std::unordered_map<uint64_t, int> e_count;
|
||||||
|
for (const auto &f : faces)
|
||||||
|
for (int e = 0; e < 3; ++e)
|
||||||
|
++e_count[edge_key(f[size_t(e)], f[size_t((e + 1) % 3)])];
|
||||||
|
|
||||||
|
std::unordered_set<int> bverts;
|
||||||
|
for (const auto &[key, c] : e_count) {
|
||||||
|
if (c != 1)
|
||||||
|
continue;
|
||||||
|
bverts.insert(int(uint32_t(key >> 32)));
|
||||||
|
bverts.insert(int(uint32_t(key & 0xFFFFFFFFu)));
|
||||||
|
}
|
||||||
|
if (bverts.empty())
|
||||||
|
break;
|
||||||
|
const std::vector<int> bv(bverts.begin(), bverts.end());
|
||||||
|
|
||||||
|
struct Split { int a, b; std::vector<int> mids; };
|
||||||
|
std::unordered_map<size_t, Split> splits;
|
||||||
|
for (size_t fi = 0; fi < faces.size(); ++fi) {
|
||||||
|
const auto &f = faces[fi];
|
||||||
|
for (int e = 0; e < 3; ++e) {
|
||||||
|
const int a = f[size_t(e)], b = f[size_t((e + 1) % 3)];
|
||||||
|
if (e_count[edge_key(a, b)] != 1)
|
||||||
|
continue; // only a boundary edge carries an unresolved T-junction
|
||||||
|
const Vec3d A = vert[size_t(a)];
|
||||||
|
const Vec3d ev = vert[size_t(b)] - A;
|
||||||
|
const double elen2 = ev.squaredNorm();
|
||||||
|
if (elen2 < 1e-20)
|
||||||
|
continue;
|
||||||
|
std::vector<std::pair<double, int>> found;
|
||||||
|
for (const int c : bv) {
|
||||||
|
if (c == a || c == b)
|
||||||
|
continue;
|
||||||
|
const Vec3d cv = vert[size_t(c)] - A;
|
||||||
|
const double tp = cv.dot(ev) / elen2;
|
||||||
|
if (tp <= 1e-4 || tp >= 1.0 - 1e-4)
|
||||||
|
continue; // strictly between the ends
|
||||||
|
if ((cv - ev * tp).squaredNorm() < on_tol2)
|
||||||
|
found.emplace_back(tp, c);
|
||||||
|
}
|
||||||
|
if (!found.empty()) {
|
||||||
|
std::sort(found.begin(), found.end(),
|
||||||
|
[](const auto &x, const auto &y) { return x.first < y.first; });
|
||||||
|
Split sp{ a, b, {} };
|
||||||
|
for (const auto &m : found)
|
||||||
|
sp.mids.push_back(m.second);
|
||||||
|
splits.emplace(fi, std::move(sp));
|
||||||
|
break; // one site per face per pass; iteration handles cascades
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (splits.empty())
|
||||||
|
break;
|
||||||
|
|
||||||
|
std::vector<std::array<int, 3>> next;
|
||||||
|
next.reserve(faces.size() + splits.size() * 2);
|
||||||
|
for (size_t fi = 0; fi < faces.size(); ++fi) {
|
||||||
|
const auto it = splits.find(fi);
|
||||||
|
if (it == splits.end()) {
|
||||||
|
next.push_back(faces[fi]);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const auto &f = faces[fi];
|
||||||
|
const auto &sp = it->second;
|
||||||
|
const int apex = (f[0] != sp.a && f[0] != sp.b) ? f[0]
|
||||||
|
: (f[1] != sp.a && f[1] != sp.b) ? f[1]
|
||||||
|
: f[2];
|
||||||
|
// Walk the base the way the face already traverses it, so the winding survives.
|
||||||
|
bool dir_ab = false;
|
||||||
|
for (int e = 0; e < 3; ++e)
|
||||||
|
if (f[size_t(e)] == sp.a && f[size_t((e + 1) % 3)] == sp.b) {
|
||||||
|
dir_ab = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
std::vector<int> seq;
|
||||||
|
if (dir_ab) {
|
||||||
|
seq.push_back(sp.a);
|
||||||
|
seq.insert(seq.end(), sp.mids.begin(), sp.mids.end());
|
||||||
|
seq.push_back(sp.b);
|
||||||
|
} else {
|
||||||
|
seq.push_back(sp.b);
|
||||||
|
seq.insert(seq.end(), sp.mids.rbegin(), sp.mids.rend());
|
||||||
|
seq.push_back(sp.a);
|
||||||
|
}
|
||||||
|
for (size_t s = 0; s + 1 < seq.size(); ++s)
|
||||||
|
next.push_back({ seq[s], seq[s + 1], apex });
|
||||||
|
}
|
||||||
|
faces.swap(next);
|
||||||
|
}
|
||||||
|
|
||||||
|
TriSoup out;
|
||||||
|
out.pos.reserve(faces.size() * 3);
|
||||||
|
out.nrm.reserve(faces.size() * 3);
|
||||||
|
for (const auto &f : faces) {
|
||||||
|
const Vec3f a = vert[size_t(f[0])].cast<float>();
|
||||||
|
const Vec3f b = vert[size_t(f[1])].cast<float>();
|
||||||
|
const Vec3f c = vert[size_t(f[2])].cast<float>();
|
||||||
|
Vec3f nrm = (b - a).cross(c - a);
|
||||||
|
const float len = nrm.norm();
|
||||||
|
nrm = (len > 0.f) ? Vec3f(nrm / len) : Vec3f(0.f, 0.f, 1.f);
|
||||||
|
out.pos.insert(out.pos.end(), { a, b, c });
|
||||||
|
out.nrm.insert(out.nrm.end(), { nrm, nrm, nrm });
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace TextureBake
|
||||||
|
} // namespace Slic3r
|
||||||
48
src/libslic3r/TextureBake/TextureBakeRepair.hpp
Normal file
48
src/libslic3r/TextureBake/TextureBakeRepair.hpp
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
// T-junction resolution and edge-defect accounting.
|
||||||
|
//
|
||||||
|
// Decimation can collapse a long edge whose interior still carries neighbouring triangles' vertices.
|
||||||
|
// Those then sit *on* an edge rather than at an end: watertight vertex-for-vertex, but the edge has
|
||||||
|
// one incident face on one side, which a slicer reads as an open boundary. This splits the offending
|
||||||
|
// face into a fan so every on-edge vertex becomes a real corner.
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "TextureBakeIndex.hpp"
|
||||||
|
|
||||||
|
namespace Slic3r {
|
||||||
|
namespace TextureBake {
|
||||||
|
|
||||||
|
struct EdgeDefects
|
||||||
|
{
|
||||||
|
size_t open = 0, non_manifold = 0, triangles = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Welds at the export grid first: counting on the un-snapped mesh reports defects the file does not
|
||||||
|
// have and misses ones it does.
|
||||||
|
EdgeDefects count_edge_defects(const TriSoup &geometry, double quant = WELD_GRID_EXPORT);
|
||||||
|
|
||||||
|
// Triangles a slicer would drop as degenerate. Each one, removed, punches a hole - so a non-zero
|
||||||
|
// count means watertight only on paper.
|
||||||
|
size_t count_area_slivers(const TriSoup &geometry);
|
||||||
|
|
||||||
|
struct RepairOptions
|
||||||
|
{
|
||||||
|
// Coordinates are snapped onto this grid, matching the precision files are written with.
|
||||||
|
double weld_quant = WELD_GRID_EXPORT;
|
||||||
|
|
||||||
|
// How far off an edge a vertex may sit and still count as on it. Well above the harvest
|
||||||
|
// tolerance, since harvesting leaves a region flat only to within that, making a collapsed edge a
|
||||||
|
// chord the on-edge vertices deviate from by about as much. Still far below the weld grid.
|
||||||
|
double on_seg_tol = 0.02;
|
||||||
|
|
||||||
|
// Splitting one face can expose another behind it, so the pass cascades.
|
||||||
|
int max_iters = 16;
|
||||||
|
};
|
||||||
|
|
||||||
|
TriSoup resolve_t_junctions(const TriSoup &geometry, const RepairOptions &opts = {});
|
||||||
|
|
||||||
|
} // namespace TextureBake
|
||||||
|
} // namespace Slic3r
|
||||||
414
src/libslic3r/TextureBake/TextureBakeSubdivide.cpp
Normal file
414
src/libslic3r/TextureBake/TextureBakeSubdivide.cpp
Normal file
@@ -0,0 +1,414 @@
|
|||||||
|
#include "TextureBakeSubdivide.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
|
namespace Slic3r {
|
||||||
|
namespace TextureBake {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
double edge_len_sq(const VertStore &v, int a, int b)
|
||||||
|
{
|
||||||
|
return (v.pos[size_t(a)] - v.pos[size_t(b)]).squaredNorm();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Both indexers accumulate raw, area-weighted cross products and normalise once at the end.
|
||||||
|
void normalize_store_normals(VertStore &verts)
|
||||||
|
{
|
||||||
|
for (Vec3d &n : verts.nrm) {
|
||||||
|
const double len = n.norm();
|
||||||
|
n = (len > 0.0) ? Vec3d(n / len) : Vec3d(0.0, 0.0, 1.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keyed by the raw parent-vertex pair rather than by position: two sharp-edge copies of one point
|
||||||
|
// need their own midpoints, since their normals differ even though the position does not.
|
||||||
|
int get_midpoint(VertStore &verts, QuantizedPointMap &cache, int a, int b,
|
||||||
|
QuantizedPointMap *pos_canon_map)
|
||||||
|
{
|
||||||
|
const int lo = std::min(a, b), hi = std::max(a, b);
|
||||||
|
if (const int cached = cache.get_key(lo, hi, 0); cached != -1)
|
||||||
|
return cached;
|
||||||
|
|
||||||
|
const Vec3d m = (verts.pos[size_t(a)] + verts.pos[size_t(b)]) * 0.5;
|
||||||
|
Vec3d n = verts.nrm[size_t(a)] + verts.nrm[size_t(b)];
|
||||||
|
const double nl = n.norm();
|
||||||
|
n = (nl > 0.0) ? Vec3d(n / nl) : verts.nrm[size_t(a)];
|
||||||
|
|
||||||
|
const int idx = verts.push(m, n);
|
||||||
|
if (!verts.wgt.empty())
|
||||||
|
verts.wgt.push_back((verts.wgt[size_t(a)] + verts.wgt[size_t(b)]) * 0.5);
|
||||||
|
if (!verts.canon.empty() && pos_canon_map != nullptr)
|
||||||
|
verts.canon.push_back(pos_canon_map->get_or_set(float(m.x()), float(m.y()), float(m.z()), idx));
|
||||||
|
|
||||||
|
cache.get_or_set_key(lo, hi, 0, idx);
|
||||||
|
return idx;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct PassResult
|
||||||
|
{
|
||||||
|
std::vector<int> indices;
|
||||||
|
std::vector<uint8_t> face_excluded;
|
||||||
|
std::vector<int> face_parent_id;
|
||||||
|
bool changed = false;
|
||||||
|
bool capped = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Three steps, so that no T-junction can appear:
|
||||||
|
// 1. Mark every too-long edge globally, so both triangles on a shared edge decide alike.
|
||||||
|
// 1.5 Predict the exact resulting count from the marks (0->1, 1->2, 2->3, 3->4) and abort the
|
||||||
|
// *whole* pass if it exceeds the cap - a partial pass leaves split parents beside unsplit
|
||||||
|
// neighbours, the very crack step 1 prevents.
|
||||||
|
// 2. Rebuild, allocating once at the now-known size.
|
||||||
|
PassResult subdivide_pass(VertStore &verts, const std::vector<int> &indices, double max_edge_length,
|
||||||
|
int safety_cap, const std::vector<uint8_t> &face_excluded,
|
||||||
|
QuantizedPointMap *pos_canon_map, const std::vector<int> &face_parent_id)
|
||||||
|
{
|
||||||
|
PassResult out;
|
||||||
|
const double max_sq = max_edge_length * max_edge_length;
|
||||||
|
const size_t tri_count = indices.size() / 3;
|
||||||
|
const bool have_canon = !verts.canon.empty();
|
||||||
|
|
||||||
|
QuantizedPointMap mid_cache(1.0, 1 << 16);
|
||||||
|
QuantizedPointMap split_edges(1.0, 1 << 16);
|
||||||
|
|
||||||
|
// With canonical ids the key is the canonical *position* id, so split copies either side of a
|
||||||
|
// sharp edge see one another's decision; without them the vertex index serves.
|
||||||
|
const auto key_of = [&](int v) -> int64_t { return have_canon ? verts.canon[size_t(v)] : v; };
|
||||||
|
const auto mark_edge = [&](int a, int b) {
|
||||||
|
const int64_t u = key_of(a), v = key_of(b);
|
||||||
|
if (u < v) split_edges.get_or_set_key(u, v, 0, 1);
|
||||||
|
else split_edges.get_or_set_key(v, u, 0, 1);
|
||||||
|
};
|
||||||
|
const auto is_marked = [&](int a, int b) {
|
||||||
|
const int64_t u = key_of(a), v = key_of(b);
|
||||||
|
return (u < v ? split_edges.get_key(u, v, 0) : split_edges.get_key(v, u, 0)) != -1;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Step 1. An excluded triangle marks none of its own edges, so its interior never refines; its
|
||||||
|
// boundary edges are still marked by an included neighbour, and it follows that split.
|
||||||
|
for (size_t t = 0; t < tri_count; ++t) {
|
||||||
|
if (!face_excluded.empty() && face_excluded[t])
|
||||||
|
continue;
|
||||||
|
const int a = indices[t * 3], b = indices[t * 3 + 1], c = indices[t * 3 + 2];
|
||||||
|
if (edge_len_sq(verts, a, b) > max_sq) mark_edge(a, b);
|
||||||
|
if (edge_len_sq(verts, b, c) > max_sq) mark_edge(b, c);
|
||||||
|
if (edge_len_sq(verts, c, a) > max_sq) mark_edge(c, a);
|
||||||
|
}
|
||||||
|
if (split_edges.size() == 0) {
|
||||||
|
out.indices = indices;
|
||||||
|
out.face_excluded = face_excluded;
|
||||||
|
out.face_parent_id = face_parent_id;
|
||||||
|
return out; // changed stays false: nothing left to refine
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 1.5.
|
||||||
|
size_t predicted = 0;
|
||||||
|
for (size_t t = 0; t < tri_count; ++t) {
|
||||||
|
const int a = indices[t * 3], b = indices[t * 3 + 1], c = indices[t * 3 + 2];
|
||||||
|
const int n = int(is_marked(a, b)) + int(is_marked(b, c)) + int(is_marked(c, a));
|
||||||
|
predicted += (n == 0) ? 1 : size_t(n + 1);
|
||||||
|
}
|
||||||
|
if (predicted > size_t(safety_cap)) {
|
||||||
|
out.indices = indices;
|
||||||
|
out.face_excluded = face_excluded;
|
||||||
|
out.face_parent_id = face_parent_id;
|
||||||
|
out.capped = true;
|
||||||
|
return out; // coarser than asked for, but watertight
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 2.
|
||||||
|
out.indices.resize(predicted * 3);
|
||||||
|
if (!face_excluded.empty())
|
||||||
|
out.face_excluded.resize(predicted);
|
||||||
|
if (!face_parent_id.empty())
|
||||||
|
out.face_parent_id.resize(predicted);
|
||||||
|
size_t wi = 0, fi = 0;
|
||||||
|
const auto emit_face_data = [&](uint8_t excl, int pid, int times) {
|
||||||
|
for (int k = 0; k < times; ++k) {
|
||||||
|
if (!out.face_excluded.empty()) out.face_excluded[fi] = excl;
|
||||||
|
if (!out.face_parent_id.empty()) out.face_parent_id[fi] = pid;
|
||||||
|
++fi;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const auto emit = [&](int x, int y, int z) {
|
||||||
|
out.indices[wi++] = x; out.indices[wi++] = y; out.indices[wi++] = z;
|
||||||
|
};
|
||||||
|
|
||||||
|
for (size_t t = 0; t < tri_count; ++t) {
|
||||||
|
const int a = indices[t * 3], b = indices[t * 3 + 1], c = indices[t * 3 + 2];
|
||||||
|
const uint8_t excl = face_excluded.empty() ? uint8_t(0) : face_excluded[t];
|
||||||
|
const int pid = face_parent_id.empty() ? 0 : face_parent_id[t];
|
||||||
|
const bool s_ab = is_marked(a, b), s_bc = is_marked(b, c), s_ca = is_marked(c, a);
|
||||||
|
const int n = int(s_ab) + int(s_bc) + int(s_ca);
|
||||||
|
|
||||||
|
if (n == 0) {
|
||||||
|
emit(a, b, c);
|
||||||
|
emit_face_data(excl, pid, 1);
|
||||||
|
} else if (n == 3) {
|
||||||
|
// a
|
||||||
|
// / \
|
||||||
|
// mCA-mAB
|
||||||
|
// / \ / \
|
||||||
|
// c--mBC--b
|
||||||
|
const int m_ab = get_midpoint(verts, mid_cache, a, b, pos_canon_map);
|
||||||
|
const int m_bc = get_midpoint(verts, mid_cache, b, c, pos_canon_map);
|
||||||
|
const int m_ca = get_midpoint(verts, mid_cache, c, a, pos_canon_map);
|
||||||
|
emit(a, m_ab, m_ca);
|
||||||
|
emit(m_ab, b, m_bc);
|
||||||
|
emit(m_ca, m_bc, c);
|
||||||
|
emit(m_ab, m_bc, m_ca);
|
||||||
|
emit_face_data(excl, pid, 4);
|
||||||
|
} else if (n == 1) {
|
||||||
|
if (s_ab) {
|
||||||
|
const int m = get_midpoint(verts, mid_cache, a, b, pos_canon_map);
|
||||||
|
emit(a, m, c);
|
||||||
|
emit(m, b, c);
|
||||||
|
} else if (s_bc) {
|
||||||
|
const int m = get_midpoint(verts, mid_cache, b, c, pos_canon_map);
|
||||||
|
emit(a, b, m);
|
||||||
|
emit(a, m, c);
|
||||||
|
} else {
|
||||||
|
const int m = get_midpoint(verts, mid_cache, c, a, pos_canon_map);
|
||||||
|
emit(a, b, m);
|
||||||
|
emit(m, b, c);
|
||||||
|
}
|
||||||
|
emit_face_data(excl, pid, 2);
|
||||||
|
} else {
|
||||||
|
// A corner triangle on the untouched-edge vertex, then the remaining quadrilateral split
|
||||||
|
// along the midpoint-to-midpoint diagonal, which keeps the winding consistent.
|
||||||
|
//
|
||||||
|
// A sliver parent propagates: that inner diagonal inherits half the short edge and hands
|
||||||
|
// the sliver to two children per pass. No better diagonal exists - one avoiding the
|
||||||
|
// midpoints must pass through one of them, giving a zero-area triangle. Regularization
|
||||||
|
// removes such slivers before the mesh reaches here.
|
||||||
|
if (!s_ab) { // fan from c
|
||||||
|
const int m_bc = get_midpoint(verts, mid_cache, b, c, pos_canon_map);
|
||||||
|
const int m_ca = get_midpoint(verts, mid_cache, c, a, pos_canon_map);
|
||||||
|
emit(a, b, m_bc);
|
||||||
|
emit(a, m_bc, m_ca);
|
||||||
|
emit(c, m_ca, m_bc);
|
||||||
|
} else if (!s_bc) { // fan from a
|
||||||
|
const int m_ab = get_midpoint(verts, mid_cache, a, b, pos_canon_map);
|
||||||
|
const int m_ca = get_midpoint(verts, mid_cache, c, a, pos_canon_map);
|
||||||
|
emit(a, m_ab, m_ca);
|
||||||
|
emit(m_ab, b, c);
|
||||||
|
emit(m_ab, c, m_ca);
|
||||||
|
} else { // fan from b
|
||||||
|
const int m_ab = get_midpoint(verts, mid_cache, a, b, pos_canon_map);
|
||||||
|
const int m_bc = get_midpoint(verts, mid_cache, b, c, pos_canon_map);
|
||||||
|
emit(b, m_bc, m_ab);
|
||||||
|
emit(a, m_ab, m_bc);
|
||||||
|
emit(a, m_bc, c);
|
||||||
|
}
|
||||||
|
emit_face_data(excl, pid, 3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
out.changed = true;
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
IndexedMesh to_indexed_fast(const TriSoup &geometry)
|
||||||
|
{
|
||||||
|
// Preview path: a plain position merge - no clustering, no sharp-edge splitting, no canonical ids.
|
||||||
|
IndexedMesh out;
|
||||||
|
const size_t n = geometry.pos.size();
|
||||||
|
QuantizedPointMap vert_map(WELD_GRID_GEOMETRY, std::min(n, size_t(1) << 22));
|
||||||
|
out.indices.resize(n);
|
||||||
|
const bool has_w = !geometry.exclude_weight.empty();
|
||||||
|
|
||||||
|
for (size_t i = 0; i < n; ++i) {
|
||||||
|
const Vec3f &p = geometry.pos[i];
|
||||||
|
const Vec3f nf = geometry.nrm.empty() ? Vec3f(0.f, 0.f, 1.f) : geometry.nrm[i];
|
||||||
|
const int idx = vert_map.get_or_set(p, int(out.verts.count()));
|
||||||
|
if (vert_map.inserted()) {
|
||||||
|
out.verts.push(p.cast<double>(), nf.cast<double>());
|
||||||
|
if (has_w)
|
||||||
|
out.verts.wgt.push_back(double(geometry.exclude_weight[i]));
|
||||||
|
} else {
|
||||||
|
out.verts.nrm[size_t(idx)] += nf.cast<double>();
|
||||||
|
// Merge exclusion by maximum: any excluded face marks the shared vertex.
|
||||||
|
if (has_w && double(geometry.exclude_weight[i]) > out.verts.wgt[size_t(idx)])
|
||||||
|
out.verts.wgt[size_t(idx)] = double(geometry.exclude_weight[i]);
|
||||||
|
}
|
||||||
|
out.indices[i] = idx;
|
||||||
|
}
|
||||||
|
normalize_store_normals(out.verts);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
IndexedMesh to_indexed(const TriSoup &geometry)
|
||||||
|
{
|
||||||
|
// Export path. Two vertices at one position merge only when their face normals agree to within
|
||||||
|
// SUBDIVIDE_SHARP_ANGLE_DEG, which keeps a cylinder from faceting while stopping a cube's edge
|
||||||
|
// normal from leaking into the flat face interiors as subdivision carries it inward.
|
||||||
|
IndexedMesh out;
|
||||||
|
out.has_canon = true;
|
||||||
|
const size_t n = geometry.pos.size();
|
||||||
|
const bool has_w = !geometry.exclude_weight.empty();
|
||||||
|
const double sharp_cos = std::cos(SUBDIVIDE_SHARP_ANGLE_DEG * M_PI / 180.0);
|
||||||
|
|
||||||
|
// Per-face normals: unit for the angle test, raw for the area-weighted accumulation.
|
||||||
|
std::vector<Vec3d> face_unit(n), face_raw(n);
|
||||||
|
for (size_t t = 0; t + 2 < n; t += 3) {
|
||||||
|
const Vec3d a = geometry.pos[t].cast<double>();
|
||||||
|
const Vec3d b = geometry.pos[t + 1].cast<double>();
|
||||||
|
const Vec3d c = geometry.pos[t + 2].cast<double>();
|
||||||
|
const Vec3d r = (b - a).cross(c - a);
|
||||||
|
const double len = r.norm();
|
||||||
|
const Vec3d u = (len > 0.0) ? Vec3d(r / len) : Vec3d(0.0, 0.0, 1.0);
|
||||||
|
for (int v = 0; v < 3; ++v) {
|
||||||
|
face_unit[t + size_t(v)] = u;
|
||||||
|
face_raw[t + size_t(v)] = r;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
out.indices.resize(n);
|
||||||
|
out.pos_canon_map = QuantizedPointMap(WELD_GRID_GEOMETRY, std::min(n, size_t(1) << 22));
|
||||||
|
struct Cluster { int idx; Vec3d fn_unit; };
|
||||||
|
std::unordered_map<int, std::vector<Cluster>> clusters_by_canon;
|
||||||
|
|
||||||
|
for (size_t i = 0; i < n; ++i) {
|
||||||
|
const Vec3f &p = geometry.pos[i];
|
||||||
|
// The first vertex at a position becomes its canonical id; later split copies share it.
|
||||||
|
const int canon_id = out.pos_canon_map.get_or_set(p, int(out.verts.count()));
|
||||||
|
const bool fresh_position = out.pos_canon_map.inserted();
|
||||||
|
|
||||||
|
const auto add_vertex = [&](int canon) {
|
||||||
|
const int idx = out.verts.push(p.cast<double>(), face_raw[i]);
|
||||||
|
if (has_w)
|
||||||
|
out.verts.wgt.push_back(double(geometry.exclude_weight[i]));
|
||||||
|
out.verts.canon.push_back(canon);
|
||||||
|
return idx;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (fresh_position) {
|
||||||
|
const int idx = add_vertex(canon_id);
|
||||||
|
clusters_by_canon[canon_id].push_back({ idx, face_unit[i] });
|
||||||
|
out.indices[i] = idx;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<Cluster> &clusters = clusters_by_canon[canon_id];
|
||||||
|
bool matched = false;
|
||||||
|
for (Cluster &cl : clusters) {
|
||||||
|
if (cl.fn_unit.dot(face_unit[i]) < sharp_cos)
|
||||||
|
continue;
|
||||||
|
out.verts.nrm[size_t(cl.idx)] += face_raw[i];
|
||||||
|
if (has_w && double(geometry.exclude_weight[i]) > out.verts.wgt[size_t(cl.idx)])
|
||||||
|
out.verts.wgt[size_t(cl.idx)] = double(geometry.exclude_weight[i]);
|
||||||
|
// Track the running average, so gradual curvature stays in one cluster instead of
|
||||||
|
// fragmenting when a distant face exceeds the threshold against the seed's fixed normal.
|
||||||
|
cl.fn_unit += face_unit[i];
|
||||||
|
if (const double rl = cl.fn_unit.norm(); rl > 0.0)
|
||||||
|
cl.fn_unit /= rl;
|
||||||
|
out.indices[i] = cl.idx;
|
||||||
|
matched = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (!matched) {
|
||||||
|
// A sharp-edge split: a new vertex at the same position, sharing its canonical id.
|
||||||
|
const int idx = add_vertex(canon_id);
|
||||||
|
clusters.push_back({ idx, face_unit[i] });
|
||||||
|
out.indices[i] = idx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
normalize_store_normals(out.verts);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
TriSoup to_non_indexed(const VertStore &verts, const std::vector<int> &indices,
|
||||||
|
const std::vector<uint8_t> &face_excluded)
|
||||||
|
{
|
||||||
|
TriSoup out;
|
||||||
|
const size_t tri_count = indices.size() / 3;
|
||||||
|
out.pos.resize(tri_count * 3);
|
||||||
|
out.nrm.resize(tri_count * 3);
|
||||||
|
const bool want_weights = !face_excluded.empty() || !verts.wgt.empty();
|
||||||
|
if (want_weights)
|
||||||
|
out.exclude_weight.resize(tri_count * 3);
|
||||||
|
|
||||||
|
for (size_t t = 0; t < tri_count; ++t) {
|
||||||
|
// The per-face flag, not the interpolated weight: merging by maximum can push an *included*
|
||||||
|
// face's corners to 1 when it borders two excluded neighbours, wrongly excluding it.
|
||||||
|
const bool have_face_flag = !face_excluded.empty();
|
||||||
|
const float face_w = have_face_flag ? (face_excluded[t] ? 1.f : 0.f) : 0.f;
|
||||||
|
for (int v = 0; v < 3; ++v) {
|
||||||
|
const size_t vidx = size_t(indices[t * 3 + size_t(v)]);
|
||||||
|
out.pos[t * 3 + size_t(v)] = verts.pos[vidx].cast<float>();
|
||||||
|
out.nrm[t * 3 + size_t(v)] = verts.nrm[vidx].cast<float>();
|
||||||
|
if (want_weights)
|
||||||
|
out.exclude_weight[t * 3 + size_t(v)] =
|
||||||
|
have_face_flag ? face_w : float(verts.wgt[vidx]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
SubdivideResult subdivide(const TriSoup &geometry, double max_edge_length,
|
||||||
|
const std::vector<uint8_t> &face_excluded, bool fast, int safety_cap,
|
||||||
|
const SubdivideProgressFn &on_progress)
|
||||||
|
{
|
||||||
|
SubdivideResult result;
|
||||||
|
if (geometry.empty() || max_edge_length <= 0.0) {
|
||||||
|
result.geometry = geometry;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
IndexedMesh indexed = fast ? to_indexed_fast(geometry) : to_indexed(geometry);
|
||||||
|
QuantizedPointMap *canon_map = indexed.has_canon ? &indexed.pos_canon_map : nullptr;
|
||||||
|
|
||||||
|
std::vector<int> current_indices = indexed.indices;
|
||||||
|
std::vector<uint8_t> current_excluded = face_excluded;
|
||||||
|
const size_t initial_tris = indexed.indices.size() / 3;
|
||||||
|
std::vector<int> current_parent(initial_tris);
|
||||||
|
for (size_t i = 0; i < initial_tris; ++i)
|
||||||
|
current_parent[i] = int(i);
|
||||||
|
|
||||||
|
for (int iter = 0; iter < SUBDIVIDE_MAX_ITERATIONS; ++iter) {
|
||||||
|
if (current_indices.size() / 3 >= size_t(safety_cap)) {
|
||||||
|
result.safety_cap_hit = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
PassResult pass = subdivide_pass(indexed.verts, current_indices, max_edge_length, safety_cap,
|
||||||
|
current_excluded, canon_map, current_parent);
|
||||||
|
current_indices = std::move(pass.indices);
|
||||||
|
if (!pass.face_excluded.empty())
|
||||||
|
current_excluded = std::move(pass.face_excluded);
|
||||||
|
if (!pass.face_parent_id.empty())
|
||||||
|
current_parent = std::move(pass.face_parent_id);
|
||||||
|
if (pass.capped || current_indices.size() / 3 >= size_t(safety_cap))
|
||||||
|
result.safety_cap_hit = true;
|
||||||
|
|
||||||
|
if (on_progress) {
|
||||||
|
// Reported after the pass, so the value falls each iteration instead of lagging a step.
|
||||||
|
double max_edge_sq = 0.0;
|
||||||
|
for (size_t t = 0; t + 2 < current_indices.size(); t += 3) {
|
||||||
|
const int a = current_indices[t], b = current_indices[t + 1], c = current_indices[t + 2];
|
||||||
|
max_edge_sq = std::max({ max_edge_sq, edge_len_sq(indexed.verts, a, b),
|
||||||
|
edge_len_sq(indexed.verts, b, c),
|
||||||
|
edge_len_sq(indexed.verts, c, a) });
|
||||||
|
}
|
||||||
|
if (!on_progress(std::min(0.95, double(iter + 1) / SUBDIVIDE_MAX_ITERATIONS),
|
||||||
|
current_indices.size() / 3, std::sqrt(max_edge_sq)))
|
||||||
|
break; // whole passes only, so what we have is still crack-free
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!pass.changed || result.safety_cap_hit)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
result.geometry = to_non_indexed(indexed.verts, current_indices, current_excluded);
|
||||||
|
result.face_parent_id = std::move(current_parent);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace TextureBake
|
||||||
|
} // namespace Slic3r
|
||||||
83
src/libslic3r/TextureBake/TextureBakeSubdivide.hpp
Normal file
83
src/libslic3r/TextureBake/TextureBakeSubdivide.hpp
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
// Adaptive subdivision to a target edge length, by global marked-edge (red-green) refinement rather
|
||||||
|
// than longest-edge bisection. Marking is global, so two triangles sharing an edge always agree and
|
||||||
|
// the result is crack-free by construction. A triangle is rebuilt from its marked-edge count: 0
|
||||||
|
// keeps, 1 bisects, 2 fans into three, 3 does the regular 1->4 split.
|
||||||
|
//
|
||||||
|
// The 1->4 case is what keeps the tessellation regular - its children are similar to the parent. An
|
||||||
|
// irregular one shows up after displacement as adjacent triangles tilting alternately, i.e. noise.
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "TextureBakeIndex.hpp"
|
||||||
|
|
||||||
|
namespace Slic3r {
|
||||||
|
namespace TextureBake {
|
||||||
|
|
||||||
|
// Memory guard for the stages downstream. At roughly 145 bytes per triangle this is about 2.9 GB.
|
||||||
|
static constexpr int SUBDIVIDE_SAFETY_CAP = 16'000'000;
|
||||||
|
|
||||||
|
// Vertices at one position stay separate when their faces disagree by more than this: a cube keeps
|
||||||
|
// hard edges, a cylinder keeps averaged ones.
|
||||||
|
static constexpr double SUBDIVIDE_SHARP_ANGLE_DEG = 30.0;
|
||||||
|
|
||||||
|
// A depth bound, not a work bound: the loop stops as soon as a pass changes nothing.
|
||||||
|
static constexpr int SUBDIVIDE_MAX_ITERATIONS = 12;
|
||||||
|
|
||||||
|
// Built by the indexers, appended to by the passes. Double precision so repeated midpointing does
|
||||||
|
// not drift.
|
||||||
|
struct VertStore
|
||||||
|
{
|
||||||
|
std::vector<Vec3d> pos;
|
||||||
|
std::vector<Vec3d> nrm;
|
||||||
|
std::vector<double> wgt; // exclusion weights; empty when the caller supplied none
|
||||||
|
std::vector<int> canon; // canonical position ids; empty in fast mode
|
||||||
|
|
||||||
|
size_t count() const { return pos.size(); }
|
||||||
|
int push(const Vec3d &p, const Vec3d &n)
|
||||||
|
{
|
||||||
|
const int idx = int(pos.size());
|
||||||
|
pos.push_back(p);
|
||||||
|
nrm.push_back(n);
|
||||||
|
return idx;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct IndexedMesh
|
||||||
|
{
|
||||||
|
VertStore verts;
|
||||||
|
std::vector<int> indices; // 3 per triangle
|
||||||
|
QuantizedPointMap pos_canon_map{ WELD_GRID_GEOMETRY, 256 };
|
||||||
|
bool has_canon = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fraction, triangle count, longest remaining edge. Returning false cancels; what comes back is
|
||||||
|
// still watertight, because passes apply whole or not at all.
|
||||||
|
using SubdivideProgressFn = std::function<bool(double fraction, size_t triangles, double longest_edge)>;
|
||||||
|
|
||||||
|
struct SubdivideResult
|
||||||
|
{
|
||||||
|
TriSoup geometry;
|
||||||
|
// Output triangle -> input triangle it descends from, so per-face data survives with no remap.
|
||||||
|
std::vector<int> face_parent_id;
|
||||||
|
bool safety_cap_hit = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// `face_excluded`: one entry per input triangle; non-zero means its interior is never refined. Its
|
||||||
|
// edges still split when an included neighbour marks them, so no T-junction appears at the boundary.
|
||||||
|
// `fast` selects the cheap position-only indexer for previews.
|
||||||
|
SubdivideResult subdivide(const TriSoup &geometry, double max_edge_length,
|
||||||
|
const std::vector<uint8_t> &face_excluded = {}, bool fast = false,
|
||||||
|
int safety_cap = SUBDIVIDE_SAFETY_CAP,
|
||||||
|
const SubdivideProgressFn &on_progress = {});
|
||||||
|
|
||||||
|
// Displacement needs the same welding and sharp-edge clustering.
|
||||||
|
IndexedMesh to_indexed(const TriSoup &geometry);
|
||||||
|
IndexedMesh to_indexed_fast(const TriSoup &geometry);
|
||||||
|
TriSoup to_non_indexed(const VertStore &verts, const std::vector<int> &indices,
|
||||||
|
const std::vector<uint8_t> &face_excluded);
|
||||||
|
|
||||||
|
} // namespace TextureBake
|
||||||
|
} // namespace Slic3r
|
||||||
@@ -15,11 +15,14 @@
|
|||||||
|
|
||||||
#include <tbb/blocked_range.h>
|
#include <tbb/blocked_range.h>
|
||||||
#include <tbb/parallel_for.h>
|
#include <tbb/parallel_for.h>
|
||||||
|
#include <tbb/parallel_sort.h>
|
||||||
|
|
||||||
#include "MeshBoolean.hpp"
|
#include "MeshBoolean.hpp"
|
||||||
#include "Model.hpp"
|
#include "Model.hpp"
|
||||||
#include "PNGReadWrite.hpp"
|
#include "PNGReadWrite.hpp"
|
||||||
#include "TriangleSelector.hpp"
|
#include "TriangleSelector.hpp"
|
||||||
|
#include "TextureBake/TextureBakeMesh.hpp"
|
||||||
|
#include "TextureBake/TextureBakePipeline.hpp"
|
||||||
|
|
||||||
namespace Slic3r {
|
namespace Slic3r {
|
||||||
|
|
||||||
@@ -1361,6 +1364,83 @@ void despeckle_triangle_colors(const indexed_triangle_set &mesh, std::vector<int
|
|||||||
}
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
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.
|
||||||
|
std::vector<uint8_t> excluded(mesh.indices.size(), 1);
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (std::all_of(excluded.begin(), excluded.end(), [](uint8_t e) { return e != 0; }))
|
||||||
|
return mesh; // nothing painted
|
||||||
|
|
||||||
|
TextureBake::PipelineSettings settings;
|
||||||
|
settings.refine_length = std::max(0.01f, options.v2_refine_mm);
|
||||||
|
settings.regularize = options.v2_regularize;
|
||||||
|
settings.max_triangles = size_t(std::max(0, options.v2_max_triangles_k)) * 1000;
|
||||||
|
settings.preserve_untextured = true;
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
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 &smooth_normal, const Vec3f &) {
|
||||||
|
// The smooth normal: the direction the vertex actually moves along.
|
||||||
|
return combined(pos, smooth_normal);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 0 means no simplification, i.e. Bake mode.
|
||||||
|
const TextureBake::PipelineMode mode = settings.max_triangles > 0 ? TextureBake::PipelineMode::Export
|
||||||
|
: TextureBake::PipelineMode::Bake;
|
||||||
|
TextureBake::PipelineResult result = TextureBake::run_pipeline(
|
||||||
|
TextureBake::to_soup(mesh, excluded), sample, settings, bounds, mode, excluded,
|
||||||
|
[&progress](const char *, double f) {
|
||||||
|
return !progress || progress(std::clamp(int(f * 100.0), 0, 99));
|
||||||
|
});
|
||||||
|
if (result.canceled || result.geometry.empty())
|
||||||
|
return {};
|
||||||
|
|
||||||
|
indexed_triangle_set out = TextureBake::to_indexed_triangle_set(result.geometry);
|
||||||
|
return out.indices.empty() ? mesh : out;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
indexed_triangle_set build_texture_displacement(const indexed_triangle_set &base_mesh,
|
indexed_triangle_set build_texture_displacement(const indexed_triangle_set &base_mesh,
|
||||||
const std::vector<TextureDisplacementLayer> &layers,
|
const std::vector<TextureDisplacementLayer> &layers,
|
||||||
const TextureDisplacementFacetsData &facets_data,
|
const TextureDisplacementFacetsData &facets_data,
|
||||||
@@ -1384,6 +1464,9 @@ indexed_triangle_set build_texture_displacement(const indexed_triangle_set
|
|||||||
if (mesh.vertices.empty() || mesh.indices.empty())
|
if (mesh.vertices.empty() || mesh.indices.empty())
|
||||||
return mesh;
|
return mesh;
|
||||||
|
|
||||||
|
if (options.pipeline_v2)
|
||||||
|
return build_texture_displacement_v2(mesh, layers, facets_data, options, progress);
|
||||||
|
|
||||||
// Layers are combined in slot order, like stacked layers in an image editor: each one folds its
|
// 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).
|
// own displacement into the running total via its blend mode (see TextureBlendMode).
|
||||||
std::vector<const TextureDisplacementLayer *> ordered_layers;
|
std::vector<const TextureDisplacementLayer *> ordered_layers;
|
||||||
@@ -2073,26 +2156,38 @@ indexed_triangle_set subdivide_mesh_adaptive(const indexed_triangle_set &mesh,
|
|||||||
// instead of with the refined region, and what forced the tiny pass budget that stopped
|
// instead of with the refined region, and what forced the tiny pass budget that stopped
|
||||||
// refinement short.
|
// refinement short.
|
||||||
{
|
{
|
||||||
struct EdgeRec { int he[2]; int count; }; // he = encoded half-edge (triangle * 3 + local edge)
|
// Sort the half-edges by their edge key and walk the equal runs, rather than hashing every one
|
||||||
std::unordered_map<uint64_t, EdgeRec> edges;
|
// of them twice into an unordered_map. Same result, but the two expensive parts - forming the
|
||||||
edges.reserve(tris.size() * 2);
|
// keys and ordering them - both parallelise, where a shared hash map cannot. The map also cost
|
||||||
for (int ti = 0; ti < int(tris.size()); ++ti)
|
// a second full pass of lookups purely to read back what the first pass had just inserted.
|
||||||
for (int e = 0; e < 3; ++e) {
|
std::vector<std::pair<uint64_t, int>> he(tris.size() * 3); // (edge key, triangle * 3 + local edge)
|
||||||
EdgeRec &r = edges.try_emplace(edge_key(tris[ti].v[e], tris[ti].v[(e + 1) % 3]),
|
tbb::parallel_for(tbb::blocked_range<size_t>(0, tris.size()),
|
||||||
EdgeRec{ { -1, -1 }, 0 })
|
[&](const tbb::blocked_range<size_t> &range) {
|
||||||
.first->second;
|
for (size_t ti = range.begin(); ti < range.end(); ++ti)
|
||||||
if (r.count < 2)
|
for (int e = 0; e < 3; ++e)
|
||||||
r.he[r.count] = ti * 3 + e;
|
he[ti * 3 + size_t(e)] = {
|
||||||
++r.count;
|
edge_key(tris[ti].v[e], tris[ti].v[(e + 1) % 3]), int(ti) * 3 + e
|
||||||
}
|
};
|
||||||
for (int ti = 0; ti < int(tris.size()); ++ti)
|
});
|
||||||
for (int e = 0; e < 3; ++e) {
|
tbb::parallel_sort(he.begin(), he.end());
|
||||||
const EdgeRec &r = edges.at(edge_key(tris[ti].v[e], tris[ti].v[(e + 1) % 3]));
|
// Runs of equal key are the half-edges of one edge: one is a boundary, two are neighbours,
|
||||||
if (r.count > 2)
|
// more is non-manifold. Serial, but it is a single linear pass over an already ordered array.
|
||||||
tris[ti].nb[e] = NB_NONMANIFOLD;
|
for (size_t i = 0; i < he.size();) {
|
||||||
else if (r.count == 2)
|
size_t j = i + 1;
|
||||||
tris[ti].nb[e] = (r.he[0] == ti * 3 + e ? r.he[1] : r.he[0]) / 3;
|
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.
|
// Feature mode: per-vertex surface normal, and the sampled displacement height at each vertex.
|
||||||
@@ -2152,6 +2247,40 @@ indexed_triangle_set subdivide_mesh_adaptive(const indexed_triangle_set &mesh,
|
|||||||
return vcolor[v];
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
auto elen_sq = [&](int a, int b) -> float { return (verts[a] - verts[b]).squaredNorm(); };
|
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 one edge of a triangle taken as its "longest": greatest squared length, exact ties broken by
|
||||||
@@ -2388,9 +2517,22 @@ indexed_triangle_set subdivide_mesh_adaptive(const indexed_triangle_set &mesh,
|
|||||||
// re-scored on pop and dropped or re-pushed. The ordering is a budget-allocation heuristic only:
|
// re-scored on pop and dropped or re-pushed. The ordering is a budget-allocation heuristic only:
|
||||||
// neither correctness nor conformality depends on it.
|
// neither correctness nor conformality depends on it.
|
||||||
std::priority_queue<std::pair<float, int>> queue;
|
std::priority_queue<std::pair<float, int>> queue;
|
||||||
for (int ti = 0; ti < int(tris.size()); ++ti)
|
{
|
||||||
if (const float p = priority(ti); p > 1.f)
|
// Scoring the starting mesh means a detail_error() per triangle - four more sampler calls each
|
||||||
queue.emplace(p, ti);
|
// - 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(initial[size_t(ti)], ti);
|
||||||
|
}
|
||||||
|
|
||||||
// Every iteration either drops one satisfied triangle from the queue or performs exactly one
|
// 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.
|
// bisection, and bisections are capped by the triangle budget, so this always terminates.
|
||||||
|
|||||||
@@ -169,7 +169,7 @@ struct TextureDisplacementLayer
|
|||||||
std::shared_ptr<std::vector<unsigned char>> image_data;
|
std::shared_ptr<std::vector<unsigned char>> image_data;
|
||||||
|
|
||||||
float depth_mm = 0.4f; // maximum displacement along the surface normal, in mm
|
float depth_mm = 0.4f; // maximum displacement along the surface normal, in mm
|
||||||
float tiling_scale = 10.f; // size of one texture tile, in mm
|
float tiling_scale = 12.5f; // size of one texture tile, in mm
|
||||||
float rotation_deg = 0.f;
|
float rotation_deg = 0.f;
|
||||||
Vec2f offset = Vec2f::Zero();
|
Vec2f offset = Vec2f::Zero();
|
||||||
bool invert = false;
|
bool invert = false;
|
||||||
@@ -365,6 +365,15 @@ struct TextureDisplacementOptions
|
|||||||
// deliberately (which is a blunter version of the per-layer edge-smoothing falloff).
|
// deliberately (which is a blunter version of the per-layer edge-smoothing falloff).
|
||||||
bool smooth_skip_border = true;
|
bool smooth_skip_border = true;
|
||||||
|
|
||||||
|
// Alternative bake pipeline, for side-by-side comparison. The path above is topology-preserving
|
||||||
|
// and needs the mesh prepared first; this one refines, removes slivers, displaces and optionally
|
||||||
|
// simplifies in one run. Off by default, and it produces no colour - it rebuilds the topology, so
|
||||||
|
// the per-facet assignment has nothing stable to attach to.
|
||||||
|
bool pipeline_v2 = false;
|
||||||
|
float v2_refine_mm = 0.3f;
|
||||||
|
bool v2_regularize = false;
|
||||||
|
int v2_max_triangles_k = 750; // 0 skips simplification, which is worth comparing on its own
|
||||||
|
|
||||||
// Colour, all of which belongs to the stack rather than to any one layer: it is about how the
|
// Colour, all of which belongs to the stack rather than to any one layer: it is about how the
|
||||||
// printer will realise the colours, not about which image they came from.
|
// printer will realise the colours, not about which image they came from.
|
||||||
|
|
||||||
@@ -381,7 +390,8 @@ struct TextureDisplacementOptions
|
|||||||
{
|
{
|
||||||
int mix_mode = int(color_mix_mode);
|
int mix_mode = int(color_mix_mode);
|
||||||
ar(displace_border, smooth_enabled, smooth_strength, smooth_iterations, smooth_skip_border,
|
ar(displace_border, smooth_enabled, smooth_strength, smooth_iterations, smooth_skip_border,
|
||||||
color_mix_enabled, mix_mode, color_despeckle);
|
pipeline_v2, v2_refine_mm, v2_regularize, v2_max_triangles_k, color_mix_enabled, mix_mode,
|
||||||
|
color_despeckle);
|
||||||
color_mix_mode = ColorMixMode(mix_mode);
|
color_mix_mode = ColorMixMode(mix_mode);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3317,7 +3317,9 @@ bool GLGizmoTextureDisplacement::collect_paint_region(
|
|||||||
++vstart[size_t(its.indices[i][k]) + 1];
|
++vstart[size_t(its.indices[i][k]) + 1];
|
||||||
for (size_t v = 0; v < nvert; ++v)
|
for (size_t v = 0; v < nvert; ++v)
|
||||||
vstart[v + 1] += vstart[v];
|
vstart[v + 1] += vstart[v];
|
||||||
std::vector<int> vtri(size_t(vstart[nvert]), 0);
|
// static_cast, not size_t(...): the latter parses as a parameter declaration (see the note
|
||||||
|
// above the identical prefix sum on `part`).
|
||||||
|
std::vector<int> vtri(static_cast<size_t>(vstart[nvert]), 0);
|
||||||
{
|
{
|
||||||
std::vector<int> fill(vstart.begin(), vstart.begin() + nvert);
|
std::vector<int> fill(vstart.begin(), vstart.begin() + nvert);
|
||||||
for (size_t i = 0; i < ntri; ++i)
|
for (size_t i = 0; i < ntri; ++i)
|
||||||
@@ -4095,6 +4097,13 @@ void GLGizmoTextureDisplacement::bake_standard()
|
|||||||
}
|
}
|
||||||
apply_standard_mode_presets(mv); // belt and braces: never bake with values the panel is not showing
|
apply_standard_mode_presets(mv); // belt and braces: never bake with values the panel is not showing
|
||||||
|
|
||||||
|
// It refines as part of the bake, so preparing first would refine a second time at another
|
||||||
|
// target.
|
||||||
|
if (mv->texture_displacement_options.pipeline_v2) {
|
||||||
|
bake();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// The whole recipe in one go. Either stage having nothing to do is normal, not a failure - a mesh
|
// The whole recipe in one go. Either stage having nothing to do is normal, not a failure - a mesh
|
||||||
// that is already even needs no remesh, one that is already fine enough for the texture needs no
|
// that is already even needs no remesh, one that is already fine enough for the texture needs no
|
||||||
// subdivision - so no "nothing changed" message here: it goes straight on to the displacement.
|
// subdivision - so no "nothing changed" message here: it goes straight on to the displacement.
|
||||||
@@ -5364,6 +5373,48 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
|||||||
}
|
}
|
||||||
m_imgui->disabled_end();
|
m_imgui->disabled_end();
|
||||||
|
|
||||||
|
// Next to Bake and shown in both modes: the two pipelines have to be switchable on their own.
|
||||||
|
if (mv != nullptr) {
|
||||||
|
TextureDisplacementOptions &opts = mv->texture_displacement_options;
|
||||||
|
ImGui::Separator();
|
||||||
|
m_preview_params_dirty |= ImGui::Checkbox(_u8L("Experimental bake pipeline").c_str(), &opts.pipeline_v2);
|
||||||
|
if (ImGui::IsItemHovered())
|
||||||
|
m_imgui->tooltip(_u8L("Bake with the alternative pipeline: it refines, cleans up sliver triangles, "
|
||||||
|
"displaces and simplifies in one run, instead of moving the vertices the mesh "
|
||||||
|
"already has. Nothing needs preparing first - Subdivide and Remesh are ignored. "
|
||||||
|
"It does not produce colours yet, because it rebuilds the topology."),
|
||||||
|
m_imgui->scaled(20.f));
|
||||||
|
if (opts.pipeline_v2) {
|
||||||
|
ImGui::PushItemWidth(m_imgui->scaled(8.4f));
|
||||||
|
if (m_imgui->slider_float(std::string(_u8L("Edge length (mm)")) + "##v2edge", &opts.v2_refine_mm,
|
||||||
|
0.02f, 2.f, "%.1f", ImGuiLogSlider)) {
|
||||||
|
opts.v2_refine_mm = std::clamp(opts.v2_refine_mm, 0.02f, 2.f);
|
||||||
|
m_preview_params_dirty = true;
|
||||||
|
}
|
||||||
|
if (ImGui::IsItemHovered())
|
||||||
|
m_imgui->tooltip(_u8L("Triangle size the painted area is refined to before displacement. This is "
|
||||||
|
"what decides how much of the texture the mesh can carry."),
|
||||||
|
m_imgui->scaled(20.f));
|
||||||
|
if (ImGui::SliderInt((_u8L("Triangle budget (k)") + "##v2budget").c_str(), &opts.v2_max_triangles_k,
|
||||||
|
0, 4000)) {
|
||||||
|
opts.v2_max_triangles_k = std::clamp(opts.v2_max_triangles_k, 0, 4000);
|
||||||
|
m_preview_params_dirty = true;
|
||||||
|
}
|
||||||
|
if (ImGui::IsItemHovered())
|
||||||
|
m_imgui->tooltip(_u8L("Triangles to simplify down to after displacement, in thousands. 0 turns "
|
||||||
|
"simplification off, which is worth comparing on its own."),
|
||||||
|
m_imgui->scaled(20.f));
|
||||||
|
ImGui::PopItemWidth();
|
||||||
|
m_preview_params_dirty |= ImGui::Checkbox(_u8L("Clean up slivers").c_str(), &opts.v2_regularize);
|
||||||
|
if (ImGui::IsItemHovered())
|
||||||
|
m_imgui->tooltip(_u8L("Collapse the thin triangles refinement inherits from the model's own "
|
||||||
|
"tessellation, before displacement samples them. A sliver's three corners "
|
||||||
|
"land on three unrelated parts of the texture, which is what makes the "
|
||||||
|
"relief look jagged."),
|
||||||
|
m_imgui->scaled(20.f));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
m_imgui->disabled_begin(m_bake_in_progress || m_prepare_in_progress || mv == nullptr ||
|
m_imgui->disabled_begin(m_bake_in_progress || m_prepare_in_progress || mv == nullptr ||
|
||||||
!mv->is_texture_displacement_painted());
|
!mv->is_texture_displacement_painted());
|
||||||
|
|||||||
@@ -511,9 +511,9 @@ private:
|
|||||||
// The default used to be 1500 (i.e. +1.5 M triangles), which is what made Standard mode's Bake
|
// The default used to be 1500 (i.e. +1.5 M triangles), which is what made Standard mode's Bake
|
||||||
// take minutes: every stage after the subdivision - the displacement itself, the convex hull, the
|
// take minutes: every stage after the subdivision - the displacement itself, the convex hull, the
|
||||||
// GLModel upload, and the re-slice changed_object() triggers - then runs on a mesh two orders of
|
// GLModel upload, and the re-slice changed_object() triggers - then runs on a mesh two orders of
|
||||||
// magnitude denser than the input. 300k is still far finer than any FDM nozzle resolves at the
|
// magnitude denser than the input. 750k is still far finer than any FDM nozzle resolves at the
|
||||||
// 0.02 mm detail tolerance Standard uses, and the slider goes to 2000 for anyone who wants more.
|
// 0.02 mm detail tolerance Standard uses, and the slider goes to 2000 for anyone who wants more.
|
||||||
int m_subdivide_budget_k = 300;
|
int m_subdivide_budget_k = 750;
|
||||||
void subdivide_model_adaptive();
|
void subdivide_model_adaptive();
|
||||||
// Fills `region` (per current-mesh triangle, a REFINE_* bitmask) from the union of every layer's
|
// Fills `region` (per current-mesh triangle, a REFINE_* bitmask) from the union of every layer's
|
||||||
// painted area plus the band straddling its edge. If `paint` is non-null, also fills the per-layer
|
// painted area plus the band straddling its edge. If `paint` is non-null, also fills the per-layer
|
||||||
@@ -572,7 +572,6 @@ private:
|
|||||||
GLModel m_paint_overlay_glmodel;
|
GLModel m_paint_overlay_glmodel;
|
||||||
// Set on every paint event, cleared when the overlay is rebuilt in render_painter_gizmo(). Kept
|
// Set on every paint event, cleared when the overlay is rebuilt in render_painter_gizmo(). Kept
|
||||||
// separate from m_bump_preview_dirty so a stroke refreshes only the small painted patch per frame,
|
// separate from m_bump_preview_dirty so a stroke refreshes only the small painted patch per frame,
|
||||||
// not the bump mesh (which also carries every *unpainted* triangle of the volume).
|
|
||||||
bool m_paint_overlay_dirty = false;
|
bool m_paint_overlay_dirty = false;
|
||||||
void rebuild_paint_overlay();
|
void rebuild_paint_overlay();
|
||||||
void render_paint_overlay();
|
void render_paint_overlay();
|
||||||
|
|||||||
Reference in New Issue
Block a user