mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-08-29 04:49:31 +00:00
Improve adaptive subdivision at border & fix some visual bugs
This commit is contained in:
@@ -30,6 +30,9 @@ uniform sampler2D height_tex;
|
||||
uniform vec2 height_tex_texel;
|
||||
uniform float depth_mm;
|
||||
uniform float tiling_scale;
|
||||
// Height map width / height. Scales the v axis so a non-square image keeps its proportions
|
||||
// instead of being squeezed into a square tile - mirrors libslic3r's apply_uv_transform().
|
||||
uniform float tex_aspect;
|
||||
uniform float rotation_rad;
|
||||
uniform vec2 uv_offset;
|
||||
uniform bool invert;
|
||||
@@ -70,7 +73,10 @@ vec2 project_uv(vec3 p, vec3 n)
|
||||
planar *= (tiling_scale > 1e-6) ? (1.0 / tiling_scale) : 1.0;
|
||||
float cs = cos(rotation_rad);
|
||||
float sn = sin(rotation_rad);
|
||||
return vec2(planar.x * cs - planar.y * sn, planar.x * sn + planar.y * cs) + uv_offset;
|
||||
vec2 r = vec2(planar.x * cs - planar.y * sn, planar.x * sn + planar.y * cs);
|
||||
// After the rotation, so the rotation stays a rotation rather than becoming a shear.
|
||||
r.y *= tex_aspect;
|
||||
return r + uv_offset;
|
||||
}
|
||||
|
||||
void main()
|
||||
@@ -156,7 +162,10 @@ void main()
|
||||
|
||||
float cs = cos(rotation_rad);
|
||||
float sn = sin(rotation_rad);
|
||||
vec2 slope = amplitude * vec2(dh_duv.x * cs + dh_duv.y * sn, -dh_duv.x * sn + dh_duv.y * cs);
|
||||
// One uv unit is tiling_scale mm along u but tiling_scale / tex_aspect mm along v, so the v
|
||||
// component of the gradient carries the extra factor before being rotated back into t/b.
|
||||
vec2 g = vec2(dh_duv.x, dh_duv.y * tex_aspect);
|
||||
vec2 slope = amplitude * vec2(g.x * cs + g.y * sn, -g.x * sn + g.y * cs);
|
||||
|
||||
vec3 gradient = slope.x * t + slope.y * b;
|
||||
gradient -= triangle_normal * dot(triangle_normal, gradient);
|
||||
|
||||
@@ -89,6 +89,9 @@ uniform sampler2D height_tex;
|
||||
uniform vec2 height_tex_texel; // (1/width, 1/height) of height_tex
|
||||
uniform float depth_mm;
|
||||
uniform float tiling_scale;
|
||||
// Height map width / height. Scales the v axis so a non-square image keeps its proportions
|
||||
// instead of being squeezed into a square tile - mirrors libslic3r's apply_uv_transform().
|
||||
uniform float tex_aspect;
|
||||
uniform float rotation_rad;
|
||||
uniform vec2 uv_offset;
|
||||
uniform bool invert;
|
||||
@@ -136,7 +139,10 @@ vec2 project_uv(vec3 p, vec3 n)
|
||||
planar *= (tiling_scale > 1e-6) ? (1.0 / tiling_scale) : 1.0;
|
||||
float cs = cos(rotation_rad);
|
||||
float sn = sin(rotation_rad);
|
||||
return vec2(planar.x * cs - planar.y * sn, planar.x * sn + planar.y * cs) + uv_offset;
|
||||
vec2 r = vec2(planar.x * cs - planar.y * sn, planar.x * sn + planar.y * cs);
|
||||
// After the rotation, so the rotation stays a rotation rather than becoming a shear.
|
||||
r.y *= tex_aspect;
|
||||
return r + uv_offset;
|
||||
}
|
||||
|
||||
void main()
|
||||
@@ -240,7 +246,10 @@ void main()
|
||||
// gradient back into the axes' frame.
|
||||
float cs = cos(rotation_rad);
|
||||
float sn = sin(rotation_rad);
|
||||
vec2 slope = amplitude * vec2(dh_duv.x * cs + dh_duv.y * sn, -dh_duv.x * sn + dh_duv.y * cs);
|
||||
// One uv unit is tiling_scale mm along u but tiling_scale / tex_aspect mm along v, so the v
|
||||
// component of the gradient carries the extra factor before being rotated back into t/b.
|
||||
vec2 g = vec2(dh_duv.x, dh_duv.y * tex_aspect);
|
||||
vec2 slope = amplitude * vec2(g.x * cs + g.y * sn, -g.x * sn + g.y * cs);
|
||||
|
||||
vec3 gradient = slope.x * t + slope.y * b;
|
||||
gradient -= triangle_normal * dot(triangle_normal, gradient);
|
||||
|
||||
@@ -356,6 +356,25 @@ std::optional<std::vector<Vec2f>> parameterize_lscm(const indexed_triangle_set &
|
||||
if (border == halfedge_descriptor())
|
||||
return std::nullopt; // no boundary at all -- a closed patch, which isn't a disk either
|
||||
|
||||
// ...and exactly one boundary loop. One connected component is not enough on its own: a patch with
|
||||
// a hole in it (paint a ring, or erase the middle of a stroke) is a single component with two
|
||||
// loops, and LSCM will happily "parameterize" it into an overlapping, folded-over chart rather
|
||||
// than fail. Walk the border halfedges and check every one of them belongs to the longest loop.
|
||||
{
|
||||
std::size_t border_halfedges = 0;
|
||||
for (halfedge_descriptor h : halfedges(cgal_mesh))
|
||||
if (is_border(h, cgal_mesh))
|
||||
++border_halfedges;
|
||||
std::size_t loop_length = 0;
|
||||
halfedge_descriptor h = border;
|
||||
do {
|
||||
++loop_length;
|
||||
h = next(h, cgal_mesh);
|
||||
} while (h != border && loop_length <= border_halfedges);
|
||||
if (loop_length != border_halfedges)
|
||||
return std::nullopt; // more than one boundary loop -- not a topological disk
|
||||
}
|
||||
|
||||
using Point_2 = EpicKernel::Point_2;
|
||||
using UV_pmap = _EpicMesh::Property_map<vertex_descriptor, Point_2>;
|
||||
UV_pmap uv_map = cgal_mesh.add_property_map<vertex_descriptor, Point_2>("h:uv", Point_2(0, 0)).first;
|
||||
|
||||
@@ -13,6 +13,9 @@
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
|
||||
#include <tbb/blocked_range.h>
|
||||
#include <tbb/parallel_for.h>
|
||||
|
||||
#include "MeshBoolean.hpp"
|
||||
#include "Model.hpp"
|
||||
#include "PNGReadWrite.hpp"
|
||||
@@ -897,7 +900,7 @@ std::vector<Vec2f> compute_lscm_uvs(const indexed_triangle_set &patch, const Tex
|
||||
return per_vertex;
|
||||
}
|
||||
|
||||
Vec2f apply_uv_transform(const Vec2f &planar, const TextureDisplacementLayer &layer)
|
||||
Vec2f apply_uv_transform(const Vec2f &planar, const TextureDisplacementLayer &layer, float aspect)
|
||||
{
|
||||
const float scale = (layer.tiling_scale > 1e-6f) ? (1.f / layer.tiling_scale) : 1.f;
|
||||
const Vec2f scaled = planar * scale;
|
||||
@@ -905,7 +908,20 @@ Vec2f apply_uv_transform(const Vec2f &planar, const TextureDisplacementLayer &la
|
||||
const float rad = layer.rotation_deg * float(M_PI) / 180.f;
|
||||
const float cs = std::cos(rad);
|
||||
const float sn = std::sin(rad);
|
||||
const Vec2f rotated(scaled.x() * cs - scaled.y() * sn, scaled.x() * sn + scaled.y() * cs);
|
||||
Vec2f rotated(scaled.x() * cs - scaled.y() * sn, scaled.x() * sn + scaled.y() * cs);
|
||||
|
||||
// Non-square textures. Without this the [0,1] square of uv covers the whole image whatever its
|
||||
// proportions, so a 2:1 image is squeezed into a square tile and every feature in it comes out
|
||||
// half as wide as it should be. `tiling_scale` is the tile's size along u; the tile is
|
||||
// `tiling_scale * height / width` mm along v, which is exactly what keeps texels square - so
|
||||
// dividing v by that extent is the same as multiplying it by width / height. A square texture has
|
||||
// aspect 1 and is untouched, which is why this changes nothing for the shipped library.
|
||||
//
|
||||
// Applied after the rotation, not before: scaling one axis of an already-rotated coordinate is a
|
||||
// shear, and doing it the other way round would make "Rotation" skew the pattern instead of
|
||||
// turning it.
|
||||
if (aspect > 0.f && aspect != 1.f)
|
||||
rotated.y() *= aspect;
|
||||
|
||||
return rotated + layer.offset;
|
||||
}
|
||||
@@ -954,8 +970,12 @@ float sample_layer_height(const DecodedHeightTexture &texture, const TextureDisp
|
||||
if (texture.empty())
|
||||
return 0.f;
|
||||
|
||||
// width / height of the height map, so a non-square image keeps its proportions (see
|
||||
// apply_uv_transform()). Every projection except the projective "from view" one funnels through
|
||||
// here, so this one line is what makes them all aspect-correct.
|
||||
const float aspect = (texture.height > 0) ? float(texture.width) / float(texture.height) : 1.f;
|
||||
auto sample_at = [&](const Vec2f &planar) {
|
||||
return texture.sample(apply_uv_transform(planar, layer), layer.tile_enabled, layer.tile_method);
|
||||
return texture.sample(apply_uv_transform(planar, layer, aspect), layer.tile_enabled, layer.tile_method);
|
||||
};
|
||||
|
||||
// Precomputed per-patch LSCM solve wins over the layer's own method (see the header): the
|
||||
@@ -1118,8 +1138,13 @@ std::vector<float> patch_boundary_distance(const indexed_triangle_set &patch, co
|
||||
indexed_triangle_set build_texture_displacement(const indexed_triangle_set &base_mesh,
|
||||
const std::vector<TextureDisplacementLayer> &layers,
|
||||
const TextureDisplacementFacetsData &facets_data,
|
||||
const TextureDisplacementOptions &options)
|
||||
const TextureDisplacementOptions &options,
|
||||
const DisplacementProgressFn &progress)
|
||||
{
|
||||
// Returns true to keep going. An aborted run returns {} (see the header): an empty mesh is the
|
||||
// one result no caller can mistake for a finished bake and commit onto the volume.
|
||||
const auto report = [&progress](int percent) { return !progress || progress(percent); };
|
||||
|
||||
indexed_triangle_set mesh = base_mesh;
|
||||
// TriangleSelector's vertex array starts with the mesh's own vertices (any extra ones, created
|
||||
// where a brush stroke split a triangle, are appended after them), and get_facets_strict()
|
||||
@@ -1183,8 +1208,14 @@ indexed_triangle_set build_texture_displacement(const indexed_triangle_set
|
||||
}
|
||||
}
|
||||
|
||||
if (!report(5))
|
||||
return {};
|
||||
|
||||
std::vector<float> displacement(mesh.vertices.size(), 0.f);
|
||||
std::vector<bool> displaced(mesh.vertices.size(), false);
|
||||
// uint8_t rather than std::vector<bool>: the sampling loop below writes these from several
|
||||
// threads at once, and vector<bool>'s bit packing makes writes to *distinct* elements a data
|
||||
// race on the shared word.
|
||||
std::vector<uint8_t> displaced(mesh.vertices.size(), 0);
|
||||
// Union, over every layer, of that layer's patch border - the vertices the post-process smoothing
|
||||
// holds when TextureDisplacementOptions::smooth_skip_border is set. A vertex on any patch's edge
|
||||
// counts, which is the conservative choice: hold it rather than let one layer's smoothing melt the
|
||||
@@ -1193,7 +1224,21 @@ indexed_triangle_set build_texture_displacement(const indexed_triangle_set
|
||||
bool any_displacement = false;
|
||||
|
||||
const TriangleMesh selector_mesh(mesh);
|
||||
// One selector for the whole stack, re-deserialized per layer. Its constructor computes
|
||||
// its_face_neighbors() and its_face_normals() over the *entire* mesh, which on a subdivided model
|
||||
// is by far the most expensive thing here - building a fresh one per layer paid that cost up to
|
||||
// eight times over. reset() (what deserialize(..., true) calls) only rebuilds the vertex/triangle
|
||||
// arrays; the neighbour and face-normal tables are immutable members and survive it.
|
||||
TriangleSelector selector(selector_mesh);
|
||||
bool selector_dirty = false;
|
||||
|
||||
const int layer_count = std::max(int(ordered_layers.size()), 1);
|
||||
int layer_index = 0;
|
||||
for (const TextureDisplacementLayer *layer : ordered_layers) {
|
||||
// Progress spans 5..65% across the layers; the apply and smoothing passes take it from there.
|
||||
if (!report(5 + (60 * layer_index++) / layer_count))
|
||||
return {};
|
||||
|
||||
const TriangleSelector::TriangleSplittingData &data = facets_data[size_t(layer->slot)];
|
||||
if (data.triangles_to_split.empty())
|
||||
continue;
|
||||
@@ -1202,8 +1247,9 @@ indexed_triangle_set build_texture_displacement(const indexed_triangle_set
|
||||
if (height.empty())
|
||||
continue;
|
||||
|
||||
TriangleSelector selector(selector_mesh);
|
||||
selector.deserialize(data, false);
|
||||
// needs_reset only from the second layer on: the selector is already pristine on the first.
|
||||
selector.deserialize(data, selector_dirty);
|
||||
selector_dirty = true;
|
||||
|
||||
const indexed_triangle_set patch = selector.get_facets_strict(EnforcerBlockerType::ENFORCER);
|
||||
if (patch.indices.empty())
|
||||
@@ -1287,8 +1333,13 @@ indexed_triangle_set build_texture_displacement(const indexed_triangle_set
|
||||
const float sign = layer->invert ? -1.f : 1.f;
|
||||
// A vertex may be reached by several of the patch's triangles; each must fold into the
|
||||
// running total exactly once, or a Multiply/Subtract layer would apply two or three times
|
||||
// over depending on how many painted triangles happen to share the vertex.
|
||||
std::vector<bool> visited(patch.vertices.size(), false);
|
||||
// over depending on how many painted triangles happen to share the vertex. Collecting the
|
||||
// unique list up front (cheap, one pass) is also what lets the expensive part - the texture
|
||||
// sampling, which is three bilinear fetches plus three pow()s per vertex for triplanar - run
|
||||
// in parallel below, instead of serially inside the triangle walk.
|
||||
std::vector<int> layer_vertices;
|
||||
std::vector<char> visited(patch.vertices.size(), 0);
|
||||
layer_vertices.reserve(patch.vertices.size());
|
||||
for (const stl_triangle_vertex_indices &tri : patch.indices)
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
const int vi = tri[i];
|
||||
@@ -1296,28 +1347,53 @@ indexed_triangle_set build_texture_displacement(const indexed_triangle_set
|
||||
// they carry no displacement of their own and are not part of the output mesh.
|
||||
if (vi >= int(mesh.vertices.size()) || (pin_boundary && is_boundary[vi]) || visited[vi])
|
||||
continue;
|
||||
visited[vi] = true;
|
||||
visited[vi] = 1;
|
||||
layer_vertices.push_back(vi);
|
||||
}
|
||||
if (layer_vertices.empty())
|
||||
continue;
|
||||
|
||||
const Vec2f *lscm_uv = lscm_uvs.empty() ? nullptr : &lscm_uvs[size_t(vi)];
|
||||
std::vector<float> sampled(layer_vertices.size(), 0.f);
|
||||
tbb::parallel_for(tbb::blocked_range<size_t>(0, layer_vertices.size()),
|
||||
[&](const tbb::blocked_range<size_t> &range) {
|
||||
for (size_t k = range.begin(); k < range.end(); ++k) {
|
||||
const size_t vi = size_t(layer_vertices[k]);
|
||||
const Vec2f *lscm_uv = lscm_uvs.empty() ? nullptr : &lscm_uvs[vi];
|
||||
const float h = sample_layer_height(height, *layer, mesh.vertices[vi], vertex_normals[vi],
|
||||
patch_centroid, patch_axis, lscm_uv);
|
||||
|
||||
// midlevel is the height that means "stay put", so anything below it displaces
|
||||
// *inwards* - see TextureDisplacementLayer::midlevel. At the default of 0 this is
|
||||
// exactly the old outward-only behaviour.
|
||||
const float edge_w = edge_weight.empty() ? 1.f : edge_weight[size_t(vi)];
|
||||
const float signed_height = (h - layer->midlevel) * layer->depth_mm * sign * edge_w;
|
||||
displacement[size_t(vi)] = blend_displacement(displacement[size_t(vi)], signed_height,
|
||||
displaced[size_t(vi)] ? layer->blend_mode : TextureBlendMode::Add);
|
||||
// The first layer to reach a vertex has nothing underneath it to blend with, so it
|
||||
// always starts the total off additively - a Multiply/Divide against an implicit
|
||||
// zero base would otherwise annihilate (or blow up) it, which is never what the
|
||||
// user means by putting a mask on the bottom of the stack.
|
||||
displaced[size_t(vi)] = true;
|
||||
any_displacement = true;
|
||||
sampled[k] = (h - layer->midlevel) * layer->depth_mm * sign;
|
||||
}
|
||||
});
|
||||
|
||||
for (size_t k = 0; k < layer_vertices.size(); ++k) {
|
||||
const size_t vi = size_t(layer_vertices[k]);
|
||||
// The first layer to reach a vertex has nothing underneath it to blend with, so it
|
||||
// always starts the total off additively - a Multiply/Divide against an implicit
|
||||
// zero base would otherwise annihilate (or blow up) it, which is never what the
|
||||
// user means by putting a mask on the bottom of the stack.
|
||||
const float accumulated = displacement[vi];
|
||||
const float blended = blend_displacement(accumulated, sampled[k],
|
||||
displaced[vi] ? layer->blend_mode : TextureBlendMode::Add);
|
||||
// Edge smoothing fades this layer's *effect*, not its input. Scaling the input instead is
|
||||
// only correct for Add/Subtract, whose neutral value is 0: on a Multiply layer a faded
|
||||
// input approaches 0, which annihilates everything beneath it at the rim rather than
|
||||
// leaving it alone, and on a Divide layer it approaches the 0.05 divisor floor, which
|
||||
// amplifies the relief underneath by up to 20x exactly where it was meant to fade out.
|
||||
// Interpolating the blended result back toward the accumulated total is the neutral
|
||||
// element for every mode at once, and reduces to the old formula exactly for Add.
|
||||
const float edge_w = edge_weight.empty() ? 1.f : edge_weight[vi];
|
||||
displacement[vi] = accumulated + (blended - accumulated) * edge_w;
|
||||
displaced[vi] = 1;
|
||||
}
|
||||
any_displacement = true;
|
||||
}
|
||||
|
||||
if (!report(65))
|
||||
return {};
|
||||
|
||||
if (!any_displacement)
|
||||
return mesh;
|
||||
|
||||
@@ -1332,12 +1408,22 @@ indexed_triangle_set build_texture_displacement(const indexed_triangle_set
|
||||
// neighbours are those pinned outsiders: relaxing it would drag the rim of the relief back down and
|
||||
// leave the pattern looking half-melted right where it meets the edge.
|
||||
if (options.smooth_enabled && options.smooth_strength > 0.f && options.smooth_iterations > 0) {
|
||||
if (!report(70))
|
||||
return {};
|
||||
std::vector<uint8_t> movable(mesh.vertices.size(), 0);
|
||||
for (size_t vi = 0; vi < mesh.vertices.size(); ++vi)
|
||||
movable[vi] = (displaced[vi] && !(options.smooth_skip_border && on_patch_border[vi])) ? 1 : 0;
|
||||
smooth_mesh_vertices(mesh, movable, options.smooth_strength, options.smooth_iterations);
|
||||
// The pass hook only *stops* the relaxation early; the report(99) below is what turns a
|
||||
// cancellation into an empty (uncommittable) result, since a cancelled run keeps reporting
|
||||
// cancelled.
|
||||
smooth_mesh_vertices(mesh, movable, options.smooth_strength, options.smooth_iterations,
|
||||
progress ? DisplacementProgressFn([&report, it = options.smooth_iterations](int pass) {
|
||||
return report(70 + (29 * (pass + 1)) / std::max(it, 1));
|
||||
}) : DisplacementProgressFn{});
|
||||
}
|
||||
|
||||
if (!report(99))
|
||||
return {};
|
||||
return mesh;
|
||||
}
|
||||
|
||||
@@ -1352,7 +1438,7 @@ indexed_triangle_set build_texture_displacement(const ModelVolume &volume)
|
||||
}
|
||||
|
||||
void smooth_mesh_vertices(indexed_triangle_set &mesh, const std::vector<uint8_t> &movable, float strength,
|
||||
int iterations)
|
||||
int iterations, const DisplacementProgressFn &on_pass)
|
||||
{
|
||||
if (iterations <= 0 || mesh.vertices.empty() || movable.size() != mesh.vertices.size())
|
||||
return;
|
||||
@@ -1391,15 +1477,21 @@ void smooth_mesh_vertices(indexed_triangle_set &mesh, const std::vector<uint8_t>
|
||||
std::vector<Vec3f> prev;
|
||||
for (int it = 0; it < iterations; ++it) {
|
||||
prev = mesh.vertices;
|
||||
for (size_t v = 0; v < nv; ++v) {
|
||||
if (!movable[v] || start[v] == start[v + 1])
|
||||
continue;
|
||||
Vec3f sum = Vec3f::Zero();
|
||||
for (int k = start[v]; k < start[v + 1]; ++k)
|
||||
sum += prev[size_t(nbr[size_t(k)])];
|
||||
const Vec3f avg = sum / float(start[v + 1] - start[v]);
|
||||
mesh.vertices[v] = prev[v] + (avg - prev[v]) * strength;
|
||||
}
|
||||
// Each vertex reads only from `prev` and writes only its own slot, so the sweep parallelises
|
||||
// with no synchronisation at all.
|
||||
tbb::parallel_for(tbb::blocked_range<size_t>(0, nv), [&](const tbb::blocked_range<size_t> &range) {
|
||||
for (size_t v = range.begin(); v < range.end(); ++v) {
|
||||
if (!movable[v] || start[v] == start[v + 1])
|
||||
continue;
|
||||
Vec3f sum = Vec3f::Zero();
|
||||
for (int k = start[v]; k < start[v + 1]; ++k)
|
||||
sum += prev[size_t(nbr[size_t(k)])];
|
||||
const Vec3f avg = sum / float(start[v + 1] - start[v]);
|
||||
mesh.vertices[v] = prev[v] + (avg - prev[v]) * strength;
|
||||
}
|
||||
});
|
||||
if (on_pass && !on_pass(it))
|
||||
return; // cancelled: leave the passes done so far in place, the caller decides what to do
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1552,7 +1644,8 @@ indexed_triangle_set subdivide_mesh_adaptive(const indexed_triangle_set &mesh,
|
||||
const std::vector<uint8_t> &refine_region,
|
||||
float target_edge_length_mm, int max_triangles,
|
||||
std::vector<int> *out_source, const HeightFieldSampler &sampler,
|
||||
float chord_tolerance_mm, float min_edge_length_mm)
|
||||
float chord_tolerance_mm, float min_edge_length_mm,
|
||||
float border_edge_length_mm)
|
||||
{
|
||||
// Neighbour slots that are not a triangle index.
|
||||
constexpr int NB_BOUNDARY = -1; // open edge: terminal on its own, bisected from this side alone
|
||||
@@ -1590,12 +1683,13 @@ indexed_triangle_set subdivide_mesh_adaptive(const indexed_triangle_set &mesh,
|
||||
const bool feature_mode = bool(sampler) && chord_tolerance_mm > 0.f;
|
||||
const float min_floor_sq = min_edge_length_mm > 0.f ? min_edge_length_mm * min_edge_length_mm : 0.f;
|
||||
const float target_sq = target_edge_length_mm > 0.f ? target_edge_length_mm * target_edge_length_mm : 0.f;
|
||||
const float border_sq = border_edge_length_mm > 0.f ? border_edge_length_mm * border_edge_length_mm : 0.f;
|
||||
|
||||
// refine_region is indexed by input-triangle index, and every triangle's src stays in that range
|
||||
// (children inherit their parent's src), so a wrong size would be an out-of-bounds read. Guard it.
|
||||
if (refine_region.size() != mesh.indices.size() || int(tris.size()) + 2 > max_triangles)
|
||||
return emit();
|
||||
if (!feature_mode && target_sq <= 0.f)
|
||||
if (!feature_mode && target_sq <= 0.f && border_sq <= 0.f)
|
||||
return emit(); // no criterion at all
|
||||
if (std::none_of(refine_region.begin(), refine_region.end(), [](uint8_t v) { return v != 0; }))
|
||||
return emit(); // nothing flagged: no-op
|
||||
@@ -1720,16 +1814,27 @@ indexed_triangle_set subdivide_mesh_adaptive(const indexed_triangle_set &mesh,
|
||||
// reach. Triangles outside the region always score 0 - they are only ever touched by the conformal
|
||||
// closure below, never refined on their own account.
|
||||
auto priority = [&](int ti) -> float {
|
||||
const Tri &t = tris[ti];
|
||||
if (refine_region[t.src] == 0)
|
||||
const Tri &t = tris[ti];
|
||||
const uint8_t flags = refine_region[t.src];
|
||||
if (flags == 0)
|
||||
return 0.f;
|
||||
const int le = longest_local(ti);
|
||||
const float ll = elen_sq(t.v[le], t.v[(le + 1) % 3]);
|
||||
if (ll <= min_floor_sq)
|
||||
return 0.f; // at the resolution floor - also what stops a sharp texture step going forever
|
||||
float p = (target_sq > 0.f) ? ll / target_sq : 0.f;
|
||||
if (feature_mode)
|
||||
p = std::max(p, detail_error(ti) / chord_tolerance_mm);
|
||||
float p = 0.f;
|
||||
if (flags & REFINE_PAINTED) {
|
||||
p = (target_sq > 0.f) ? ll / target_sq : 0.f;
|
||||
if (feature_mode)
|
||||
p = std::max(p, detail_error(ti) / chord_tolerance_mm);
|
||||
}
|
||||
// The band straddling the paint's edge, refined by plain edge length. Deliberately *not* run
|
||||
// through detail_error(): outside the paint the sampler still reports full relief (it has no
|
||||
// per-point paint test), so the chord test there would chase texture detail on a surface the
|
||||
// bake is going to leave flat. Length alone is what this band needs - the error it is fixing
|
||||
// is the size of the triangles spanning the displacement step, not the curvature of anything.
|
||||
if ((flags & REFINE_BORDER) && border_sq > 0.f)
|
||||
p = std::max(p, ll / border_sq);
|
||||
return p;
|
||||
};
|
||||
|
||||
|
||||
@@ -22,6 +22,14 @@ namespace Slic3r {
|
||||
|
||||
class ModelVolume;
|
||||
|
||||
// Bits of subdivide_mesh_adaptive()'s per-triangle `refine_region` mask. See that function.
|
||||
static constexpr uint8_t REFINE_PAINTED = 1;
|
||||
static constexpr uint8_t REFINE_BORDER = 2;
|
||||
|
||||
// Progress/cancellation hook for the (potentially multi-second) bake. Called with a 0..100
|
||||
// percentage; return false to abort. See build_texture_displacement().
|
||||
using DisplacementProgressFn = std::function<bool(int)>;
|
||||
|
||||
// Maximum number of simultaneous texture-displacement layers a single ModelVolume can hold.
|
||||
// Each layer owns its own paint mask (ModelVolume::texture_displacement_facet(slot)), so this
|
||||
// is also the number of independent EnforcerBlockerType selectors kept per volume.
|
||||
@@ -370,7 +378,11 @@ Vec2f project_planar(const Vec3f &position, const Vec3f &normal);
|
||||
// LSCM's per-patch UV solve through the same scale/rotate/offset controls as every other
|
||||
// projection method, without going through project_texture_displacement_uv()'s own dispatch
|
||||
// (which only knows how to compute the *analytic* methods from a single vertex + normal).
|
||||
Vec2f apply_uv_transform(const Vec2f &planar, const TextureDisplacementLayer &layer);
|
||||
// `aspect` is the height map's width / height. It scales the v axis so a non-square image is not
|
||||
// squeezed into a square tile: `tiling_scale` is the tile's size along u, and the tile is
|
||||
// `tiling_scale * height / width` mm along v, which keeps texels square. 1 (the default) is the
|
||||
// square case and leaves the coordinate exactly as it always was.
|
||||
Vec2f apply_uv_transform(const Vec2f &planar, const TextureDisplacementLayer &layer, float aspect = 1.f);
|
||||
|
||||
// Applies a row-major 3x4 projective matrix (see TextureDisplacementLayer::view_project_matrix) to a
|
||||
// local-space point, writing the resulting texture uv. Returns false - and leaves `uv` untouched -
|
||||
@@ -532,10 +544,18 @@ using TextureDisplacementFacetsData = std::array<TriangleSelector::TriangleSplit
|
||||
// mesh-boolean ops) the way TriangleSelector::remap_painting() does for the other paint channels.
|
||||
// Such operations will silently drop any unbaked texture-displacement paint on the affected
|
||||
// volume. This is an explicit extension point for a later phase, not an oversight.
|
||||
//
|
||||
// `progress`, when set, is called from the worker thread with a 0..100 completion percentage as the
|
||||
// bake proceeds. Returning false from it aborts the run, which then returns an *empty* mesh - never
|
||||
// a partially displaced one, so a cancelled bake can never be mistaken for a finished result and
|
||||
// committed. It exists because this is the one call in the feature that can take seconds on a
|
||||
// subdivided mesh, and without it the progress notification the Job framework puts on screen sits at
|
||||
// 0% for the whole run and offers no way to close it (its close button only appears at 100%).
|
||||
indexed_triangle_set build_texture_displacement(const indexed_triangle_set &base_mesh,
|
||||
const std::vector<TextureDisplacementLayer> &layers,
|
||||
const TextureDisplacementFacetsData &facets_data,
|
||||
const TextureDisplacementOptions &options = {});
|
||||
const TextureDisplacementOptions &options = {},
|
||||
const DisplacementProgressFn &progress = {});
|
||||
|
||||
// Convenience overload for main-thread callers: extracts the mesh/layers/paint data/options from
|
||||
// `volume` and forwards to the overload above.
|
||||
@@ -551,8 +571,10 @@ indexed_triangle_set build_texture_displacement(const ModelVolume &volume);
|
||||
// after displacing it" runs, and it is also safe to run standalone on an already baked mesh.
|
||||
// `strength` is clamped to [0, 1]; 0 iterations, an empty/mis-sized `movable`, or an all-false one
|
||||
// leave the mesh untouched.
|
||||
// `on_pass`, when set, is called with the 0-based index of each completed pass; returning false stops
|
||||
// the relaxation there, leaving the passes already done in place.
|
||||
void smooth_mesh_vertices(indexed_triangle_set &mesh, const std::vector<uint8_t> &movable, float strength,
|
||||
int iterations);
|
||||
int iterations, const DisplacementProgressFn &on_pass = {});
|
||||
|
||||
// Returns a scalar height (in mm - a displacement magnitude) at a surface point, given that point's
|
||||
// position and interpolated normal. This is what feature-adaptive subdivision samples to decide
|
||||
@@ -638,12 +660,30 @@ indexed_triangle_set subdivide_mesh_uniform(const indexed_triangle_set &mesh, fl
|
||||
// the input triangle that output triangle i descends from (children inherit their parent's index), so
|
||||
// a caller can carry per-triangle data - e.g. a paint mask - across the topology change without a
|
||||
// geometric remap.
|
||||
//
|
||||
// `refine_region` is a **bitmask** per input triangle, not a plain flag:
|
||||
// bit 0 (REFINE_PAINTED) - inside the painted area: refine by the length baseline and, in feature
|
||||
// mode, by the chord-error test.
|
||||
// bit 1 (REFINE_BORDER) - inside the band straddling the paint's edge: refine by
|
||||
// `border_edge_length_mm` alone.
|
||||
// A value of 1 therefore means exactly what a plain 1 always meant, and 0 still means "never touch
|
||||
// this triangle except through the conformal closure".
|
||||
//
|
||||
// The border band exists because the chord-error test is blind to the one discontinuity the bake
|
||||
// actually creates. `make_combined_displacement_sampler()` evaluates the height field everywhere,
|
||||
// with no per-point paint test, so where the paint *stops* it keeps reporting full relief - smooth
|
||||
// and low-curvature - while the baked surface steps from full displacement to zero. The test sees no
|
||||
// error there and leaves the transition at whatever density the input had, which is what turns the
|
||||
// rim of an unpainted island into a ring of large, steeply tilted triangles. Refining that band by
|
||||
// plain edge length is bounded (it is a thin ring, and a length target always terminates) and needs
|
||||
// no paint-aware sampler.
|
||||
indexed_triangle_set subdivide_mesh_adaptive(const indexed_triangle_set &mesh,
|
||||
const std::vector<uint8_t> &refine_region,
|
||||
float target_edge_length_mm, int max_triangles = 1000000,
|
||||
std::vector<int> *out_source = nullptr,
|
||||
const HeightFieldSampler &sampler = nullptr,
|
||||
float chord_tolerance_mm = 0.f, float min_edge_length_mm = 0.f);
|
||||
float chord_tolerance_mm = 0.f, float min_edge_length_mm = 0.f,
|
||||
float border_edge_length_mm = 0.f);
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
|
||||
@@ -1519,9 +1519,11 @@ void TriangleSelector::get_facets(std::vector<indexed_triangle_set>& facets_per_
|
||||
}
|
||||
}
|
||||
|
||||
indexed_triangle_set TriangleSelector::get_facets_strict(EnforcerBlockerType state) const
|
||||
indexed_triangle_set TriangleSelector::get_facets_strict(EnforcerBlockerType state, std::vector<int> *out_source) const
|
||||
{
|
||||
indexed_triangle_set out;
|
||||
if (out_source)
|
||||
out_source->clear();
|
||||
|
||||
size_t num_vertices = 0;
|
||||
for (const Vertex &v : m_vertices)
|
||||
@@ -1535,8 +1537,13 @@ indexed_triangle_set TriangleSelector::get_facets_strict(EnforcerBlockerType sta
|
||||
out.vertices.emplace_back(v.v);
|
||||
}
|
||||
|
||||
for (int itriangle = 0; itriangle < m_orig_size_indices; ++ itriangle)
|
||||
for (int itriangle = 0; itriangle < m_orig_size_indices; ++ itriangle) {
|
||||
this->get_facets_strict_recursive(m_triangles[itriangle], m_neighbors[itriangle], state, out.indices);
|
||||
// Everything the recursion just appended came from this original triangle, whatever depth it
|
||||
// was split to. Recording it here keeps the recursive helpers untouched.
|
||||
if (out_source)
|
||||
out_source->resize(out.indices.size(), itriangle);
|
||||
}
|
||||
|
||||
for (auto &triangle : out.indices)
|
||||
for (int i = 0; i < 3; ++ i)
|
||||
|
||||
@@ -332,7 +332,14 @@ public:
|
||||
// Get facets at a given state. Don't triangulate T-joints.
|
||||
indexed_triangle_set get_facets(EnforcerBlockerType state) const;
|
||||
// Get facets at a given state. Triangulate T-joints.
|
||||
indexed_triangle_set get_facets_strict(EnforcerBlockerType state) const;
|
||||
// Sub-triangles in `state`, with the *whole* mesh's referenced vertex array (only .indices is
|
||||
// filtered by state, so two calls with different states share one indexing).
|
||||
//
|
||||
// `out_source`, when given, is filled parallel to the returned .indices with the index of the
|
||||
// original mesh triangle each sub-triangle came from. That is what lets a caller carry partial
|
||||
// paint - the pieces of a triangle a brush stroke only partly covered - across a refinement of
|
||||
// the same surface, instead of having to round each source triangle to wholly painted or not.
|
||||
indexed_triangle_set get_facets_strict(EnforcerBlockerType state, std::vector<int> *out_source = nullptr) const;
|
||||
// Get edges around the selected area by seed fill.
|
||||
std::vector<Vec2i32> get_seed_fill_contour() const;
|
||||
|
||||
|
||||
@@ -68,13 +68,43 @@ constexpr float ImGuiLogSlider = float(ImGuiSliderFlags_Logarithmic);
|
||||
// frequencies that would alias, and it cuts the VRAM these hold by ~16x as a bonus.
|
||||
constexpr int THUMBNAIL_MAX_PX = 128;
|
||||
|
||||
std::unique_ptr<GLTexture> upload_height_thumbnail(const DecodedHeightTexture &decoded)
|
||||
// Everything above is about drawing a ~48 px panel row, and none of it applies to the height texture
|
||||
// the fast-preview *shader* samples: that one is magnified across the model, not minified into a
|
||||
// row, and every texel it loses is relief the preview cannot show. It gets its own upload at (up to)
|
||||
// this size, so the bump preview reads the same height field the bake does instead of a 128 px box
|
||||
// blur of it - which is what made Fast look flatter and softer than the result it was previewing.
|
||||
constexpr int HEIGHT_TEX_MAX_PX = 2048;
|
||||
|
||||
// How many rings of vertex-adjacent triangles either side of the paint's edge join the border refine
|
||||
// band (see collect_paint_region()). Two is enough to grade the size change without the band's own
|
||||
// cost growing to matter: it is a ring around a perimeter, not an area.
|
||||
constexpr int BORDER_BAND_RINGS = 2;
|
||||
|
||||
// Is `p` inside triangle `t`, given that it already lies in the triangle's plane? Barycentric via the
|
||||
// three sub-triangle cross products, compared against the whole triangle's normal. Used to carry a
|
||||
// partly painted source triangle's coverage onto the children of a subdivision, which are coplanar
|
||||
// with it by construction (subdivision only adds edge midpoints).
|
||||
bool point_in_triangle_coplanar(const Vec3f &p, const std::array<Vec3f, 3> &t)
|
||||
{
|
||||
const Vec3f n = (t[1] - t[0]).cross(t[2] - t[0]);
|
||||
const float n2 = n.squaredNorm();
|
||||
if (n2 < 1e-20f)
|
||||
return false; // degenerate: it covers no area, so nothing is inside it
|
||||
// A small negative tolerance, scaled by the triangle, keeps a centroid sitting exactly on a shared
|
||||
// edge from falling through the gap between two neighbouring pieces.
|
||||
const float eps = -1e-4f * n2;
|
||||
return (t[1] - t[0]).cross(p - t[0]).dot(n) >= eps &&
|
||||
(t[2] - t[1]).cross(p - t[1]).dot(n) >= eps &&
|
||||
(t[0] - t[2]).cross(p - t[2]).dot(n) >= eps;
|
||||
}
|
||||
|
||||
std::unique_ptr<GLTexture> upload_height_thumbnail(const DecodedHeightTexture &decoded, int max_px = THUMBNAIL_MAX_PX)
|
||||
{
|
||||
if (decoded.empty())
|
||||
return nullptr;
|
||||
|
||||
// Preserve aspect; never upscale a texture that is already small.
|
||||
const int scale = std::max(1, (std::max(decoded.width, decoded.height) + THUMBNAIL_MAX_PX - 1) / THUMBNAIL_MAX_PX);
|
||||
const int scale = std::max(1, (std::max(decoded.width, decoded.height) + max_px - 1) / max_px);
|
||||
const int w = std::max(1, decoded.width / scale);
|
||||
const int h = std::max(1, decoded.height / scale);
|
||||
|
||||
@@ -155,6 +185,12 @@ void GLGizmoTextureDisplacement::on_shutdown()
|
||||
m_parent.toggle_model_objects_visibility(true);
|
||||
m_preview_glmodel.reset();
|
||||
m_bump_preview_glmodel.reset();
|
||||
m_paint_overlay_glmodel.reset();
|
||||
m_paint_overlay_dirty = false;
|
||||
// Any preview still in flight is superseded: bumping the shared counter makes it abort at its next
|
||||
// progress poll, and its completion handler then finds nothing to do.
|
||||
m_preview_generation->fetch_add(1);
|
||||
m_preview_job_pending = false;
|
||||
m_uvcheck_glmodel.reset();
|
||||
m_wireframe_overlay_glmodel.reset();
|
||||
m_wireframe_overlay_vcount = 0;
|
||||
@@ -221,32 +257,50 @@ void GLGizmoTextureDisplacement::render_painter_gizmo()
|
||||
//
|
||||
// The bump preview is different: it never actually moves geometry (it's a shading trick), so
|
||||
// its depth is identical to the overlay's *everywhere*, not just in the unpainted area - the
|
||||
// depth-biased overlay would win the depth test across the whole surface and hide the bump
|
||||
// shading entirely. So the overlay is skipped for it; the bump shading itself is the only
|
||||
// feedback in that mode (still fine for painting, since render_cursor() below shows the brush).
|
||||
// depth-biased opaque overlay would win the depth test across the whole surface and hide the bump
|
||||
// shading entirely. So render_triangles() is skipped for it. What is *not* skipped is
|
||||
// render_paint_overlay(): leaving the bump shading as the only paint feedback meant a stroke that
|
||||
// erased paint, or added it with no texture picked, changed nothing on screen until the whole
|
||||
// preview rebuilt at stroke end - and in the true-displacement view the opaque overlay is hidden
|
||||
// by the raised surface for the same reason. The translucent tint covers both cases.
|
||||
// Coalesced bump rebuild from an in-progress UV island drag (see on_island_edited): done here, at
|
||||
// most once per drawn frame, rather than synchronously in the UV canvas's mouse-move handler.
|
||||
if (m_use_bump_preview && m_bump_preview_dirty) {
|
||||
rebuild_bump_preview_mesh();
|
||||
m_bump_preview_dirty = false;
|
||||
}
|
||||
const bool use_bump = m_use_bump_preview && m_bump_preview_glmodel.is_initialized();
|
||||
// Same coalescing for the paint tint, but on its own flag: a stroke marks this every mouse move
|
||||
// (see on_mouse()) and it only costs the painted patch, whereas the bump mesh also carries every
|
||||
// unpainted triangle of the volume and stays on the stroke-end cadence.
|
||||
if (m_paint_overlay_dirty) {
|
||||
rebuild_paint_overlay();
|
||||
m_paint_overlay_dirty = false;
|
||||
}
|
||||
// is_initialized() alone is not enough: render_bump_preview_mesh() also needs an active layer
|
||||
// with a decoded texture and a compiled shader, and bails silently without them. Hiding the real
|
||||
// volume for a bump pass that then draws nothing is what made the model vanish - most obviously
|
||||
// with zero layers, but equally with a layer that has no texture picked yet.
|
||||
const bool use_bump = m_use_bump_preview && m_bump_preview_glmodel.is_initialized() && bump_preview_ready();
|
||||
const bool use_true_preview = !use_bump && m_preview_glmodel.is_initialized();
|
||||
// In Checker/Distortion mode the UV-check overlay *is* the surface visualization the user is
|
||||
// looking at, so the opaque paint-selection highlight must not be drawn on top of it - same
|
||||
// reasoning as skipping it for the bump preview (see bug #12). Without this the painted area
|
||||
// covers the checker/heatmap and it can't be seen.
|
||||
const bool show_paint_overlay = m_uv_check_mode == UVCheckMode::None;
|
||||
|
||||
// Hide the real volume only when something is actually going to be drawn in its place; otherwise
|
||||
// put it back. Getting this wrong leaves an invisible model, so it is decided once, here, rather
|
||||
// than per branch below.
|
||||
m_parent.toggle_model_objects_visibility(true);
|
||||
if (use_bump || use_true_preview) {
|
||||
if (ModelVolume *mv = texture_volume())
|
||||
m_parent.toggle_model_objects_visibility(false, m_c->selection_info()->model_object(),
|
||||
m_c->selection_info()->get_active_instance(), mv);
|
||||
}
|
||||
|
||||
if (use_bump) {
|
||||
m_parent.toggle_model_objects_visibility(true);
|
||||
if (ModelVolume *mv = texture_volume())
|
||||
m_parent.toggle_model_objects_visibility(false, m_c->selection_info()->model_object(),
|
||||
m_c->selection_info()->get_active_instance(), mv);
|
||||
render_bump_preview_mesh();
|
||||
} else if (m_preview_glmodel.is_initialized()) {
|
||||
m_parent.toggle_model_objects_visibility(true);
|
||||
if (ModelVolume *mv = texture_volume())
|
||||
m_parent.toggle_model_objects_visibility(false, m_c->selection_info()->model_object(),
|
||||
m_c->selection_info()->get_active_instance(), mv);
|
||||
} else if (use_true_preview) {
|
||||
render_preview_mesh();
|
||||
|
||||
if (show_paint_overlay) {
|
||||
@@ -259,6 +313,13 @@ void GLGizmoTextureDisplacement::render_painter_gizmo()
|
||||
render_triangles(selection);
|
||||
}
|
||||
|
||||
// The translucent paint tint. Needed in the bump view because the opaque highlight above is
|
||||
// skipped there, and in the true-displacement view because the displaced surface rises *above*
|
||||
// the undisplaced overlay geometry and hides it exactly where the relief is strongest - in both
|
||||
// cases leaving an erase stroke with no visible effect until the next full preview rebuild.
|
||||
if (show_paint_overlay && (use_bump || use_true_preview))
|
||||
render_paint_overlay();
|
||||
|
||||
// Diagnostic overlays, drawn on top of whatever preview is active (both pull toward the camera
|
||||
// with a polygon offset so they win the depth test against the coincident surface).
|
||||
if (m_uv_check_mode != UVCheckMode::None)
|
||||
@@ -290,7 +351,16 @@ bool GLGizmoTextureDisplacement::on_mouse(const wxMouseEvent &mouse_event)
|
||||
return on_mouse_seam(mouse_event);
|
||||
if (m_adjust_texture_mode)
|
||||
return on_mouse_adjust_texture(mouse_event);
|
||||
return GLGizmoPainterBase::on_mouse(mouse_event);
|
||||
|
||||
const bool handled = GLGizmoPainterBase::on_mouse(mouse_event);
|
||||
// A consumed drag/click is a paint (or erase) event: the base class has already updated the live
|
||||
// TriangleSelector, but nothing is flushed to the model - and so nothing rebuilds - until the
|
||||
// stroke ends. Mark the tint stale so it follows the brush from the first frame instead. Only the
|
||||
// flag is set here; the rebuild is coalesced to once per drawn frame in render_painter_gizmo().
|
||||
if (handled && (mouse_event.Dragging() || mouse_event.LeftDown() || mouse_event.RightDown() ||
|
||||
mouse_event.LeftUp() || mouse_event.RightUp()))
|
||||
m_paint_overlay_dirty = true;
|
||||
return handled;
|
||||
}
|
||||
|
||||
bool GLGizmoTextureDisplacement::on_mouse_seam(const wxMouseEvent &mouse_event)
|
||||
@@ -707,11 +777,29 @@ void GLGizmoTextureDisplacement::render_preview_mesh()
|
||||
shader->stop_using();
|
||||
}
|
||||
|
||||
float GLGizmoTextureDisplacement::layer_texture_aspect(const TextureDisplacementLayer &layer)
|
||||
{
|
||||
// decode_height_texture() is cached on the image_data allocation, so this is a hash lookup rather
|
||||
// than a PNG decode - cheap enough to call per rebuild.
|
||||
const DecodedHeightTexture tex = decode_height_texture(layer);
|
||||
return (tex.width > 0 && tex.height > 0) ? float(tex.width) / float(tex.height) : 1.f;
|
||||
}
|
||||
|
||||
std::vector<Vec2f> GLGizmoTextureDisplacement::compute_layer_vertex_uvs(const indexed_triangle_set &patch,
|
||||
const TextureDisplacementLayer &layer) const
|
||||
{
|
||||
if (layer.projection_method == TextureProjectionMethod::LSCM)
|
||||
return compute_lscm_uvs(patch, layer); // one final uv per patch vertex (0 where unassigned)
|
||||
const float aspect = layer_texture_aspect(layer);
|
||||
if (layer.projection_method == TextureProjectionMethod::LSCM) {
|
||||
// compute_lscm_uvs() returns the unwrap's own (raw, mm) coordinates with the island placement
|
||||
// folded in - it does *not* apply the layer's tiling/rotation/offset. The bake applies those
|
||||
// on top (sample_layer_height()'s lscm branch runs the result through sample_at()), so the
|
||||
// shader's precomputed-uv path has to as well, or the fast preview samples millimetre-valued
|
||||
// coordinates as if they were uv and shows the texture at a wildly wrong scale.
|
||||
std::vector<Vec2f> uv = compute_lscm_uvs(patch, layer);
|
||||
for (Vec2f &p : uv)
|
||||
p = apply_uv_transform(p, layer, aspect);
|
||||
return uv;
|
||||
}
|
||||
if (layer.projection_method == TextureProjectionMethod::ViewProjected) {
|
||||
std::vector<Vec2f> uv(patch.vertices.size());
|
||||
for (size_t vi = 0; vi < patch.vertices.size(); ++vi) {
|
||||
@@ -725,7 +813,7 @@ std::vector<Vec2f> GLGizmoTextureDisplacement::compute_layer_vertex_uvs(const in
|
||||
}
|
||||
const Vec2f planar(patch.vertices[vi].dot(layer.view_project_right),
|
||||
patch.vertices[vi].dot(layer.view_project_up));
|
||||
uv[vi] = apply_uv_transform(planar, layer);
|
||||
uv[vi] = apply_uv_transform(planar, layer, aspect);
|
||||
}
|
||||
return uv;
|
||||
}
|
||||
@@ -913,11 +1001,13 @@ void GLGizmoTextureDisplacement::render_bump_preview_mesh()
|
||||
if (layer == nullptr || layer->empty())
|
||||
return;
|
||||
|
||||
// Reuses the layer-list panel's already-decoded, already-uploaded GPU thumbnail (smoothing-aware),
|
||||
// whose grayscale value lives in the R channel exactly as the shader samples it. Its width/height
|
||||
// are read straight off the texture - decoding the PNG here every frame would re-run the smoothing
|
||||
// blur on every camera move, which is what tanked the frame rate at high smoothing.
|
||||
GLTexture *tex = get_layer_thumbnail(*layer);
|
||||
// Full-resolution height upload (smoothing-aware), whose grayscale value lives in the R channel
|
||||
// exactly as the shader samples it. Deliberately *not* the layer-list panel's thumbnail: that one
|
||||
// is box-filtered down to 128 px for a ~48 px row, and feeding it to the shader cost the preview
|
||||
// three quarters of the height map's detail - and, since height_tex_texel is derived from it, also
|
||||
// flattened the shading gradient and made the parallax march skip itself at angles where it should
|
||||
// run. Cached on the image_data pointer + smoothing, so no PNG is decoded per frame.
|
||||
GLTexture *tex = get_layer_height_texture(*layer);
|
||||
if (tex == nullptr || tex->get_width() <= 0 || tex->get_height() <= 0)
|
||||
return;
|
||||
|
||||
@@ -942,10 +1032,36 @@ void GLGizmoTextureDisplacement::render_bump_preview_mesh()
|
||||
shader->set_uniform("volume_mirrored", trafo_matrix.matrix().determinant() < 0.0);
|
||||
glsafe(::glActiveTexture(GL_TEXTURE0));
|
||||
glsafe(::glBindTexture(GL_TEXTURE_2D, tex->get_id()));
|
||||
// Match DecodedHeightTexture::sample()'s tiling. The sampler's wrap mode is the only place the
|
||||
// GPU path can express this, and nothing ever set it - so it sat at GL_REPEAT no matter what the
|
||||
// layer said: a MirroredRepeat layer previewed as a plain repeat, and a layer with tiling *off*
|
||||
// previewed as an endless tiling where the bake produces one placement and nothing around it.
|
||||
// CLAMP_TO_BORDER with a zero border is the exact analogue of sample()'s "outside [0,1) is 0".
|
||||
// GL_CLAMP_TO_BORDER is desktop-GL only; on ES the nearest thing is CLAMP_TO_EDGE, which smears
|
||||
// the border row instead of vanishing - still much closer to the bake than an endless repeat.
|
||||
#if SLIC3R_OPENGL_ES
|
||||
const GLint no_tile_wrap = GL_CLAMP_TO_EDGE;
|
||||
#else
|
||||
const GLint no_tile_wrap = GL_CLAMP_TO_BORDER;
|
||||
#endif
|
||||
const GLint wrap = !layer->tile_enabled ? no_tile_wrap :
|
||||
(layer->tile_method == TextureTileMethod::MirroredRepeat) ? GL_MIRRORED_REPEAT :
|
||||
GL_REPEAT;
|
||||
glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, wrap));
|
||||
glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, wrap));
|
||||
#if !SLIC3R_OPENGL_ES
|
||||
if (wrap == GL_CLAMP_TO_BORDER) {
|
||||
static const GLfloat border[4] = { 0.f, 0.f, 0.f, 0.f };
|
||||
glsafe(::glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, border));
|
||||
}
|
||||
#endif // !SLIC3R_OPENGL_ES
|
||||
shader->set_uniform("height_tex", 0);
|
||||
shader->set_uniform("height_tex_texel", Vec2f(1.f / float(tex->get_width()), 1.f / float(tex->get_height())));
|
||||
shader->set_uniform("depth_mm", layer->depth_mm);
|
||||
shader->set_uniform("tiling_scale", layer->tiling_scale);
|
||||
// Read off the uploaded texture rather than the decoded one: they are the same image, and this is
|
||||
// the aspect the sampler will actually see.
|
||||
shader->set_uniform("tex_aspect", float(tex->get_width()) / float(tex->get_height()));
|
||||
shader->set_uniform("rotation_rad", layer->rotation_deg * float(M_PI) / 180.f);
|
||||
shader->set_uniform("uv_offset", layer->offset);
|
||||
shader->set_uniform("invert", layer->invert);
|
||||
@@ -969,6 +1085,100 @@ void GLGizmoTextureDisplacement::render_bump_preview_mesh()
|
||||
shader->stop_using();
|
||||
}
|
||||
|
||||
bool GLGizmoTextureDisplacement::bump_preview_ready() const
|
||||
{
|
||||
// Mirrors render_bump_preview_mesh()'s own preconditions. Kept as a separate query because the
|
||||
// caller has to know whether the bump pass will draw *before* it hides the real volume for it.
|
||||
if (m_c->selection_info() == nullptr || m_c->selection_info()->model_object() == nullptr)
|
||||
return false;
|
||||
if (texture_volume() == nullptr)
|
||||
return false;
|
||||
const TextureDisplacementLayer *layer = active_layer();
|
||||
if (layer == nullptr || layer->empty())
|
||||
return false;
|
||||
// The same texture render_bump_preview_mesh() will bind, not the panel thumbnail - the two are
|
||||
// separate caches and either can fail on its own.
|
||||
const GLTexture *tex = const_cast<GLGizmoTextureDisplacement *>(this)->get_layer_height_texture(*layer);
|
||||
if (tex == nullptr || tex->get_width() <= 0 || tex->get_height() <= 0)
|
||||
return false;
|
||||
return wxGetApp().get_shader("texture_displacement_bump") != nullptr;
|
||||
}
|
||||
|
||||
void GLGizmoTextureDisplacement::rebuild_paint_overlay()
|
||||
{
|
||||
m_paint_overlay_glmodel.reset();
|
||||
const ModelVolume *mv = texture_volume();
|
||||
if (mv == nullptr || m_triangle_selectors.empty())
|
||||
return;
|
||||
|
||||
// The *live* selector, so an in-progress stroke shows immediately - which is the whole point:
|
||||
// this is the only feedback that a brush actually added or erased anything until the (much more
|
||||
// expensive) preview catches up at stroke end.
|
||||
const indexed_triangle_set patch = m_triangle_selectors[0]->get_facets_strict(EnforcerBlockerType::ENFORCER);
|
||||
if (patch.indices.empty())
|
||||
return;
|
||||
|
||||
// In the true-displacement view the surface on screen is the *raised* one, and a tint built on
|
||||
// the flat base mesh would sink underneath it wherever the relief is deepest - which is precisely
|
||||
// where the user is looking. The bake is topology-preserving (patch vertex i is mesh vertex i, see
|
||||
// build_texture_displacement()), so the displaced positions can be read straight across. Vertices
|
||||
// the brush split live past the end of that array and keep their flat position; they sit on the
|
||||
// patch boundary, where the displacement is smallest anyway.
|
||||
const std::vector<Vec3f> *displaced = nullptr;
|
||||
if (!m_use_bump_preview && m_preview_its.vertices.size() == mv->mesh().its.vertices.size() &&
|
||||
!m_preview_its.vertices.empty())
|
||||
displaced = &m_preview_its.vertices;
|
||||
|
||||
GLModel::Geometry init_data;
|
||||
init_data.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3 };
|
||||
init_data.reserve_vertices(patch.indices.size() * 3);
|
||||
init_data.reserve_indices(patch.indices.size() * 3);
|
||||
unsigned n = 0;
|
||||
for (const stl_triangle_vertex_indices &tri : patch.indices) {
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
const size_t idx = size_t(tri[i]);
|
||||
init_data.add_vertex((displaced != nullptr && idx < displaced->size()) ? (*displaced)[idx]
|
||||
: patch.vertices[idx]);
|
||||
}
|
||||
init_data.add_triangle(n, n + 1, n + 2);
|
||||
n += 3;
|
||||
}
|
||||
m_paint_overlay_glmodel.init_from(std::move(init_data));
|
||||
// GLModel::render() re-sets "uniform_color" from this field just before drawing, so the colour
|
||||
// has to be set here rather than as a uniform at draw time.
|
||||
m_paint_overlay_glmodel.set_color(ColorRGBA(0.16f, 0.79f, 0.35f, 0.38f));
|
||||
}
|
||||
|
||||
void GLGizmoTextureDisplacement::render_paint_overlay()
|
||||
{
|
||||
const ModelObject *mo = m_c->selection_info()->model_object();
|
||||
const ModelVolume *mv = texture_volume();
|
||||
if (mo == nullptr || mv == nullptr || !m_paint_overlay_glmodel.is_initialized())
|
||||
return;
|
||||
GLShaderProgram *shader = wxGetApp().get_shader("flat");
|
||||
if (shader == nullptr)
|
||||
return;
|
||||
|
||||
const Selection &selection = m_parent.get_selection();
|
||||
const Transform3d trafo_matrix = mo->instances[selection.get_instance_idx()]->get_transformation().get_matrix() * mv->get_matrix();
|
||||
const Camera &camera = wxGetApp().plater()->get_camera();
|
||||
|
||||
shader->start_using();
|
||||
shader->set_uniform("view_model_matrix", camera.get_view_matrix() * trafo_matrix);
|
||||
shader->set_uniform("projection_matrix", camera.get_projection_matrix());
|
||||
// Translucent, and pulled toward the camera so it wins the depth test against the coincident
|
||||
// bump surface. Depth writes are off: this is a tint, and letting it own the depth buffer would
|
||||
// make the wireframe and seam overlays drawn after it fight with geometry that is not really
|
||||
// there. Blending is already enabled by render_painter_gizmo().
|
||||
glsafe(::glEnable(GL_POLYGON_OFFSET_FILL));
|
||||
glsafe(::glPolygonOffset(-1.5f, -1.5f));
|
||||
glsafe(::glDepthMask(GL_FALSE));
|
||||
m_paint_overlay_glmodel.render();
|
||||
glsafe(::glDepthMask(GL_TRUE));
|
||||
glsafe(::glDisable(GL_POLYGON_OFFSET_FILL));
|
||||
shader->stop_using();
|
||||
}
|
||||
|
||||
void GLGizmoTextureDisplacement::rebuild_uvcheck_mesh()
|
||||
{
|
||||
m_uvcheck_glmodel.reset();
|
||||
@@ -1178,10 +1388,12 @@ void GLGizmoTextureDisplacement::rebuild_preview()
|
||||
{
|
||||
// Bumped first: any in-flight job's result (captured generation from before this call) will
|
||||
// now compare unequal to m_preview_generation and be discarded when it completes, even if it
|
||||
// finishes after the job queued below.
|
||||
const uint64_t generation = ++m_preview_generation;
|
||||
// finishes after the job queued below - and, since the counter is shared with the worker, that
|
||||
// job also notices mid-run and aborts rather than computing a result nobody will use.
|
||||
m_preview_generation->fetch_add(1);
|
||||
update_uv_editor();
|
||||
rebuild_bump_preview_mesh();
|
||||
rebuild_paint_overlay();
|
||||
rebuild_uvcheck_mesh();
|
||||
rebuild_seam_overlay();
|
||||
// The adaptive subdivision preview is driven by the painted area, so it has to follow the paint
|
||||
@@ -1194,13 +1406,43 @@ void GLGizmoTextureDisplacement::rebuild_preview()
|
||||
if (mv == nullptr || !mv->is_texture_displacement_painted()) {
|
||||
m_preview_glmodel.reset();
|
||||
m_preview_its = indexed_triangle_set{}; // no displaced mesh; wireframe falls back to the base
|
||||
m_preview_job_pending = false;
|
||||
refresh_wireframe();
|
||||
return;
|
||||
}
|
||||
// In Fast/paint modes the wireframe follows the base mesh and can be built now; the true-displacement
|
||||
// view's wireframe needs the displaced mesh, which only exists once the job below completes.
|
||||
if (m_use_bump_preview)
|
||||
if (m_use_bump_preview) {
|
||||
refresh_wireframe();
|
||||
// Fast view: the shader *is* the preview, and m_preview_glmodel is never drawn. Running the
|
||||
// full CPU displacement anyway - which is what happened on every stroke and slider release -
|
||||
// was the single largest cost in the gizmo, and it bought nothing. The switch back to the
|
||||
// true-displacement view queues it (see the View row in on_render_input_window()).
|
||||
m_preview_job_pending = false;
|
||||
return;
|
||||
}
|
||||
|
||||
queue_preview_job();
|
||||
}
|
||||
|
||||
void GLGizmoTextureDisplacement::queue_preview_job()
|
||||
{
|
||||
// A job in flight when the gizmo closes still runs its completion handler, which would otherwise
|
||||
// happily queue the follow-up run it was holding - against a gizmo nobody is looking at any more.
|
||||
if (m_state != On)
|
||||
return;
|
||||
const ModelVolume *mv = texture_volume();
|
||||
if (mv == nullptr || !mv->is_texture_displacement_painted())
|
||||
return;
|
||||
|
||||
// One in flight at a time; everything requested meanwhile collapses into a single follow-up run
|
||||
// issued from the completion handler. See m_preview_job_running.
|
||||
if (m_preview_job_running) {
|
||||
m_preview_job_pending = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const uint64_t generation = m_preview_generation->load();
|
||||
|
||||
TextureDisplacementPreviewInput input;
|
||||
input.base_mesh = mv->mesh().its;
|
||||
@@ -1209,20 +1451,35 @@ void GLGizmoTextureDisplacement::rebuild_preview()
|
||||
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i)
|
||||
input.facets_data[size_t(i)] = mv->texture_displacement_facet(i).get_data();
|
||||
|
||||
m_preview_job_running = true;
|
||||
auto &worker = wxGetApp().plater()->get_ui_job_worker();
|
||||
queue_job(worker, std::make_unique<TextureDisplacementPreviewJob>(std::move(input), generation,
|
||||
queue_job(worker, std::make_unique<TextureDisplacementPreviewJob>(std::move(input), generation, m_preview_generation,
|
||||
[this](indexed_triangle_set its, uint64_t result_generation) {
|
||||
if (result_generation != m_preview_generation)
|
||||
return; // superseded by a newer edit while this was computing
|
||||
m_preview_glmodel.reset();
|
||||
if (!its.indices.empty()) {
|
||||
m_preview_job_running = false;
|
||||
if (result_generation != m_preview_generation->load()) {
|
||||
// Superseded while this was computing (it will have aborted early and come back
|
||||
// empty). Whatever the newest state is, it still needs a run.
|
||||
m_preview_job_pending = true;
|
||||
} else if (its.indices.empty()) {
|
||||
// Aborted or cancelled rather than finished - the handler runs on every outcome so
|
||||
// the in-flight latch above always clears. Keep whatever preview is already on screen
|
||||
// rather than blanking it; there is no new result to show, not a new empty one.
|
||||
} else {
|
||||
m_preview_glmodel.reset();
|
||||
m_preview_glmodel.init_from(its);
|
||||
m_preview_glmodel.set_color(GLVolume::NEUTRAL_COLOR);
|
||||
// Keep the displaced mesh so the wireframe overlay can be drawn on it (the
|
||||
// true-displacement view), then refresh the wireframe from it.
|
||||
m_preview_its = std::move(its);
|
||||
refresh_wireframe();
|
||||
// The paint tint rides the displaced surface in this view, so it follows the new mesh.
|
||||
m_paint_overlay_dirty = true;
|
||||
}
|
||||
if (m_preview_job_pending) {
|
||||
m_preview_job_pending = false;
|
||||
if (!m_use_bump_preview)
|
||||
queue_preview_job(); // no-ops if the gizmo has closed in the meantime
|
||||
}
|
||||
// Keep the displaced mesh so the wireframe overlay can be drawn on it (the true-displacement
|
||||
// view), then refresh the wireframe from it.
|
||||
m_preview_its = std::move(its);
|
||||
refresh_wireframe();
|
||||
m_parent.set_as_dirty();
|
||||
}));
|
||||
}
|
||||
@@ -1935,7 +2192,11 @@ Vec3f GLGizmoTextureDisplacement::adjust_handle_center(const TextureDisplacement
|
||||
// follows the cursor precisely, and is back on the anchor exactly when offset is zero.
|
||||
const float rad = layer.rotation_deg * float(M_PI) / 180.f;
|
||||
const float cs = std::cos(rad), sn = std::sin(rad);
|
||||
const Vec2f unrotated(layer.offset.x() * cs + layer.offset.y() * sn, -layer.offset.x() * sn + layer.offset.y() * cs);
|
||||
// Undo the non-square v scaling first - it is the last thing apply_uv_transform() does, so it is
|
||||
// the first thing to come off on the way back.
|
||||
const float aspect = layer_texture_aspect(layer);
|
||||
const Vec2f o(layer.offset.x(), (aspect > 0.f) ? layer.offset.y() / aspect : layer.offset.y());
|
||||
const Vec2f unrotated(o.x() * cs + o.y() * sn, -o.x() * sn + o.y() * cs);
|
||||
const Vec2f planar = -unrotated * layer.tiling_scale;
|
||||
|
||||
Vec3f u_axis, v_axis;
|
||||
@@ -2168,7 +2429,10 @@ bool GLGizmoTextureDisplacement::on_mouse_adjust_texture(const wxMouseEvent &mou
|
||||
const Vec2f delta_scaled = delta_planar * scale;
|
||||
const float rad = layer->rotation_deg * float(M_PI) / 180.f;
|
||||
const float cs = std::cos(rad), sn = std::sin(rad);
|
||||
const Vec2f delta_rotated(delta_scaled.x() * cs - delta_scaled.y() * sn, delta_scaled.x() * sn + delta_scaled.y() * cs);
|
||||
Vec2f delta_rotated(delta_scaled.x() * cs - delta_scaled.y() * sn, delta_scaled.x() * sn + delta_scaled.y() * cs);
|
||||
// ...and the same v scaling apply_uv_transform() applies for a non-square texture, so the
|
||||
// handle keeps tracking the cursor exactly instead of drifting on the v axis.
|
||||
delta_rotated.y() *= layer_texture_aspect(*layer);
|
||||
// Increasing `offset` shifts which texel is sampled at a fixed world position, which
|
||||
// visually slides the pattern the *opposite* way - subtracting is this session's
|
||||
// best-effort reasoning about the direction that feels like "dragging the texture",
|
||||
@@ -2701,7 +2965,7 @@ void GLGizmoTextureDisplacement::subdivide_model()
|
||||
|
||||
bool GLGizmoTextureDisplacement::collect_paint_region(
|
||||
std::vector<uint8_t> ®ion,
|
||||
std::array<std::vector<uint8_t>, TEXTURE_DISPLACEMENT_MAX_LAYERS> *painted_tri) const
|
||||
std::array<LayerPaintMap, TEXTURE_DISPLACEMENT_MAX_LAYERS> *paint) const
|
||||
{
|
||||
const ModelVolume *mv = texture_volume();
|
||||
if (mv == nullptr)
|
||||
@@ -2711,21 +2975,24 @@ bool GLGizmoTextureDisplacement::collect_paint_region(
|
||||
const size_t nvert = its.vertices.size();
|
||||
|
||||
region.assign(ntri, 0);
|
||||
if (painted_tri)
|
||||
for (auto &pt : *painted_tri)
|
||||
pt.clear();
|
||||
if (paint)
|
||||
for (LayerPaintMap &pm : *paint)
|
||||
pm = LayerPaintMap{};
|
||||
|
||||
// Sorted-vertex-triple -> triangle index, so a fully-painted patch sub-triangle (which comes back
|
||||
// with the original mesh's own three vertex indices) can be mapped to its source triangle. Only
|
||||
// the paint carry-forward needs it, and the live subdivide preview calls this on every slider
|
||||
// frame, so it is not built for the region-only path.
|
||||
std::map<std::array<int, 3>, int> tri_by_verts;
|
||||
if (painted_tri)
|
||||
for (size_t i = 0; i < ntri; ++i) {
|
||||
std::array<int, 3> k{ its.indices[i][0], its.indices[i][1], its.indices[i][2] };
|
||||
std::sort(k.begin(), k.end());
|
||||
tri_by_verts.emplace(k, int(i));
|
||||
}
|
||||
// Twice the area of each source triangle, for the "is this one covered edge to edge" test below.
|
||||
// Only the paint carry-forward needs it, and the live subdivide preview calls this on every
|
||||
// slider frame, so it is not built for the region-only path.
|
||||
const auto tri_area2 = [](const Vec3f &a, const Vec3f &b, const Vec3f &c) {
|
||||
return (b - a).cross(c - a).norm();
|
||||
};
|
||||
std::vector<float> source_area2;
|
||||
if (paint) {
|
||||
source_area2.resize(ntri);
|
||||
for (size_t i = 0; i < ntri; ++i)
|
||||
source_area2[i] = tri_area2(its.vertices[size_t(its.indices[i][0])],
|
||||
its.vertices[size_t(its.indices[i][1])],
|
||||
its.vertices[size_t(its.indices[i][2])]);
|
||||
}
|
||||
|
||||
bool any_paint = false;
|
||||
for (int slot = 0; slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++slot) {
|
||||
@@ -2743,27 +3010,114 @@ bool GLGizmoTextureDisplacement::collect_paint_region(
|
||||
// subdivide_mesh_adaptive() already grades the size change outward on its own.
|
||||
for (const TriangleSelector::TriangleBitStreamMapping &m : data.triangles_to_split)
|
||||
if (size_t(m.triangle_idx) < ntri)
|
||||
region[m.triangle_idx] = 1;
|
||||
region[m.triangle_idx] |= REFINE_PAINTED;
|
||||
|
||||
if (painted_tri) {
|
||||
if (paint) {
|
||||
TriangleSelector sel(mv->mesh());
|
||||
sel.deserialize(data, false);
|
||||
(*painted_tri)[slot].assign(ntri, 0);
|
||||
for (const stl_triangle_vertex_indices &t : sel.get_facets_strict(EnforcerBlockerType::ENFORCER).indices) {
|
||||
// A sub-triangle produced by a *partial* stroke always carries at least one appended
|
||||
// (split) vertex, so "all three indices are original" is exactly the test for a whole,
|
||||
// fully-painted triangle - the only kind whose paint can be inherited wholesale.
|
||||
if (size_t(t[0]) >= nvert || size_t(t[1]) >= nvert || size_t(t[2]) >= nvert)
|
||||
// get_facets_strict() now reports which source triangle each sub-triangle came from, which
|
||||
// is what lets partial coverage be carried forward geometrically instead of being rounded
|
||||
// away. Note a *fully* painted source can still come back as several sub-triangles (T-joint
|
||||
// splits forced by a refined neighbour), so "wholly painted" is an area test, not a
|
||||
// one-piece test.
|
||||
std::vector<int> src;
|
||||
const indexed_triangle_set patch = sel.get_facets_strict(EnforcerBlockerType::ENFORCER, &src);
|
||||
|
||||
LayerPaintMap &pm = (*paint)[slot];
|
||||
pm.full.assign(ntri, 0);
|
||||
pm.part_start.assign(ntri + 1, 0);
|
||||
|
||||
std::vector<float> covered2(ntri, 0.f);
|
||||
for (size_t j = 0; j < patch.indices.size() && j < src.size(); ++j) {
|
||||
if (size_t(src[j]) >= ntri)
|
||||
continue;
|
||||
std::array<int, 3> k{ t[0], t[1], t[2] };
|
||||
std::sort(k.begin(), k.end());
|
||||
if (auto it = tri_by_verts.find(k); it != tri_by_verts.end())
|
||||
(*painted_tri)[slot][it->second] = 1;
|
||||
const stl_triangle_vertex_indices &t = patch.indices[j];
|
||||
covered2[size_t(src[j])] += tri_area2(patch.vertices[size_t(t[0])], patch.vertices[size_t(t[1])],
|
||||
patch.vertices[size_t(t[2])]);
|
||||
}
|
||||
for (size_t i = 0; i < ntri; ++i)
|
||||
pm.full[i] = (source_area2[i] > 0.f && covered2[i] >= 0.999f * source_area2[i]) ? 1 : 0;
|
||||
|
||||
// Only partly covered sources need their pieces kept - a full one answers every query with
|
||||
// "painted", and an untouched one with "not painted".
|
||||
for (size_t j = 0; j < patch.indices.size() && j < src.size(); ++j)
|
||||
if (size_t(src[j]) < ntri && !pm.full[size_t(src[j])])
|
||||
++pm.part_start[size_t(src[j]) + 1];
|
||||
for (size_t i = 0; i < ntri; ++i)
|
||||
pm.part_start[i + 1] += pm.part_start[i];
|
||||
pm.part.resize(size_t(pm.part_start[ntri]));
|
||||
{
|
||||
std::vector<int> fill(pm.part_start.begin(), pm.part_start.begin() + ntri);
|
||||
for (size_t j = 0; j < patch.indices.size() && j < src.size(); ++j) {
|
||||
const size_t S = size_t(src[j]);
|
||||
if (S >= ntri || pm.full[S])
|
||||
continue;
|
||||
const stl_triangle_vertex_indices &t = patch.indices[j];
|
||||
pm.part[size_t(fill[S]++)] = { patch.vertices[size_t(t[0])], patch.vertices[size_t(t[1])],
|
||||
patch.vertices[size_t(t[2])] };
|
||||
}
|
||||
}
|
||||
}
|
||||
any_paint = true;
|
||||
}
|
||||
return any_paint;
|
||||
if (!any_paint)
|
||||
return false;
|
||||
|
||||
// The band straddling the paint's edge. The bake steps the surface from full displacement to zero
|
||||
// across it, and nothing else in the refinement criteria can see that step: the chord-error
|
||||
// sampler has no per-point paint test, so just outside the paint it keeps reporting the same
|
||||
// smooth height field and reports no error at all. Left alone, the transition therefore stays at
|
||||
// whatever density the input had - which is what makes the rim of an unpainted island a ring of
|
||||
// big, steeply tilted triangles.
|
||||
//
|
||||
// Seeded from the vertices shared by a painted and an unpainted triangle (the actual edge of the
|
||||
// paint) and grown outward over vertex adjacency, so the band covers both sides of the step.
|
||||
if (BORDER_BAND_RINGS > 0) {
|
||||
// Vertex -> incident triangles, CSR-style (counted, prefix-summed, filled). This runs on every
|
||||
// frame of the subdivide preview's sliders, so it must not allocate a small vector per vertex.
|
||||
std::vector<int> vstart(nvert + 1, 0);
|
||||
for (size_t i = 0; i < ntri; ++i)
|
||||
for (int k = 0; k < 3; ++k)
|
||||
if (size_t(its.indices[i][k]) < nvert)
|
||||
++vstart[size_t(its.indices[i][k]) + 1];
|
||||
for (size_t v = 0; v < nvert; ++v)
|
||||
vstart[v + 1] += vstart[v];
|
||||
std::vector<int> vtri(size_t(vstart[nvert]), 0);
|
||||
{
|
||||
std::vector<int> fill(vstart.begin(), vstart.begin() + nvert);
|
||||
for (size_t i = 0; i < ntri; ++i)
|
||||
for (int k = 0; k < 3; ++k)
|
||||
if (size_t(its.indices[i][k]) < nvert)
|
||||
vtri[size_t(fill[size_t(its.indices[i][k])]++)] = int(i);
|
||||
}
|
||||
|
||||
// A vertex used by both a painted and an unpainted triangle sits exactly on the paint's edge.
|
||||
std::vector<uint8_t> ring_vertex(nvert, 0);
|
||||
for (size_t v = 0; v < nvert; ++v) {
|
||||
bool painted = false, unpainted = false;
|
||||
for (int k = vstart[v]; k < vstart[v + 1]; ++k)
|
||||
((region[size_t(vtri[size_t(k)])] & REFINE_PAINTED) ? painted : unpainted) = true;
|
||||
ring_vertex[v] = (painted && unpainted) ? 1 : 0;
|
||||
}
|
||||
|
||||
for (int ring = 0; ring < BORDER_BAND_RINGS; ++ring) {
|
||||
std::vector<uint8_t> next = ring_vertex;
|
||||
for (size_t v = 0; v < nvert; ++v) {
|
||||
if (!ring_vertex[v])
|
||||
continue;
|
||||
for (int k = vstart[v]; k < vstart[v + 1]; ++k) {
|
||||
const size_t t = size_t(vtri[size_t(k)]);
|
||||
region[t] |= REFINE_BORDER;
|
||||
// Grow through this triangle's other corners, for the following ring.
|
||||
for (int c = 0; c < 3; ++c)
|
||||
if (size_t(its.indices[t][c]) < nvert)
|
||||
next[size_t(its.indices[t][c])] = 1;
|
||||
}
|
||||
}
|
||||
ring_vertex.swap(next);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool GLGizmoTextureDisplacement::plan_adaptive_subdivision(const ModelVolume &mv, SubdivisionPlan &out) const
|
||||
@@ -2772,7 +3126,7 @@ bool GLGizmoTextureDisplacement::plan_adaptive_subdivision(const ModelVolume &mv
|
||||
return false;
|
||||
|
||||
std::vector<uint8_t> region;
|
||||
if (!collect_paint_region(region, &out.painted_tri))
|
||||
if (!collect_paint_region(region, &out.paint))
|
||||
return false;
|
||||
|
||||
// Feature-adaptive: sample the combined displacement so refinement follows texture curvature. A
|
||||
@@ -2796,7 +3150,7 @@ bool GLGizmoTextureDisplacement::plan_adaptive_subdivision(const ModelVolume &mv
|
||||
// baseline - otherwise the control would be meaningless (or a dead end) on a dense model.
|
||||
out.refined = subdivide_mesh_adaptive(mv.mesh().its, region, m_subdivide_target_mm,
|
||||
int(mv.mesh().its.indices.size()) + m_subdivide_budget_k * 1000,
|
||||
&out.source, sampler, tol, floor);
|
||||
&out.source, sampler, tol, floor, m_subdivide_border_mm);
|
||||
}
|
||||
return out.refined.indices.size() != mv.mesh().its.indices.size();
|
||||
}
|
||||
@@ -2811,15 +3165,33 @@ void GLGizmoTextureDisplacement::apply_adaptive_subdivision(ModelVolume &mv, Sub
|
||||
mv.calculate_convex_hull();
|
||||
mv.restore_painting(saved_painting); // resets extra facets (incl. texture-displacement) + remaps the rest
|
||||
|
||||
// Carry each layer's paint onto the new mesh: a new triangle is painted iff its source triangle
|
||||
// was fully painted in that layer. Children inherit their parent's source, so this is exact.
|
||||
// Carry each layer's paint onto the new mesh. Children inherit their parent's source triangle, and
|
||||
// subdivision only ever adds edge midpoints, so every new triangle lies inside its source and on
|
||||
// the same surface - which means the source's painted *pieces* can be queried directly by point
|
||||
// containment. That is what keeps a brush outline smooth: rounding each source to wholly painted
|
||||
// or not instead leaves a ragged fringe of isolated triangles along any curved boundary, and the
|
||||
// refined mesh then reproduces that fringe exactly rather than hiding it.
|
||||
const indexed_triangle_set &new_its = mv.mesh().its;
|
||||
for (int slot = 0; slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++slot) {
|
||||
if (plan.painted_tri[size_t(slot)].empty())
|
||||
const LayerPaintMap &pm = plan.paint[size_t(slot)];
|
||||
if (pm.empty())
|
||||
continue;
|
||||
TriangleSelector sel(mv.mesh());
|
||||
for (size_t i = 0; i < plan.source.size(); ++i)
|
||||
if (plan.painted_tri[size_t(slot)][size_t(plan.source[i])])
|
||||
for (size_t i = 0; i < plan.source.size() && i < new_its.indices.size(); ++i) {
|
||||
const size_t S = size_t(plan.source[i]);
|
||||
if (S >= pm.full.size())
|
||||
continue;
|
||||
bool painted = pm.full[S] != 0;
|
||||
if (!painted && pm.part_start[S] != pm.part_start[S + 1]) {
|
||||
const stl_triangle_vertex_indices &t = new_its.indices[i];
|
||||
const Vec3f centroid = (new_its.vertices[size_t(t[0])] + new_its.vertices[size_t(t[1])] +
|
||||
new_its.vertices[size_t(t[2])]) / 3.f;
|
||||
for (int k = pm.part_start[S]; k < pm.part_start[S + 1] && !painted; ++k)
|
||||
painted = point_in_triangle_coplanar(centroid, pm.part[size_t(k)]);
|
||||
}
|
||||
if (painted)
|
||||
sel.set_facet(int(i), EnforcerBlockerType::ENFORCER);
|
||||
}
|
||||
mv.texture_displacement_facet(slot).set(sel);
|
||||
}
|
||||
}
|
||||
@@ -3063,7 +3435,7 @@ void GLGizmoTextureDisplacement::rebuild_subdivide_preview()
|
||||
const float floor = m_subdivide_feature ? m_subdivide_min_edge_mm : 0.f;
|
||||
its = subdivide_mesh_adaptive(mv->mesh().its, region, m_subdivide_target_mm,
|
||||
int(mv->mesh().its.indices.size()) + m_subdivide_budget_k * 1000,
|
||||
nullptr, sampler, tol, floor);
|
||||
nullptr, sampler, tol, floor, m_subdivide_border_mm);
|
||||
} else {
|
||||
if (m_subdivide_count < 1)
|
||||
return;
|
||||
@@ -3135,6 +3507,26 @@ GLTexture *GLGizmoTextureDisplacement::get_layer_thumbnail(const TextureDisplace
|
||||
return m_thumbnails[slot].get();
|
||||
}
|
||||
|
||||
GLTexture *GLGizmoTextureDisplacement::get_layer_height_texture(const TextureDisplacementLayer &layer)
|
||||
{
|
||||
if (layer.empty())
|
||||
return nullptr;
|
||||
|
||||
// One slot, not one per layer: the bump shader only ever shades the *active* layer, so a single
|
||||
// full-resolution upload is enough and the VRAM cost stays at one texture rather than eight.
|
||||
if (m_height_tex && m_height_tex_source == layer.image_data.get() && m_height_tex_smoothing == layer.smoothing)
|
||||
return m_height_tex.get();
|
||||
|
||||
std::unique_ptr<GLTexture> texture = upload_height_thumbnail(decode_height_texture(layer), HEIGHT_TEX_MAX_PX);
|
||||
if (!texture)
|
||||
return nullptr;
|
||||
|
||||
m_height_tex = std::move(texture);
|
||||
m_height_tex_source = layer.image_data.get();
|
||||
m_height_tex_smoothing = layer.smoothing;
|
||||
return m_height_tex.get();
|
||||
}
|
||||
|
||||
void GLGizmoTextureDisplacement::bake(bool own_snapshot)
|
||||
{
|
||||
ModelVolume *mv = texture_volume();
|
||||
@@ -3177,6 +3569,10 @@ static constexpr float STD_REMESH_SHARP_DEG = 40.f;
|
||||
static constexpr float STD_SUBDIV_MAX_EDGE_MM = 20.f;
|
||||
static constexpr float STD_SUBDIV_DETAIL_MM = 0.02f;
|
||||
static constexpr float STD_SUBDIV_MIN_EDGE_MM = 0.02f;
|
||||
// Edge length the band straddling the paint's edge is refined to. This is the one number that decides
|
||||
// how clean the rim of an unpainted island looks: the bake steps the surface from full displacement to
|
||||
// zero across that band, and nothing else in the criteria can see the step (see collect_paint_region()).
|
||||
static constexpr float STD_SUBDIV_BORDER_MM = 0.4f;
|
||||
|
||||
bool GLGizmoTextureDisplacement::apply_standard_mode_presets(ModelVolume *mv)
|
||||
{
|
||||
@@ -3197,6 +3593,7 @@ bool GLGizmoTextureDisplacement::apply_standard_mode_presets(ModelVolume *mv)
|
||||
pin(m_subdivide_target_mm, STD_SUBDIV_MAX_EDGE_MM);
|
||||
pin(m_subdivide_detail_mm, STD_SUBDIV_DETAIL_MM);
|
||||
pin(m_subdivide_min_edge_mm, STD_SUBDIV_MIN_EDGE_MM);
|
||||
pin(m_subdivide_border_mm, STD_SUBDIV_BORDER_MM);
|
||||
// Deliberately *not* pinned: the triangle budget stays visible and editable in Standard mode, so
|
||||
// pinning it would fight the user's own slider every frame.
|
||||
pin(m_remesh_target_edge_mm, STD_REMESH_EDGE_MM);
|
||||
@@ -3226,6 +3623,7 @@ void GLGizmoTextureDisplacement::bake_standard()
|
||||
const bool do_remesh = plan_remesh(*mv, STD_REMESH_EDGE_MM, STD_REMESH_SHARP_DEG, remeshed);
|
||||
|
||||
Plater *plater = wxGetApp().plater();
|
||||
bool paint_transfer_failed = false;
|
||||
{
|
||||
// ONE undo step for the whole pipeline. take_snapshot() records the state *before* the change,
|
||||
// so a single Undo goes all the way back to the untouched mesh - which is the only thing "undo
|
||||
@@ -3241,17 +3639,19 @@ void GLGizmoTextureDisplacement::bake_standard()
|
||||
|
||||
// The remesh carries the paint across spatially, but if that remap came back empty the rest of
|
||||
// the pipeline has nothing to work from - stop here rather than silently baking a flat mesh.
|
||||
if (!mv->is_texture_displacement_painted()) {
|
||||
show_error(nullptr, _u8L("The painted area could not be transferred onto the remeshed model. Undo, "
|
||||
"then switch to Pro mode to prepare the mesh before painting."));
|
||||
return;
|
||||
}
|
||||
// Note this cannot just `return`: the remesh above has already replaced the volume's mesh, so
|
||||
// the scene and the gizmo's own TriangleSelectors still have to be brought back into step with
|
||||
// it below. Returning from here left the gizmo painting and raycasting against a mesh that no
|
||||
// longer existed.
|
||||
paint_transfer_failed = !mv->is_texture_displacement_painted();
|
||||
|
||||
// Planning the subdivision has to happen inside the snapshot because it reads the mesh the
|
||||
// remesh just produced. It is the expensive step, but by here we are committed anyway.
|
||||
SubdivisionPlan plan;
|
||||
if (plan_adaptive_subdivision(*mv, plan))
|
||||
apply_adaptive_subdivision(*mv, std::move(plan));
|
||||
if (!paint_transfer_failed) {
|
||||
SubdivisionPlan plan;
|
||||
if (plan_adaptive_subdivision(*mv, plan))
|
||||
apply_adaptive_subdivision(*mv, std::move(plan));
|
||||
}
|
||||
}
|
||||
|
||||
if (ObjectList *obj_list = wxGetApp().obj_list()) {
|
||||
@@ -3264,6 +3664,12 @@ void GLGizmoTextureDisplacement::bake_standard()
|
||||
update_from_model_object(false); // reload selectors against the prepared mesh + carried paint
|
||||
m_parent.set_as_dirty();
|
||||
|
||||
if (paint_transfer_failed) {
|
||||
show_error(nullptr, _u8L("The painted area could not be transferred onto the remeshed model. Undo, "
|
||||
"then switch to Pro mode to prepare the mesh before painting."));
|
||||
return;
|
||||
}
|
||||
|
||||
// ... and finally the displacement itself, in the background exactly as the Pro-mode button does -
|
||||
// except that it commits into the snapshot taken above instead of pushing another one.
|
||||
bake(/* own_snapshot */ false);
|
||||
@@ -3526,6 +3932,12 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
rebuild_uvcheck_mesh();
|
||||
if (m_use_bump_preview)
|
||||
rebuild_bump_preview_mesh();
|
||||
else
|
||||
// The Fast view skips the CPU displacement entirely (see rebuild_preview()), so
|
||||
// leaving it means m_preview_glmodel may be stale or absent - ask for it now.
|
||||
queue_preview_job();
|
||||
// ...and the paint tint between the base and the displaced surface, for the same reason.
|
||||
m_paint_overlay_dirty = true;
|
||||
refresh_wireframe(); // Normal<->Fast swaps the wireframe between displaced and base mesh
|
||||
update_uv_editor(); // mirror the checker / distortion heatmap into the UV pane too (#7)
|
||||
m_parent.set_as_dirty();
|
||||
@@ -4225,6 +4637,20 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
m_imgui->scaled(20.f));
|
||||
}
|
||||
|
||||
// Applies in both adaptive sub-modes: it is not a texture-detail criterion, it is about the
|
||||
// step the bake puts at the paint's edge, which exists whether or not "Follow texture detail"
|
||||
// is on. 0 turns the band off and restores the old behaviour.
|
||||
if (m_imgui->slider_float(std::string(_u8L("Edge detail (mm)")) + "##subdivborder", &m_subdivide_border_mm,
|
||||
0.f, 5.f, "%.3f"))
|
||||
preview_live();
|
||||
if (ImGui::IsItemHovered())
|
||||
m_imgui->tooltip(_u8L("Triangle size along the boundary of the painted area. The relief drops back to the "
|
||||
"flat surface across that boundary, and the triangles spanning the drop are what you "
|
||||
"see as a jagged rim around an unpainted region - smaller values make the outline "
|
||||
"cleaner. Costs triangles along the outline only, not over the whole area. "
|
||||
"0 turns it off."),
|
||||
m_imgui->scaled(20.f));
|
||||
|
||||
ImGui::PopItemWidth();
|
||||
budget_slider();
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include "slic3r/GUI/TextureLibrary.hpp"
|
||||
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
@@ -101,11 +102,26 @@ private:
|
||||
// so a run that turns out to be a no-op does not leave an empty undo step behind - and it keeps
|
||||
// snapshot ownership with the caller, which matters because the buttons want one snapshot per click
|
||||
// while the pipeline wants a single one around remesh + subdivide together.
|
||||
// How one layer's paint sits on the pre-subdivision mesh, precise enough to carry across the
|
||||
// refinement without rounding each source triangle to wholly painted or not.
|
||||
//
|
||||
// Rounding is what made the outline of a painted region come out ragged: a source triangle near a
|
||||
// smooth brush boundary is wholly painted essentially at random, so "painted iff the source was
|
||||
// full" turns a clean curve into a noisy fringe of isolated painted and unpainted triangles - and
|
||||
// once the border band refines the mesh there, that fringe is reproduced faithfully instead of
|
||||
// being blurred away by coarse geometry.
|
||||
struct LayerPaintMap
|
||||
{
|
||||
std::vector<uint8_t> full; // per source triangle: covered edge to edge
|
||||
std::vector<int> part_start; // CSR offsets into `part`, size (source tris + 1)
|
||||
std::vector<std::array<Vec3f, 3>> part; // painted pieces of partly covered source triangles
|
||||
bool empty() const { return full.empty(); }
|
||||
};
|
||||
struct SubdivisionPlan
|
||||
{
|
||||
indexed_triangle_set refined;
|
||||
std::vector<int> source; // new tri -> input tri
|
||||
std::array<std::vector<uint8_t>, TEXTURE_DISPLACEMENT_MAX_LAYERS> painted_tri; // per layer, per input tri
|
||||
indexed_triangle_set refined;
|
||||
std::vector<int> source; // new tri -> input tri
|
||||
std::array<LayerPaintMap, TEXTURE_DISPLACEMENT_MAX_LAYERS> paint; // per layer
|
||||
};
|
||||
// False means "nothing to refine" and `out` must not be used.
|
||||
bool plan_adaptive_subdivision(const ModelVolume &mv, SubdivisionPlan &out) const;
|
||||
@@ -162,10 +178,20 @@ private:
|
||||
// remapped (texture-displacement paint has no remap-across-topology-change support yet).
|
||||
void subdivide_model();
|
||||
|
||||
// The layer height map's width / height, for apply_uv_transform()'s non-square handling. 1 when
|
||||
// there is no usable texture.
|
||||
static float layer_texture_aspect(const TextureDisplacementLayer &layer);
|
||||
|
||||
// Returns a cached GPU thumbnail of layer's texture (decoding + uploading it the first time it
|
||||
// is requested, or whenever its image_data changes), or nullptr if it has no usable texture.
|
||||
// Panel-sized: box-filtered down to THUMBNAIL_MAX_PX, which is right for a list row and wrong for
|
||||
// anything the shader samples - see get_layer_height_texture().
|
||||
GLTexture *get_layer_thumbnail(const TextureDisplacementLayer &layer);
|
||||
|
||||
// The same texture at full resolution, for the fast-preview shader. One slot, shared by whichever
|
||||
// layer is active, because that is the only one the bump shader ever shades.
|
||||
GLTexture *get_layer_height_texture(const TextureDisplacementLayer &layer);
|
||||
|
||||
// A texture from the picker's library (see slic3r/GUI/TextureLibrary.hpp), read and uploaded
|
||||
// once and then kept for the gizmo's lifetime. The decoded bytes are held alongside the GPU
|
||||
// thumbnail so that picking the texture can hand the layer this very same image_data buffer -
|
||||
@@ -366,18 +392,33 @@ private:
|
||||
bool m_subdivide_feature = false;
|
||||
float m_subdivide_detail_mm = 0.05f;
|
||||
float m_subdivide_min_edge_mm = 0.1f;
|
||||
// Edge length the band straddling the paint's boundary is refined to (0 = leave it alone). Applies
|
||||
// in both adaptive sub-modes, because it is not a texture-detail criterion: the bake steps the
|
||||
// surface from full displacement to zero across that boundary whatever the texture is doing, and
|
||||
// the chord-error test cannot see that step at all - its sampler has no per-point paint test, so
|
||||
// just outside the paint it goes on reporting the same smooth height field. Without this the
|
||||
// transition keeps the input's density and the rim of an unpainted island comes out as a ring of
|
||||
// large, steeply tilted triangles. See collect_paint_region() and subdivide_mesh_adaptive().
|
||||
float m_subdivide_border_mm = 0.4f;
|
||||
// How many thousand triangles refinement may *add* (the mesh's own count is added on before it is
|
||||
// passed as subdivide_mesh_adaptive()'s absolute cap, so the control still means something on a
|
||||
// dense model). Refinement is worst-error-first, so hitting the budget still yields the best mesh
|
||||
// that many triangles can buy - and it is what keeps a fine "Detail" over a noisy texture from
|
||||
// turning into an out-of-memory, or an unrenderable preview wireframe.
|
||||
int m_subdivide_budget_k = 1500;
|
||||
//
|
||||
// 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
|
||||
// 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
|
||||
// 0.02 mm detail tolerance Standard uses, and the slider goes to 2000 for anyone who wants more.
|
||||
int m_subdivide_budget_k = 300;
|
||||
void subdivide_model_adaptive();
|
||||
// Fills `region` (per current-mesh triangle, 1 = refine) from the union of every layer's painted
|
||||
// area. If `painted_tri` is non-null, also fills, per layer, the fully-painted triangles to carry
|
||||
// forward. Returns false when nothing is painted at all. Shared by the preview and the commit.
|
||||
// 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
|
||||
// coverage map the subdivision carries forward - the expensive half, skipped by the live preview,
|
||||
// which only needs the region. Returns false when nothing is painted at all.
|
||||
bool collect_paint_region(std::vector<uint8_t> ®ion,
|
||||
std::array<std::vector<uint8_t>, TEXTURE_DISPLACEMENT_MAX_LAYERS> *painted_tri) const;
|
||||
std::array<LayerPaintMap, TEXTURE_DISPLACEMENT_MAX_LAYERS> *paint) const;
|
||||
|
||||
// Runs the volume's TextureDisplacementOptions smoothing over the *already committed* geometry,
|
||||
// restricted to the painted area. The same settings are folded into Preview/Bake automatically;
|
||||
@@ -417,6 +458,25 @@ private:
|
||||
// and the 3D view. The rebuild is instead coalesced to once per 3D frame (render_painter_gizmo).
|
||||
bool m_bump_preview_dirty = false;
|
||||
GLModel m_bump_preview_glmodel;
|
||||
|
||||
// Translucent tint over the active layer's painted triangles, drawn on top of whichever preview
|
||||
// is showing. The base painter's own opaque paint highlight (render_triangles()) cannot be used
|
||||
// in either preview mode - it is coincident with the surface and simply covers it - so the only
|
||||
// paint feedback the gizmo had was the relief itself, which meant erasing showed nothing at all
|
||||
// until the stroke ended and the whole preview rebuilt. This is that feedback: cheap (the painted
|
||||
// patch only), translucent (the preview stays visible through it) and rebuilt live during a
|
||||
// stroke.
|
||||
GLModel m_paint_overlay_glmodel;
|
||||
// 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,
|
||||
// not the bump mesh (which also carries every *unpainted* triangle of the volume).
|
||||
bool m_paint_overlay_dirty = false;
|
||||
void rebuild_paint_overlay();
|
||||
void render_paint_overlay();
|
||||
// Whether render_bump_preview_mesh() would actually draw something. Checked before the real volume
|
||||
// is hidden: with no layer, no texture or no shader the bump path draws nothing, and hiding the
|
||||
// volume for it left the model invisible.
|
||||
bool bump_preview_ready() const;
|
||||
// Whether the current bump mesh carries a precomputed per-vertex uv (LSCM) that the shader
|
||||
// should sample at directly, rather than projecting in-shader. Set by rebuild_bump_preview_mesh().
|
||||
bool m_bump_preview_uses_vertex_uv = false;
|
||||
@@ -507,16 +567,36 @@ private:
|
||||
// Bumped on every rebuild_preview() call; a background TextureDisplacementPreviewJob's result
|
||||
// is only applied if this hasn't moved on since the job was queued (see rebuild_preview()),
|
||||
// so a burst of edits can't have an earlier, now-stale job clobber a later one's result.
|
||||
uint64_t m_preview_generation = 0;
|
||||
//
|
||||
// Shared with the worker thread (hence the atomic) so a running job can notice mid-computation
|
||||
// that it has been superseded and abort, instead of running to completion for a result that will
|
||||
// only be discarded on arrival.
|
||||
std::shared_ptr<std::atomic<uint64_t>> m_preview_generation = std::make_shared<std::atomic<uint64_t>>(0);
|
||||
// At most one preview job is ever queued. The UI job worker is a single FIFO queue shared with
|
||||
// Bake (and with arrange/orient/send), and rebuild_preview() is called on every stroke end, every
|
||||
// slider release and - with "Auto update" on - every frame of a slider drag. Queuing one full
|
||||
// displacement per call built a backlog that took minutes to drain: the preview appeared frozen,
|
||||
// and a Bake pressed afterwards sat behind the whole queue. So a request made while a job is in
|
||||
// flight is recorded here and issued once that job settles, collapsing any number of edits into a
|
||||
// single follow-up run.
|
||||
bool m_preview_job_running = false;
|
||||
bool m_preview_job_pending = false;
|
||||
void queue_preview_job();
|
||||
|
||||
// Per-slot GPU thumbnail cache for the layer list panel, keyed by the image_data pointer that
|
||||
// was current the last time each thumbnail was built (see get_layer_thumbnail()).
|
||||
std::array<std::unique_ptr<GLTexture>, TEXTURE_DISPLACEMENT_MAX_LAYERS> m_thumbnails;
|
||||
std::array<const void *, TEXTURE_DISPLACEMENT_MAX_LAYERS> m_thumbnail_source{};
|
||||
// The smoothing each cached thumbnail was built at, so a smoothing change re-uploads it (and the
|
||||
// fast/bump preview, which samples this texture, actually shows the blur).
|
||||
// The smoothing each cached thumbnail was built at, so a smoothing change re-uploads it.
|
||||
std::array<float, TEXTURE_DISPLACEMENT_MAX_LAYERS> m_thumbnail_smoothing{};
|
||||
|
||||
// Full-resolution height texture for the bump shader, keyed the same way (see
|
||||
// get_layer_height_texture()). A smoothing change re-uploads it, so the fast preview shows the
|
||||
// blur the bake will apply.
|
||||
std::unique_ptr<GLTexture> m_height_tex;
|
||||
const void *m_height_tex_source = nullptr;
|
||||
float m_height_tex_smoothing = -1.f;
|
||||
|
||||
// Library textures the picker has shown at least once, keyed by file path (see LibraryTexture).
|
||||
std::map<std::string, LibraryTexture> m_library_textures;
|
||||
|
||||
|
||||
@@ -166,7 +166,11 @@ void GLGizmosManager::switch_gizmos_icon_filename()
|
||||
gizmo->set_icon_filename(m_is_dark ? "toolbar_fuzzy_skin_paint_dark.svg" : "toolbar_fuzzy_skin_paint.svg");
|
||||
break;
|
||||
case(EType::TextureDisplacement):
|
||||
gizmo->set_icon_filename(m_is_dark ? "toolbar_fuzzy_skin_paint_dark.svg" : "toolbar_fuzzy_skin_paint.svg");
|
||||
// One shared icon in both themes (no dedicated dark variant yet) - but it must still be
|
||||
// *this* gizmo's icon. Handing it the fuzzy-skin one here quietly replaced the icon set at
|
||||
// construction, so the toolbar ended up showing two identical fuzzy-skin buttons after any
|
||||
// light/dark switch.
|
||||
gizmo->set_icon_filename("toolbar_texture_displacement.svg");
|
||||
break;
|
||||
case(EType::MeshBoolean):
|
||||
gizmo->set_icon_filename(m_is_dark ? "toolbar_meshboolean_dark.svg" : "toolbar_meshboolean.svg");
|
||||
|
||||
@@ -20,12 +20,34 @@ TextureDisplacementBakeJob::TextureDisplacementBakeJob(TextureDisplacementBakeIn
|
||||
|
||||
void TextureDisplacementBakeJob::process(Ctl &ctl)
|
||||
{
|
||||
ctl.update_status(0, _u8L("Baking texture displacement"));
|
||||
const std::string status = _u8L("Baking texture displacement");
|
||||
ctl.update_status(1, status);
|
||||
|
||||
// Only ever touches m_input (captured by value before this job was queued) and local state -
|
||||
// never the live Model - so this is safe to run concurrently with the UI thread.
|
||||
m_result = TriangleMesh(build_texture_displacement(m_input.base_mesh, m_input.layers, m_input.facets_data,
|
||||
m_input.options));
|
||||
//
|
||||
// The progress hook matters for more than cosmetics: the framework's progress notification only
|
||||
// grows a close button once it reaches 100%, so a job that reports 0 and nothing else leaves an
|
||||
// uncloseable notification pinned on screen. It also carries the Cancel button's effect into the
|
||||
// bake, which on a subdivided mesh can run for several seconds.
|
||||
int last_reported = 1;
|
||||
m_result = TriangleMesh(build_texture_displacement(
|
||||
m_input.base_mesh, m_input.layers, m_input.facets_data, m_input.options,
|
||||
[&ctl, &status, &last_reported](int percent) {
|
||||
if (ctl.was_canceled())
|
||||
return false;
|
||||
// The notification repaints (and wakes the idle loop) on every call, so only push a
|
||||
// message when the displayed integer percentage actually moves.
|
||||
if (percent > last_reported) {
|
||||
last_reported = percent;
|
||||
ctl.update_status(percent, status);
|
||||
}
|
||||
return true;
|
||||
}));
|
||||
|
||||
// Always finish at 100: this is what closes the notification. Reported even on cancel, where
|
||||
// build_texture_displacement() returns an empty mesh and finalize() commits nothing.
|
||||
ctl.update_status(100, status);
|
||||
}
|
||||
|
||||
void TextureDisplacementBakeJob::finalize(bool canceled, std::exception_ptr &eptr)
|
||||
|
||||
@@ -5,25 +5,44 @@
|
||||
namespace Slic3r::GUI {
|
||||
|
||||
TextureDisplacementPreviewJob::TextureDisplacementPreviewJob(TextureDisplacementPreviewInput &&input, uint64_t generation,
|
||||
std::shared_ptr<const std::atomic<uint64_t>> current_generation,
|
||||
std::function<void(indexed_triangle_set, uint64_t)> on_finished)
|
||||
: m_input(std::move(input)), m_generation(generation), m_on_finished(std::move(on_finished))
|
||||
: m_input(std::move(input)), m_generation(generation), m_current_generation(std::move(current_generation)),
|
||||
m_on_finished(std::move(on_finished))
|
||||
{
|
||||
}
|
||||
|
||||
void TextureDisplacementPreviewJob::process(Ctl &ctl)
|
||||
{
|
||||
ctl.update_status(0, _u8L("Computing texture displacement preview"));
|
||||
// No ctl.update_status() anywhere in here on purpose - see the class comment. A preview is
|
||||
// invisible bookkeeping; the only thing on screen should be the preview itself.
|
||||
|
||||
// Only ever touches m_input (captured by value before this job was queued) and local state -
|
||||
// never the live Model - so this is safe to run concurrently with the UI thread.
|
||||
m_result = build_texture_displacement(m_input.base_mesh, m_input.layers, m_input.facets_data, m_input.options);
|
||||
m_result = build_texture_displacement(m_input.base_mesh, m_input.layers, m_input.facets_data, m_input.options,
|
||||
[this, &ctl](int) {
|
||||
// Bail the moment this preview stops being the current
|
||||
// one; build_texture_displacement() then returns an
|
||||
// empty mesh and finalize() drops it.
|
||||
return !ctl.was_canceled() &&
|
||||
(!m_current_generation ||
|
||||
m_current_generation->load() == m_generation);
|
||||
});
|
||||
}
|
||||
|
||||
void TextureDisplacementPreviewJob::finalize(bool canceled, std::exception_ptr &eptr)
|
||||
{
|
||||
if (canceled || eptr || !m_on_finished)
|
||||
if (!m_on_finished)
|
||||
return;
|
||||
m_on_finished(std::move(m_result), m_generation);
|
||||
// The handler must run on *every* outcome, cancellation included, because the caller uses it to
|
||||
// clear its "a job is in flight" latch. Returning early on `canceled` - which is what a cancel_all()
|
||||
// from Plater (project load/close, app exit) delivers - left that latch stuck true and no preview
|
||||
// was ever queued again for the rest of the session. An empty result is the caller's signal that
|
||||
// nothing usable came back; it already handles that.
|
||||
if (canceled || eptr)
|
||||
m_on_finished(indexed_triangle_set{}, m_generation);
|
||||
else
|
||||
m_on_finished(std::move(m_result), m_generation);
|
||||
}
|
||||
|
||||
} // namespace Slic3r::GUI
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
#ifndef slic3r_TextureDisplacementPreviewJob_hpp_
|
||||
#define slic3r_TextureDisplacementPreviewJob_hpp_
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include "libslic3r/TextureDisplacement.hpp"
|
||||
@@ -27,6 +29,10 @@ struct TextureDisplacementPreviewInput
|
||||
// to run synchronously on every paint stroke and parameter tweak and made editing feel slow with
|
||||
// more than one or two layers. Unlike Bake, this never touches the live Model - a preview is
|
||||
// purely informational, there is nothing to commit.
|
||||
//
|
||||
// Deliberately reports no status: the Job framework turns the first update_status() call into an
|
||||
// on-screen progress notification, and a preview firing one on every stroke and slider release
|
||||
// buried the user in notifications that only closed at 100%.
|
||||
class TextureDisplacementPreviewJob : public Job
|
||||
{
|
||||
public:
|
||||
@@ -35,7 +41,14 @@ public:
|
||||
// current generation when the job completes, so that a burst of edits queuing several of
|
||||
// these jobs in a row can't have an earlier, now-stale result clobber a later one that
|
||||
// finishes first.
|
||||
//
|
||||
// `current_generation` is the caller's live counter, shared with the worker thread. The job
|
||||
// polls it *while computing* and aborts as soon as it no longer matches - so a preview that has
|
||||
// already been superseded stops burning CPU instead of running to completion for a result that
|
||||
// will only be thrown away. That matters because the UI job worker runs one job at a time in FIFO
|
||||
// order: without it, a Bake queued behind a handful of stale previews waits for every one of them.
|
||||
TextureDisplacementPreviewJob(TextureDisplacementPreviewInput &&input, uint64_t generation,
|
||||
std::shared_ptr<const std::atomic<uint64_t>> current_generation,
|
||||
std::function<void(indexed_triangle_set, uint64_t)> on_finished);
|
||||
|
||||
void process(Ctl &ctl) override;
|
||||
@@ -44,6 +57,7 @@ public:
|
||||
private:
|
||||
TextureDisplacementPreviewInput m_input;
|
||||
uint64_t m_generation;
|
||||
std::shared_ptr<const std::atomic<uint64_t>> m_current_generation;
|
||||
indexed_triangle_set m_result;
|
||||
std::function<void(indexed_triangle_set, uint64_t)> m_on_finished;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user