mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-20 06:01:16 +00:00
* Add Missing Includes Across src/libslic3r Every libslic3r source and header now directly includes the headers declaring what it uses, rather than relying on the precompiled header or transitive includes. Generated with clang-tidy misc-include-cleaner, with libslic3r headers spelled libslic3r/... so they resolve outside the library's private include paths. MultiMaterialSegmentation.hpp, Support/SupportParameters.hpp and Format/STEP.hpp are made self-contained by hand. * Make the libslic3r Headers Compile on Their Own Each now includes, or forward-declares, what it uses instead of relying on what its includers happened to include first. Left out: I18N.hpp, which errors on purpose when included from GUI code, and VoxelizeCSGMesh.hpp and SLA/bicubic.h, which nothing includes and which no longer compile at all. * Add the Includes Missing From the Hand-Fixed libslic3r Headers clang-tidy would not edit these headers while they failed to compile on their own, so the first pass skipped them. With the headers now self-contained, a second pass adds the rest. * Keep Windows Setup Ahead of the Added libslic3r Includes Print.cpp and Thread.cpp open with a _WIN32 block that has to come first; without the precompiled header, Print.cpp otherwise reaches windows.h through OCCT with NONLS defined and boost/regex fails. OpenVDBUtils.cpp and SLA/SupportTreeBuilder.cpp had includes inside #ifndef NOMINMAX, which libslic3r defines on Windows, so those were skipped there. .clang-tidy also ignores the MSVC STL and UCRT internals, Boost.Multiprecision's fwd.hpp and CPython's Windows include directory. * Re-Add libslic3r Includes After the Clipper2 2.0.1 Migration Rebasing onto main took main's version of the files the Clipper2 migration rewrote, so their added includes are restored here, along with includes for main's new code. Clipper2's individual headers are now ignored by clang-tidy: they only build the Z variant through clipper2_z.hpp, which defines USINGZ first, so including clipper.core.h and the like directly broke ClipperZUtils.cpp.
293 lines
15 KiB
C++
293 lines
15 KiB
C++
#include "TextureBakeDisplace.hpp"
|
|
#include "libslic3r/TextureBake/TextureBakeIndex.hpp"
|
|
#include "libslic3r/Point.hpp"
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <cstddef>
|
|
#include <cstdint>
|
|
#include <limits>
|
|
|
|
#include <math.h>
|
|
#include <tbb/blocked_range.h>
|
|
#include <tbb/parallel_for.h>
|
|
#include <vector>
|
|
#include <utility>
|
|
|
|
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) {
|
|
// Jacobi, so every vertex reads the previous iteration and the rows are independent.
|
|
tbb::parallel_for(tbb::blocked_range<size_t>(0, unique_count, 4096), [&](const tbb::blocked_range<size_t> &r) {
|
|
for (size_t id = r.begin(); id < r.end(); ++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. A representative corner is picked first so the sampling
|
|
// itself is a flat parallel loop - it is a texture fetch plus projection maths per layer, and by
|
|
// far the most expensive thing in this stage.
|
|
std::vector<double> grey(unique_count, 0.0);
|
|
std::vector<int> representative(unique_count, -1);
|
|
for (size_t i = 0; i < count; ++i)
|
|
if (representative[size_t(vertex_id[i])] < 0)
|
|
representative[size_t(vertex_id[i])] = int(i);
|
|
tbb::parallel_for(tbb::blocked_range<size_t>(0, unique_count),
|
|
[&](const tbb::blocked_range<size_t> &range) {
|
|
for (size_t vid = range.begin(); vid < range.end(); ++vid) {
|
|
const int rep = representative[vid];
|
|
if (rep < 0)
|
|
continue;
|
|
grey[vid] = double(sample(geometry.pos[size_t(rep)],
|
|
smooth_nrm[vid].cast<float>(),
|
|
blend_nrm[vid].cast<float>()));
|
|
}
|
|
});
|
|
|
|
// Pass 3: move every copy of a position by the identical vector. Each iteration writes only its
|
|
// own output slot, so the loop is independent per corner.
|
|
tbb::parallel_for(tbb::blocked_range<size_t>(0, count), [&](const tbb::blocked_range<size_t> &range) {
|
|
for (size_t i = range.begin(); i < range.end(); ++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>();
|
|
}
|
|
});
|
|
|
|
// Per-face, not averaged across shared positions: averaging can flip an excluded face's normal
|
|
// when its neighbours moved outward.
|
|
tbb::parallel_for(tbb::blocked_range<size_t>(0, count / 3), [&](const tbb::blocked_range<size_t> &r) {
|
|
for (size_t f = r.begin(); f < r.end(); ++f) {
|
|
const size_t t = f * 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
|