mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-12 18:31:18 +00:00
Carry the paint across with transfer_facet_paint
The branch used TriangleSelector::remap_painting(), which pairs a source paint triangle with a target triangle overlapping it to within TriangleCursor::tolerance. That is right after a remesh, which leaves the surface where it was, and wrong after a bake, which has moved it by the relief depth - most of the paint finds no partner. transfer_facet_paint() asks the other question: every triangle of the new mesh takes the state painted at its foot point on the old one, which is exactly where it came from however deep the relief. It also does all four paint channels rather than colour alone, and it runs where the volume is read, so the snapshot and the timestamp that guarded against the user painting while the bake ran are no longer needed: the annotation is read at commit time and is by definition current. The function and its call site are byte for byte what the model-UV branch carries, so the two merge in any order and in either order produce one mechanism rather than two fighting over mmu_segmentation_facets. The regression test is unchanged except for the call it makes.
This commit is contained in:
@@ -1570,6 +1570,61 @@ std::vector<TextureIsland> compute_connected_net(const PatchUnwrap &unwrap)
|
||||
return islands;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> transfer_facet_paint(const indexed_triangle_set &src_mesh,
|
||||
const TriangleSelector::TriangleSplittingData &src_paint,
|
||||
const indexed_triangle_set &dst_mesh)
|
||||
{
|
||||
if (src_mesh.indices.empty() || dst_mesh.indices.empty() || src_paint.bitstream.empty())
|
||||
return {};
|
||||
|
||||
const TriangleMesh src(src_mesh);
|
||||
TriangleSelector selector(src);
|
||||
selector.deserialize(src_paint, false);
|
||||
|
||||
// Every sub-triangle of the source, each carrying its own state - NONE included, so the soup
|
||||
// covers the whole surface and a foot point always lands on something. One soup rather than one
|
||||
// tree per state: a foot point has one nearest triangle, and that triangle's state is the answer.
|
||||
std::vector<indexed_triangle_set> per_state;
|
||||
selector.get_facets(per_state);
|
||||
indexed_triangle_set soup;
|
||||
std::vector<uint8_t> label;
|
||||
for (size_t st = 0; st < per_state.size(); ++st) {
|
||||
const indexed_triangle_set &part = per_state[st];
|
||||
if (part.indices.empty())
|
||||
continue;
|
||||
const int base = int(soup.vertices.size());
|
||||
soup.vertices.insert(soup.vertices.end(), part.vertices.begin(), part.vertices.end());
|
||||
for (const stl_triangle_vertex_indices &t : part.indices) {
|
||||
soup.indices.emplace_back(t[0] + base, t[1] + base, t[2] + base);
|
||||
label.push_back(uint8_t(st));
|
||||
}
|
||||
}
|
||||
if (soup.indices.empty())
|
||||
return {};
|
||||
|
||||
const AABBTreeIndirect::Tree3f tree =
|
||||
AABBTreeIndirect::build_aabb_tree_over_indexed_triangle_set(soup.vertices, soup.indices);
|
||||
|
||||
std::vector<uint8_t> out(dst_mesh.indices.size(), 0);
|
||||
tbb::parallel_for(tbb::blocked_range<size_t>(0, dst_mesh.indices.size()),
|
||||
[&](const tbb::blocked_range<size_t> &range) {
|
||||
for (size_t j = range.begin(); j < range.end(); ++j) {
|
||||
const stl_triangle_vertex_indices &t = dst_mesh.indices[j];
|
||||
// The centroid, not a corner: a facet takes one state, and its middle is the point that
|
||||
// is inside it whatever the triangle looks like.
|
||||
const Vec3f centroid = (dst_mesh.vertices[size_t(t[0])] + dst_mesh.vertices[size_t(t[1])] +
|
||||
dst_mesh.vertices[size_t(t[2])]) / 3.f;
|
||||
size_t hit = 0;
|
||||
Vec3f foot;
|
||||
const float d2 = AABBTreeIndirect::squared_distance_to_indexed_triangle_set(soup.vertices, soup.indices,
|
||||
tree, centroid, hit, foot);
|
||||
if (d2 >= 0.f && hit < label.size())
|
||||
out[j] = label[hit];
|
||||
}
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
void average_island_scales(std::vector<TextureIsland> &islands)
|
||||
{
|
||||
if (islands.empty())
|
||||
|
||||
@@ -578,6 +578,31 @@ bool sample_layer_color(const DecodedHeightTexture &texture, const TextureDispla
|
||||
const Vec3f &patch_center = Vec3f::Zero(), const Vec3f &patch_axis = Vec3f::UnitZ(),
|
||||
const Vec2f *lscm_uv = nullptr);
|
||||
|
||||
// Carries a per-facet paint annotation - the colour paint above all - from a mesh onto the mesh a
|
||||
// bake produced from it: every triangle of `dst_mesh` takes the state painted at its own foot point on
|
||||
// `src_mesh`, i.e. on the nearest point of the surface it was built from. Returned as one
|
||||
// EnforcerBlockerType per `dst_mesh` triangle, 0 (NONE) where that foot point carried no paint.
|
||||
//
|
||||
// ModelVolume::restore_painting() cannot do this. Its remap pairs a source paint triangle with a
|
||||
// target triangle that *overlaps* it to within 0.01 mm, which is right for a remesh or a
|
||||
// simplification - they leave the surface where it was - and useless after a bake, which moves the
|
||||
// surface by the relief depth. Measured on a model displaced by a tenth of its own size, that remap
|
||||
// kept a quarter of the colour and left the rest unpainted; this keeps essentially all of it, because
|
||||
// a foot point is exactly as far away as the relief is deep and the lookup does not care how far that
|
||||
// is.
|
||||
//
|
||||
// The source's *sub*-triangles are what is looked up, not its triangles, so the part of a triangle a
|
||||
// brush stroke only clipped a corner off keeps its own state rather than rounding to the whole
|
||||
// triangle's. No distance limit is needed or wanted: the nearest point on the source surface is where
|
||||
// a baked triangle came from however far the relief carried it, and an unpainted origin reports NONE
|
||||
// on its own.
|
||||
//
|
||||
// Returns an empty vector when `src_paint` is empty or either mesh is - the caller then has nothing to
|
||||
// carry, which is not the same as "carry nothing".
|
||||
std::vector<uint8_t> transfer_facet_paint(const indexed_triangle_set &src_mesh,
|
||||
const TriangleSelector::TriangleSplittingData &src_paint,
|
||||
const indexed_triangle_set &dst_mesh);
|
||||
|
||||
// Area-weighted centroid and average normal of a layer's currently painted patch, in mesh-local
|
||||
// coordinates - the same measurements build_texture_displacement() uses to pick its dominant
|
||||
// projection axis. Used by the GUI to anchor the on-canvas "adjust texture placement" gizmo to
|
||||
@@ -771,12 +796,9 @@ struct TextureColorRequest
|
||||
// single facets; an image detail a few facets wide still leaves thousands of pinhead islands
|
||||
// that the slicer's multi-material segmentation cannot digest. 0 turns it off.
|
||||
float min_color_region_mm2 = 0.5f;
|
||||
// Filled per triangle of the *returned* mesh: the quantize callback's index plus one, or 0 for
|
||||
// "this triangle takes no colour from the texture". That is the same indexing as the base mesh
|
||||
// only on the classic path, which moves vertices and leaves the topology alone; the one-run
|
||||
// pipeline rebuilds the mesh, so nothing recorded against the base mesh - an existing paint mask,
|
||||
// say - may be carried over by triangle id (see TriangleSelector::remap_painting()).
|
||||
// The +1 lines up with EnforcerBlockerType, where 0 is NONE ("use the volume's own
|
||||
// Filled per *base mesh* triangle (the bake is topology-preserving, so this indexes the returned
|
||||
// mesh too): the quantize callback's index plus one, or 0 for "this triangle takes no colour from
|
||||
// the texture". The +1 lines up with EnforcerBlockerType, where 0 is NONE ("use the volume's own
|
||||
// filament"): where every palette entry is a filament, these go straight to a TriangleSelector. A
|
||||
// palette with mixes maps each index to the mix's filament slot first (see
|
||||
// GLGizmoTextureDisplacement::palette_filaments()).
|
||||
|
||||
@@ -8,12 +8,12 @@
|
||||
#include "libslic3r/TextureDisplacement.hpp"
|
||||
#include <exception>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include "slic3r/GUI/Jobs/Worker.hpp"
|
||||
#include <memory>
|
||||
|
||||
#include "libslic3r/Model.hpp"
|
||||
#include "libslic3r/format.hpp"
|
||||
#include "libslic3r/Point.hpp"
|
||||
#include "libslic3r/TriangleSelector.hpp"
|
||||
|
||||
#include "slic3r/GUI/GLCanvas3D.hpp"
|
||||
@@ -27,6 +27,7 @@
|
||||
#include "slic3r/Utils/UndoRedo.hpp"
|
||||
#include "libslic3r/ObjectID.hpp"
|
||||
#include "libslic3r/TriangleMesh.hpp"
|
||||
#include <array>
|
||||
|
||||
namespace Slic3r::GUI {
|
||||
|
||||
@@ -77,23 +78,6 @@ void TextureDisplacementBakeJob::process(Ctl &ctl)
|
||||
},
|
||||
color, m_input.volume_to_world, nullptr, &m_stats));
|
||||
|
||||
// Bring the colour that was already painted onto the mesh this bake just built, while still on
|
||||
// the worker thread. Triangle ids only mean the same thing on both meshes when the bake preserved
|
||||
// the topology - the classic path moves vertices and nothing else - and the one-run pipeline
|
||||
// rebuilds the mesh from scratch. Deserializing the old mask against the new one only checks that
|
||||
// the ids are in range, and a bake almost always *grows* the triangle count, so every stale id
|
||||
// passes that check and lands on an unrelated face: colour debris where nothing was painted, and
|
||||
// the earlier pass's colour destroyed rather than kept. remap_painting() transfers it by position
|
||||
// instead, which is what lets successive bakes accumulate. It is spatial work over both meshes,
|
||||
// which is why it belongs here and not in finalize().
|
||||
if (!m_input.mmu_segmentation.bitstream.empty() && !m_result.empty() && !ctl.was_canceled()) {
|
||||
if (m_result.its.indices.size() == m_input.base_mesh.indices.size())
|
||||
m_carried_mmu = m_input.mmu_segmentation;
|
||||
else
|
||||
m_carried_mmu = TriangleSelector::remap_painting(m_input.base_mesh, m_input.mmu_segmentation,
|
||||
m_result.its, Transform3d::Identity(), {});
|
||||
}
|
||||
|
||||
// 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);
|
||||
@@ -144,39 +128,76 @@ void TextureDisplacementBakeJob::finalize(bool canceled, std::exception_ptr &ept
|
||||
{ mix.a_percent(), 100 - mix.a_percent() });
|
||||
});
|
||||
|
||||
// Read while the volume still holds the mesh the paint was recorded against, and after the
|
||||
// filament slots above - adding one can clamp the paint, and that clamp has to survive.
|
||||
// Normally process() has already carried the colour onto the baked topology; it only has to
|
||||
// be redone here when the paint changed after the job was queued, because the snapshot
|
||||
// process() worked from is then not what the user is looking at.
|
||||
const bool mmu_changed = volume->mmu_segmentation_facets.timestamp() != m_input.mmu_segmentation_timestamp;
|
||||
const indexed_triangle_set pre_bake_mesh = mmu_changed ? volume->mesh().its
|
||||
: indexed_triangle_set{};
|
||||
const TriangleSelector::TriangleSplittingData live_mmu = mmu_changed
|
||||
? volume->mmu_segmentation_facets.get_data()
|
||||
: TriangleSelector::TriangleSplittingData{};
|
||||
// Whether the bake replaced the triangles or only moved the vertices it was given. The classic
|
||||
// path is topology-preserving by contract, so every per-triangle annotation on the volume still
|
||||
// means what it did; the default pipeline refines, cuts and simplifies, so none of them do.
|
||||
// Both are checked: the flag is what the bake promised, the count is what it delivered. Only
|
||||
// the triangles matter - the annotations are per facet and reference no vertex index - so a
|
||||
// vertex count that changed on its own (the bake compacts unreferenced vertices away) is not a
|
||||
// reason to remap anything.
|
||||
const bool topology_changed = m_input.options.pipeline_v2 ||
|
||||
m_result.its.indices.size() != m_input.base_mesh.indices.size();
|
||||
|
||||
// Carry the volume's existing paint - the model's own colours above all - across that rebuild.
|
||||
// Without it a bake left the mmu_segmentation_facets bitstream pointing at a triangle set that
|
||||
// no longer existed, which is what scrambled the colours of an imported coloured model even
|
||||
// with colour switched off on every layer and nothing in the bake that was meant to touch them.
|
||||
//
|
||||
// transfer_facet_paint() rather than ModelVolume::restore_painting(): the latter's remap wants
|
||||
// a target triangle overlapping the source paint to within 0.01 mm, which the baked surface is
|
||||
// nowhere near - it has moved by the relief depth. Nothing to do when the topology is intact:
|
||||
// triangle i is still triangle i, so the paint is already exactly right and carrying it at all
|
||||
// could only lose detail.
|
||||
std::array<TriangleSelector::TriangleSplittingData, 4> before;
|
||||
if (topology_changed) {
|
||||
before[0] = volume->supported_facets.get_data();
|
||||
before[1] = volume->seam_facets.get_data();
|
||||
before[2] = volume->mmu_segmentation_facets.get_data();
|
||||
before[3] = volume->fuzzy_skin_facets.get_data();
|
||||
}
|
||||
|
||||
volume->set_mesh(std::move(m_result));
|
||||
volume->set_new_unique_id();
|
||||
volume->calculate_convex_hull();
|
||||
|
||||
if (topology_changed) {
|
||||
const std::array<FacetsAnnotation *, 4> target{ &volume->supported_facets, &volume->seam_facets,
|
||||
&volume->mmu_segmentation_facets,
|
||||
&volume->fuzzy_skin_facets };
|
||||
for (size_t ch = 0; ch < target.size(); ++ch) {
|
||||
const std::vector<uint8_t> state =
|
||||
transfer_facet_paint(m_input.base_mesh, before[ch], volume->mesh().its);
|
||||
// Nothing was painted in this channel, or there is nothing to paint it onto: either
|
||||
// way the annotation must not keep describing the mesh that is gone.
|
||||
if (state.empty()) {
|
||||
target[ch]->reset();
|
||||
continue;
|
||||
}
|
||||
TriangleSelector selector(volume->mesh());
|
||||
bool any = false;
|
||||
for (size_t i = 0; i < state.size(); ++i)
|
||||
if (state[i] != 0) {
|
||||
selector.set_facet(int(i), EnforcerBlockerType(state[i]));
|
||||
any = true;
|
||||
}
|
||||
if (any)
|
||||
target[ch]->set(selector);
|
||||
else
|
||||
target[ch]->reset();
|
||||
}
|
||||
}
|
||||
|
||||
// Colour lands in mmu_segmentation_facets, merged *over* whatever is already painted there
|
||||
// rather than replacing it: a triangle the texture does not colour keeps its existing filament,
|
||||
// and one the user never painted at all stays at NONE, which already means "the volume's own
|
||||
// filament". That is what confines the effect to the painted area without having to invent a
|
||||
// colour for everything outside it. What was already painted comes from m_carried_mmu, which
|
||||
// process() brought onto this mesh's topology; this bake's own colour goes on top of it, so it
|
||||
// wins wherever it painted and leaves the rest alone.
|
||||
// colour for everything outside it. What was already painted is whatever the transfer above
|
||||
// left in the annotation, so this bake's colour lands on top of it rather than instead of it.
|
||||
if (!m_triangle_color.empty() && m_triangle_color.size() == volume->mesh().its.indices.size()) {
|
||||
if (mmu_changed && !live_mmu.bitstream.empty())
|
||||
m_carried_mmu = pre_bake_mesh.indices.size() == volume->mesh().its.indices.size()
|
||||
? live_mmu
|
||||
: TriangleSelector::remap_painting(pre_bake_mesh, live_mmu,
|
||||
volume->mesh().its,
|
||||
Transform3d::Identity(), {});
|
||||
TriangleSelector selector(volume->mesh());
|
||||
if (!m_carried_mmu.bitstream.empty())
|
||||
selector.deserialize(m_carried_mmu, false);
|
||||
const TriangleSelector::TriangleSplittingData &existing = volume->mmu_segmentation_facets.get_data();
|
||||
if (!existing.bitstream.empty())
|
||||
selector.deserialize(existing, false);
|
||||
for (size_t i = 0; i < m_triangle_color.size(); ++i) {
|
||||
const size_t entry = size_t(m_triangle_color[i]);
|
||||
if (entry > 0 && entry <= filament_of.size() && filament_of[entry - 1] >= 0)
|
||||
@@ -195,7 +216,6 @@ void TextureDisplacementBakeJob::finalize(bool canceled, std::exception_ptr &ept
|
||||
// pipeline, which rebuilds and then simplifies the mesh. A mask left behind against the old
|
||||
// topology is exactly what makes the gizmo reload an empty selector over a non-empty mask and
|
||||
// then erase it on the next flush - see GLGizmoTextureDisplacement::update_from_model_object().
|
||||
const bool topology_changed = m_input.base_mesh.indices.size() != volume->mesh().its.indices.size();
|
||||
for (int slot = 0; slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++slot) {
|
||||
const auto it = std::find_if(m_input.layers.begin(), m_input.layers.end(),
|
||||
[slot](const TextureDisplacementLayer &l) { return l.slot == slot; });
|
||||
@@ -258,8 +278,6 @@ void queue_texture_displacement_bake(const ModelVolume &volume, const TextureCol
|
||||
input.layers = volume.texture_displacement_layers;
|
||||
input.options = volume.texture_displacement_options;
|
||||
input.volume_to_world = texture_displacement_volume_to_world(volume);
|
||||
input.mmu_segmentation = volume.mmu_segmentation_facets.get_data();
|
||||
input.mmu_segmentation_timestamp = volume.mmu_segmentation_facets.timestamp();
|
||||
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i)
|
||||
input.facets_data[size_t(i)] = volume.texture_displacement_facet(i).get_data();
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
#include "libslic3r/ObjectID.hpp"
|
||||
#include "libslic3r/TextureDisplacement.hpp"
|
||||
#include "libslic3r/TriangleMesh.hpp"
|
||||
#include "libslic3r/TriangleSelector.hpp"
|
||||
|
||||
#include "Job.hpp"
|
||||
|
||||
@@ -35,13 +34,6 @@ struct TextureDisplacementBakeInput
|
||||
// writes the volume's mmu_segmentation_facets - the same per-triangle filament assignment the MMU
|
||||
// paint gizmo writes - alongside the displaced geometry.
|
||||
TextureColorSettings color;
|
||||
// The colour already painted on the volume, indexed against `base_mesh`. Carried here rather than
|
||||
// read at commit time because the one-run pipeline rebuilds the mesh, and bringing this across to
|
||||
// the new topology is spatial work (see process()) that has no business on the UI thread.
|
||||
TriangleSelector::TriangleSplittingData mmu_segmentation;
|
||||
// What `mmu_segmentation` was taken from, so the commit can tell whether the volume was painted
|
||||
// while the bake ran and fall back to the live data if it was.
|
||||
ObjectBase::Timestamp mmu_segmentation_timestamp = 0;
|
||||
// Whether this job pushes its own undo step when it commits. False when the caller has already
|
||||
// taken one that is meant to cover the displacement as well - Standard mode's Bake, which remeshes
|
||||
// and subdivides first and has to undo as a single action.
|
||||
@@ -68,9 +60,6 @@ private:
|
||||
// Empty unless a layer asked for colour. See TextureColorRequest; finalize() turns these into
|
||||
// filaments.
|
||||
std::vector<uint8_t> m_triangle_color;
|
||||
// m_input.mmu_segmentation brought onto m_result's topology, so finalize() can lay this bake's
|
||||
// colour over the colour that was already there. Indexed against m_result, not base_mesh.
|
||||
TriangleSelector::TriangleSplittingData m_carried_mmu;
|
||||
std::function<void()> m_on_finished;
|
||||
};
|
||||
|
||||
|
||||
@@ -2105,78 +2105,6 @@ TEST_CASE("Cancelling while flat faces are harvested past the budget stops the d
|
||||
CHECK(canceled > full);
|
||||
}
|
||||
|
||||
TEST_CASE("A second bake beside a first comes out as fine as a single bake", "[TextureDisplacement]")
|
||||
{
|
||||
// A stroke in each of the cube's two top triangles, baked one after the other. The first bake used
|
||||
// to leave the unpainted ground fanned into slivers from a far corner, or re-triangulated by the
|
||||
// flat harvest into long triangles, and the second bake refined those shapes rather than the
|
||||
// cube's own grid: its stroke came out many times as dense, with walls off the texture's lines.
|
||||
const indexed_triangle_set cube = its_make_cube(20.f, 20.f, 20.f);
|
||||
const Vec2f first_stroke(5.f, 15.f), second_stroke(15.f, 5.f);
|
||||
constexpr float radius = 3.f;
|
||||
|
||||
TextureDisplacementLayer layer;
|
||||
layer.slot = 0;
|
||||
layer.image_data = make_checkerboard_png(16, 16);
|
||||
layer.tiling_scale = 5.f;
|
||||
layer.depth_mm = 0.5f;
|
||||
TextureDisplacementOptions options;
|
||||
options.pipeline_v2 = true;
|
||||
options.v2_refine_mm = 0.5f;
|
||||
options.v2_max_triangles_k = 750; // export: the flat harvest runs
|
||||
const auto bake = [&](const indexed_triangle_set &mesh, const Vec2f &stroke) {
|
||||
TextureDisplacementFacetsData facets;
|
||||
facets[0] = paint_top_disc(mesh, 20.f, stroke, radius);
|
||||
return build_texture_displacement(mesh, { layer }, facets, options);
|
||||
};
|
||||
// Aspect ratios of the top-face triangles whose centroid's distance from `centre` passes `keep`.
|
||||
const auto top_aspects = [](const indexed_triangle_set &mesh, const Vec2f ¢re, auto keep) {
|
||||
std::vector<float> out;
|
||||
for (const stl_triangle_vertex_indices &t : mesh.indices) {
|
||||
const Vec3f &a = mesh.vertices[size_t(t[0])], &b = mesh.vertices[size_t(t[1])], &c = mesh.vertices[size_t(t[2])];
|
||||
const Vec3f g = (a + b + c) / 3.f;
|
||||
if (g.z() > 19.9f && keep((Vec2f(g.x(), g.y()) - centre).norm()))
|
||||
out.push_back(triangle_aspect(a, b, c));
|
||||
}
|
||||
return out;
|
||||
};
|
||||
const auto in_stroke = [&](float d) { return d < radius; };
|
||||
|
||||
const indexed_triangle_set first = bake(cube, first_stroke);
|
||||
REQUIRE(first.indices.size() > cube.indices.size());
|
||||
// What the first bake leaves around its stroke is ground the second one refines: whole pieces of
|
||||
// the cube's grid, not slivers.
|
||||
const std::vector<float> ground = top_aspects(first, first_stroke, [&](float d) { return d > radius + 1.f; });
|
||||
REQUIRE(!ground.empty());
|
||||
const float worst = *std::max_element(ground.begin(), ground.end());
|
||||
INFO("worst aspect ratio on the ground the first bake left: " << worst);
|
||||
CHECK(worst < 6.f);
|
||||
|
||||
const size_t single = top_aspects(bake(cube, second_stroke), second_stroke, in_stroke).size();
|
||||
const size_t second = top_aspects(bake(first, second_stroke), second_stroke, in_stroke).size();
|
||||
INFO("second stroke baked alone: " << single << " triangles, after the first: " << second);
|
||||
REQUIRE(single > 0);
|
||||
CHECK(second <= single * 5 / 4);
|
||||
}
|
||||
|
||||
TEST_CASE("whether a layer's texture has colour agrees with its decode", "[TextureDisplacement]")
|
||||
{
|
||||
TextureDisplacementLayer gray;
|
||||
gray.image_data = make_flat_gray_png(128);
|
||||
CHECK_FALSE(height_texture_has_color(gray));
|
||||
|
||||
TextureDisplacementLayer color;
|
||||
color.image_data = make_flat_rgb_png(200, 40, 10);
|
||||
// Before the image is decoded and after, and whatever the smoothing.
|
||||
CHECK(height_texture_has_color(color));
|
||||
CHECK(decode_height_texture(color).has_color());
|
||||
CHECK(height_texture_has_color(color));
|
||||
color.smoothing = 0.5f;
|
||||
CHECK(height_texture_has_color(color));
|
||||
|
||||
CHECK_FALSE(height_texture_has_color(TextureDisplacementLayer{}));
|
||||
}
|
||||
|
||||
TEST_CASE("Baking twice keeps the mesh closed and carries the first bake's colours across",
|
||||
"[TextureDisplacement][Regression]")
|
||||
{
|
||||
@@ -2185,8 +2113,9 @@ TEST_CASE("Baking twice keeps the mesh closed and carries the first bake's colou
|
||||
// thing. Taken across by triangle id it lands on unrelated faces - a bake almost always grows the
|
||||
// triangle count, so every stale id passes the only bound check deserialize() makes - which both
|
||||
// scatters colour over surfaces nothing painted and destroys what the first pass laid down.
|
||||
// TriangleSelector::remap_painting() carries it by position instead, which is what makes successive
|
||||
// bakes accumulate. See TextureDisplacementBakeJob, which is where the two meet.
|
||||
// transfer_facet_paint() carries it across by position instead - every triangle of the new mesh
|
||||
// takes the state painted at its foot point on the old one - which is what makes successive bakes
|
||||
// accumulate. See TextureDisplacementBakeJob, which is where the two meet.
|
||||
constexpr float side = 20.f;
|
||||
constexpr float refine = 0.5f;
|
||||
constexpr float radius = 4.f;
|
||||
@@ -2264,10 +2193,11 @@ TEST_CASE("Baking twice keeps the mesh closed and carries the first bake's colou
|
||||
{
|
||||
const TriangleMesh mesh(second);
|
||||
TriangleSelector selector(mesh);
|
||||
const TriangleSelector::TriangleSplittingData carried = TriangleSelector::remap_painting(
|
||||
first, painted_first, second, Transform3d::Identity(), {});
|
||||
REQUIRE_FALSE(carried.bitstream.empty());
|
||||
selector.deserialize(carried, false);
|
||||
const std::vector<uint8_t> carried = transfer_facet_paint(first, painted_first, second);
|
||||
REQUIRE(carried.size() == second.indices.size());
|
||||
for (size_t i = 0; i < carried.size(); ++i)
|
||||
if (carried[i] != 0)
|
||||
selector.set_facet(int(i), EnforcerBlockerType(carried[i]));
|
||||
for (size_t i = 0; i < color2.size(); ++i)
|
||||
if (color2[i] != 0)
|
||||
selector.set_facet(int(i), EnforcerBlockerType(color2[i]));
|
||||
@@ -2292,9 +2222,10 @@ TEST_CASE("Baking twice keeps the mesh closed and carries the first bake's colou
|
||||
coloured_first.vertices, coloured_first.indices);
|
||||
|
||||
// A triangle may carry colour the second bake did not produce only where the first bake's colour
|
||||
// actually was. Carrying the mask by position can widen that region by a triangle, so the margin
|
||||
// is the edge length the bake was told to refine to; landing anywhere else means the mask was read
|
||||
// against a mesh it no longer matches.
|
||||
// actually was. A foot point sits on the surface the bake started from, which the relief has since
|
||||
// moved, and the transfer can widen the region by a triangle, so the margin is twice the edge
|
||||
// length the bake was told to refine to; landing anywhere else means the mask was read against a
|
||||
// mesh it no longer matches.
|
||||
size_t own = 0, carried_over = 0, stranded = 0;
|
||||
float worst = 0.f;
|
||||
for (size_t i = 0; i < second.indices.size(); ++i) {
|
||||
@@ -2324,3 +2255,75 @@ TEST_CASE("Baking twice keeps the mesh closed and carries the first bake's colou
|
||||
CHECK(carried_over > 0); // the first bake's colour survived the second
|
||||
CHECK(stranded == 0); // and nothing was painted where neither bake put colour
|
||||
}
|
||||
|
||||
TEST_CASE("A second bake beside a first comes out as fine as a single bake", "[TextureDisplacement]")
|
||||
{
|
||||
// A stroke in each of the cube's two top triangles, baked one after the other. The first bake used
|
||||
// to leave the unpainted ground fanned into slivers from a far corner, or re-triangulated by the
|
||||
// flat harvest into long triangles, and the second bake refined those shapes rather than the
|
||||
// cube's own grid: its stroke came out many times as dense, with walls off the texture's lines.
|
||||
const indexed_triangle_set cube = its_make_cube(20.f, 20.f, 20.f);
|
||||
const Vec2f first_stroke(5.f, 15.f), second_stroke(15.f, 5.f);
|
||||
constexpr float radius = 3.f;
|
||||
|
||||
TextureDisplacementLayer layer;
|
||||
layer.slot = 0;
|
||||
layer.image_data = make_checkerboard_png(16, 16);
|
||||
layer.tiling_scale = 5.f;
|
||||
layer.depth_mm = 0.5f;
|
||||
TextureDisplacementOptions options;
|
||||
options.pipeline_v2 = true;
|
||||
options.v2_refine_mm = 0.5f;
|
||||
options.v2_max_triangles_k = 750; // export: the flat harvest runs
|
||||
const auto bake = [&](const indexed_triangle_set &mesh, const Vec2f &stroke) {
|
||||
TextureDisplacementFacetsData facets;
|
||||
facets[0] = paint_top_disc(mesh, 20.f, stroke, radius);
|
||||
return build_texture_displacement(mesh, { layer }, facets, options);
|
||||
};
|
||||
// Aspect ratios of the top-face triangles whose centroid's distance from `centre` passes `keep`.
|
||||
const auto top_aspects = [](const indexed_triangle_set &mesh, const Vec2f ¢re, auto keep) {
|
||||
std::vector<float> out;
|
||||
for (const stl_triangle_vertex_indices &t : mesh.indices) {
|
||||
const Vec3f &a = mesh.vertices[size_t(t[0])], &b = mesh.vertices[size_t(t[1])], &c = mesh.vertices[size_t(t[2])];
|
||||
const Vec3f g = (a + b + c) / 3.f;
|
||||
if (g.z() > 19.9f && keep((Vec2f(g.x(), g.y()) - centre).norm()))
|
||||
out.push_back(triangle_aspect(a, b, c));
|
||||
}
|
||||
return out;
|
||||
};
|
||||
const auto in_stroke = [&](float d) { return d < radius; };
|
||||
|
||||
const indexed_triangle_set first = bake(cube, first_stroke);
|
||||
REQUIRE(first.indices.size() > cube.indices.size());
|
||||
// What the first bake leaves around its stroke is ground the second one refines: whole pieces of
|
||||
// the cube's grid, not slivers.
|
||||
const std::vector<float> ground = top_aspects(first, first_stroke, [&](float d) { return d > radius + 1.f; });
|
||||
REQUIRE(!ground.empty());
|
||||
const float worst = *std::max_element(ground.begin(), ground.end());
|
||||
INFO("worst aspect ratio on the ground the first bake left: " << worst);
|
||||
CHECK(worst < 6.f);
|
||||
|
||||
const size_t single = top_aspects(bake(cube, second_stroke), second_stroke, in_stroke).size();
|
||||
const size_t second = top_aspects(bake(first, second_stroke), second_stroke, in_stroke).size();
|
||||
INFO("second stroke baked alone: " << single << " triangles, after the first: " << second);
|
||||
REQUIRE(single > 0);
|
||||
CHECK(second <= single * 5 / 4);
|
||||
}
|
||||
|
||||
TEST_CASE("whether a layer's texture has colour agrees with its decode", "[TextureDisplacement]")
|
||||
{
|
||||
TextureDisplacementLayer gray;
|
||||
gray.image_data = make_flat_gray_png(128);
|
||||
CHECK_FALSE(height_texture_has_color(gray));
|
||||
|
||||
TextureDisplacementLayer color;
|
||||
color.image_data = make_flat_rgb_png(200, 40, 10);
|
||||
// Before the image is decoded and after, and whatever the smoothing.
|
||||
CHECK(height_texture_has_color(color));
|
||||
CHECK(decode_height_texture(color).has_color());
|
||||
CHECK(height_texture_has_color(color));
|
||||
color.smoothing = 0.5f;
|
||||
CHECK(height_texture_has_color(color));
|
||||
|
||||
CHECK_FALSE(height_texture_has_color(TextureDisplacementLayer{}));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user