mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-10 09:11:08 +00:00
The green highlight and tint no longer cover a colour preview, and return while a stroke is painted. The Fast view also keeps the other parts of a multi-part object.
1118 lines
78 KiB
C++
1118 lines
78 KiB
C++
#ifndef slic3r_TextureDisplacement_hpp_
|
|
#define slic3r_TextureDisplacement_hpp_
|
|
|
|
#include <cmath>
|
|
#include <cstddef>
|
|
#include <Eigen/Core>
|
|
#include <cstdint>
|
|
#include <functional>
|
|
#include <memory>
|
|
#include <string>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
#include <cereal/cereal.hpp>
|
|
#include <cereal/types/array.hpp> // view_project_matrix is a std::array<float, 12>
|
|
#include <cereal/types/string.hpp>
|
|
#include <cereal/types/vector.hpp>
|
|
|
|
#include <array>
|
|
|
|
#include "Point.hpp"
|
|
#include "TriangleSelector.hpp"
|
|
|
|
namespace Slic3r {
|
|
|
|
// Optional step-by-step capture of a bake; see TextureBake/TextureBakeDebug.hpp. Forward declared and
|
|
// taken by pointer so this header, which most of the texture feature includes, does not grow a
|
|
// dependency for something only the debug view and the benchmarks use.
|
|
class BakeStageRecorder;
|
|
|
|
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.
|
|
static constexpr size_t TEXTURE_DISPLACEMENT_MAX_LAYERS = 8;
|
|
|
|
// How a layer's height texture is sampled outside its [0, 1) tile when tiling is enabled. Ignored
|
|
// (always clamp) when TextureDisplacementLayer::tile_enabled is false.
|
|
enum class TextureTileMethod : int
|
|
{
|
|
Repeat = 0, // wrap around, tile i and tile i+1 are identical (default)
|
|
MirroredRepeat = 1, // wrap around, every other tile is mirrored (no visible seam at tile edges)
|
|
};
|
|
|
|
// How a layer's texture is mapped onto the mesh.
|
|
enum class TextureProjectionMethod : int
|
|
{
|
|
// Standard *blended* tri-planar projection: the texture is sampled once per world axis (the
|
|
// XY, XZ and YZ planes) and the three samples are blended per vertex, weighted by that
|
|
// vertex's own normal raised to TRIPLANAR_BLEND_SHARPNESS.
|
|
//
|
|
// Earlier versions instead *hard-picked* the single axis most aligned with the normal. That
|
|
// has a real, visible failure mode at any edge where the dominant axis flips: on a +X face the
|
|
// planar coordinate is (y, z), on a -Y face it is (x, z), so at the shared edge u jumps from
|
|
// y_edge to x_edge. On a box centred near the origin those two agree at the (+,+) and (-,-)
|
|
// corners (making them look fine) but differ by the full corner width at the (+,-) and (-,+)
|
|
// corners, which is exactly the "two bad corners, two good ones" seam that was reported.
|
|
// Blending across the transition removes that hard discontinuity by construction.
|
|
Triplanar = 0,
|
|
// Wrapped around an axis running through the patch's centroid. The axis itself is picked
|
|
// automatically as the world axis *least* aligned with the patch's average normal (since a
|
|
// cylinder's own axis is perpendicular to its outward radial normal) - a reasonable default
|
|
// for roughly cylindrical selections, not a precise fit for arbitrary geometry.
|
|
Cylindrical = 1,
|
|
// Wrapped around the patch's centroid using longitude/latitude - reasonable for roughly
|
|
// spherical/rounded selections, again an approximation rather than an exact geodesic map.
|
|
Spherical = 2,
|
|
// Real UV unwrap of the painted patch - a proper low-distortion flattening rather than a
|
|
// planar/cylindrical/spherical approximation. The patch is first cut into charts along its
|
|
// sharp edges and each chart is flattened on its own (see compute_patch_unwrap()), so a patch
|
|
// that is not a single developable surface still unwraps sensibly. Falls back to Triplanar for
|
|
// any chart that cannot be flattened at all.
|
|
LSCM = 3,
|
|
// Flat projection along a fixed direction captured from the 3D camera ("project from view"): the
|
|
// texture is laid onto the painted area as seen from that angle, like a decal projector. Single
|
|
// planar map (no per-face axis switch), so it can smear on faces turned away from the projector -
|
|
// that is inherent to view projection and is the user's call, not a bug. The projector's two
|
|
// in-plane axes live in TextureDisplacementLayer::view_project_right/up.
|
|
ViewProjected = 4,
|
|
};
|
|
|
|
// Dihedral angle (degrees) above which an edge between two painted triangles becomes a chart seam
|
|
// - i.e. the unwrap is cut there rather than being forced to flatten across it.
|
|
//
|
|
// The whole point of this being a threshold rather than "flatten everything as one piece": three
|
|
// faces meeting at a cube corner are not developable, so a single-chart solve has to distort them
|
|
// badly to lie flat (they splay out into a fan, which is what "it merges all the edges into a
|
|
// triangle" describes). Cutting at the 90-degree edges instead lets each face flatten exactly.
|
|
// Meanwhile a smoothly curved surface - a subdivided sphere, say - has only small angles between
|
|
// neighbouring triangles, stays a single chart, and unwraps as one piece the way it should.
|
|
static constexpr float LSCM_DEFAULT_SEAM_ANGLE_DEG = 30.f;
|
|
|
|
// Exponent the tri-planar blend weights are raised to (see TextureProjectionMethod::Triplanar).
|
|
// Higher means a tighter, more "hard-edged" transition between the three axis projections; lower
|
|
// means a wider cross-fade. 4 is the usual default: tight enough that a flat face is sampled
|
|
// almost purely along its own axis, wide enough that a 90-degree edge has no visible hard seam.
|
|
static constexpr float TRIPLANAR_BLEND_SHARPNESS = 4.f;
|
|
|
|
// How a layer's displacement combines with the displacement accumulated by the layers below it
|
|
// (i.e. those in lower slots), evaluated per vertex. Analogous to an image editor's layer blend
|
|
// modes, except the quantity being blended is a signed displacement distance in mm rather than a
|
|
// pixel value.
|
|
//
|
|
// Add/Subtract are in mm and need no further explanation. Multiply/Divide are *scaling* operations
|
|
// and therefore need a unit convention: they treat the layer's own value as a unitless factor
|
|
// relative to 1 mm. That makes `depth_mm` act as a gain - a layer with depth 1 mm and a white
|
|
// (1.0) texel multiplies the accumulated relief by exactly 1, i.e. leaves it unchanged - which is
|
|
// the behaviour that makes a Multiply layer usable as a mask over the layers beneath it.
|
|
enum class TextureBlendMode : int
|
|
{
|
|
Add = 0, // acc + value (default; several layers pile their relief up together)
|
|
Subtract = 1, // acc - value (carve this layer's relief out of the layers below)
|
|
Multiply = 2, // acc * (value / 1mm) (mask/modulate the layers below by this layer)
|
|
Divide = 3, // acc / (value / 1mm) (inverse mask; guarded against a zero/near-zero divisor)
|
|
};
|
|
|
|
// Combines one layer's signed displacement `value` (mm) into `accumulated` (mm) per `mode`.
|
|
// Shared by the bake/preview path and exposed for tests.
|
|
float blend_displacement(float accumulated, float value, TextureBlendMode mode);
|
|
|
|
// Where one unwrap island (chart) sits in UV space, on top of wherever compute_patch_unwrap() first
|
|
// packed it. This is what the UV editor's drag/rotate gestures write to, so a user can lay the
|
|
// islands out by hand - move them, rotate them, overlap them - rather than being stuck with the
|
|
// automatic packing.
|
|
//
|
|
// Indexed by chart id, which compute_patch_unwrap() assigns in first-encountered-triangle order. That
|
|
// is stable for a given patch and seam angle, but *not* across a change to either: repainting the
|
|
// patch, or moving the seam-angle slider, can renumber the charts and so leave a hand-placed island
|
|
// applied to a different one. Accepted deliberately - the alternative is a persistent chart identity
|
|
// that survives arbitrary re-segmentation, which is a much larger problem than this feature warrants.
|
|
struct TextureIsland
|
|
{
|
|
Vec2f offset = Vec2f::Zero(); // in the unwrap's own mm space
|
|
float rotation_deg = 0.f; // about the island's own centroid
|
|
// About the island's own centroid too. 1 = the size compute_patch_unwrap() gave it, which is
|
|
// already its true surface area in mm - so scaling an island away from 1 deliberately makes its
|
|
// texel density differ from its neighbours'. See average_island_scales().
|
|
float scale = 1.f;
|
|
|
|
template<class Archive> void serialize(Archive &ar) { ar(offset, rotation_deg, scale); }
|
|
};
|
|
|
|
// Sets every island's scale to the mean of the current ones (Blender's "Average Islands Scale").
|
|
// Only meaningful after islands have been scaled by hand: compute_patch_unwrap() already sizes every
|
|
// chart to its true mm area, so a freshly unwrapped patch has uniform texel density to begin with.
|
|
void average_island_scales(std::vector<TextureIsland> &islands);
|
|
|
|
// One texture asset plus its projection/displacement parameters. Several layers may be painted
|
|
// onto overlapping areas of the same volume: their displacements are combined per vertex, in slot
|
|
// order, each layer folding into the total via its own TextureBlendMode (see
|
|
// build_texture_displacement()). This is what "layered/blended" texture displacement means here.
|
|
struct TextureDisplacementLayer
|
|
{
|
|
// Index into ModelVolume::texture_displacement_facets, assigned once when the layer is
|
|
// created. Not reused for the lifetime of the ModelVolume, so a deleted layer's slot simply
|
|
// becomes unused rather than being handed to a different layer.
|
|
int slot = -1;
|
|
|
|
std::string name;
|
|
// Path on the local filesystem the image was loaded from (informational; may be stale or
|
|
// empty, e.g. after loading a .3mf on a different machine).
|
|
std::string path;
|
|
// Path inside the .3mf archive once saved (empty until the project is saved once).
|
|
std::string path_in_3mf;
|
|
// Raw encoded image bytes. Only 8-bit grayscale PNG is understood by decode_height_texture()
|
|
// (libslic3r has no GUI image toolkit available); the GUI converts any imported image to that
|
|
// format before storing it here, so the baking code never needs to depend on wxWidgets.
|
|
std::shared_ptr<std::vector<unsigned char>> image_data;
|
|
|
|
float depth_mm = 0.4f; // maximum displacement along the surface normal, in mm
|
|
float tiling_scale = 12.5f; // size of one texture tile, in mm
|
|
float rotation_deg = 0.f;
|
|
Vec2f offset = Vec2f::Zero();
|
|
bool invert = false;
|
|
|
|
// The height value that means "don't move this vertex". The sampled height (0..1) has this
|
|
// subtracted before being scaled by depth_mm, so with the default of 0 the surface only ever
|
|
// moves *outwards* (the classic height-map convention), while 0.5 makes mid-grey neutral and
|
|
// lets darker texels cut *into* the surface - an engraved-and-embossed result from one map.
|
|
//
|
|
// Cutting inward is not free: vertices move along their own normals, which converge inside a
|
|
// concave corner and inside a thin wall, so a large depth_mm against a small feature really can
|
|
// fold the surface through itself. There is no cheap way to detect that here (it needs a full
|
|
// self-intersection test on the displaced mesh), so the GUI warns rather than promising safety.
|
|
float midlevel = 0.f;
|
|
|
|
// Optional blur applied to the decoded height map before it is sampled, in [0, 1]: 0 is the raw
|
|
// texture, 1 the strongest blur. Softens the relief (rounds hard edges, removes speckle) without
|
|
// needing a pre-blurred source image. Applied in decode_height_texture(), so it feeds the true
|
|
// preview, the UV editor backdrop and the bake identically.
|
|
float smoothing = 0.f;
|
|
|
|
// Optional feathering of the displacement toward the edge of the painted patch. When enabled, the
|
|
// displacement is scaled down as a vertex approaches the patch boundary, so the relief blends
|
|
// smoothly into the surrounding surface instead of ending abruptly. `edge_smoothing_amount` in
|
|
// (0, 1] sets how far the fade reaches into the patch: small values only soften a thin band at the
|
|
// very edge, 1 fades the whole patch to nothing (the painted face comes out flat). Off by default.
|
|
bool edge_smoothing = false;
|
|
float edge_smoothing_amount = 0.5f;
|
|
|
|
// Only used by TextureProjectionMethod::LSCM: when set, a fresh unwrap is laid out as a connected
|
|
// net (adjacent charts unfolded edge-to-edge along a spanning tree, see compute_connected_net())
|
|
// rather than as separately packed islands. On by default. Hand-moving an island overrides its
|
|
// placement until the next re-unwrap.
|
|
bool auto_connect_islands = true;
|
|
|
|
// When false, the texture is sampled once (clamped to its edge pixels outside [0, 1)) instead
|
|
// of being repeated - useful for a single decal-like placement rather than a repeating tile.
|
|
bool tile_enabled = true;
|
|
TextureTileMethod tile_method = TextureTileMethod::Repeat;
|
|
|
|
TextureProjectionMethod projection_method = TextureProjectionMethod::Triplanar;
|
|
// Only used by TextureProjectionMethod::LSCM. See LSCM_DEFAULT_SEAM_ANGLE_DEG.
|
|
float lscm_seam_angle_deg = LSCM_DEFAULT_SEAM_ANGLE_DEG;
|
|
// Only used by TextureProjectionMethod::LSCM: gap left between islands by the automatic packing,
|
|
// in the unwrap's mm space. Negative means "auto" (a small fraction of the packed size), which is
|
|
// what a patch that has never had the slider touched gets.
|
|
float island_padding_mm = -1.f;
|
|
// Only used by TextureProjectionMethod::LSCM: edges the unwrap is forced to cut along, on top of
|
|
// whatever the seam angle already cuts. Each pair is an undirected edge in *mesh vertex index*
|
|
// space (first < second). This is what "mark seam" (manual) and "cut island" (auto) both write to.
|
|
// Mesh-index space, so like the paint masks these are dropped on any topology change.
|
|
std::vector<std::pair<int, int>> lscm_seam_edges;
|
|
|
|
// Only used by TextureProjectionMethod::ViewProjected: the projector's in-plane axes, in the
|
|
// volume's *local* space, captured from the camera when the user hits "Project from view". A point
|
|
// projects to Vec2f(dot(pos, right), dot(pos, up)) before the usual tiling/rotation/offset.
|
|
Vec3f view_project_right = Vec3f::UnitX();
|
|
Vec3f view_project_up = Vec3f::UnitY();
|
|
|
|
// Also ViewProjected, and takes precedence over the two axes above when set: an exact *projective*
|
|
// map from a local-space position straight to a texture uv, written by the projection-frame overlay
|
|
// (the semi-transparent window dragged over the 3D view - its border becomes the uv unit square).
|
|
//
|
|
// Row-major 3x4, applied to the homogeneous point p~ = (x, y, z, 1):
|
|
// uv = ( row0.p~ / row2.p~ , row1.p~ / row2.p~ )
|
|
// The perspective divide is the whole point. view_project_right/up can only express an *affine*
|
|
// projection, which matches an orthographic camera exactly but not a perspective one - under
|
|
// perspective the near end of a part projects larger than the far end, and no pair of axes
|
|
// reproduces that. Folding the camera's full projection*view*model product into one matrix does.
|
|
// Because a point behind the projector has row2.p~ <= 0 and no meaningful uv, sampling must check
|
|
// the sign rather than divide blindly; see project_uv_projective().
|
|
//
|
|
// Note this map already includes placement, so the usual tiling/rotation/offset transform is NOT
|
|
// applied on top of it - the window's own position and size are the placement.
|
|
bool view_project_projective = false;
|
|
std::array<float, 12> view_project_matrix{};
|
|
// Only used by TextureProjectionMethod::LSCM: hand placement of the unwrap's islands, indexed by
|
|
// chart id (see TextureIsland). Shorter than the chart count simply means the missing ones are
|
|
// still where the automatic packing put them.
|
|
std::vector<TextureIsland> islands;
|
|
|
|
// Only used by TextureProjectionMethod::LSCM: persistent "join" groups, indexed by chart id. Charts
|
|
// that share a group id move together as one in the UV editor - this is what the explicit "Join"
|
|
// command records (over and above placing the child next to its parent). An entry of -1, or an index
|
|
// past the end of the vector, means the chart is its own singleton group (moves alone). Empty means
|
|
// every chart is a singleton. Same chart-renumbering caveat as `islands`: a re-unwrap can reshuffle
|
|
// chart ids, so this is meaningful only against the unwrap it was made on.
|
|
std::vector<int> island_groups;
|
|
|
|
// Only used by TextureProjectionMethod::LSCM: manual UV edits made in the UV editor's Vertex/Edge
|
|
// select modes. Each pair is (key, its overriding raw-unwrap coordinate in mm) - the *raw* unwrap
|
|
// position, i.e. before the island transform, so the edited vertex still moves and rotates with its
|
|
// island. The key names what is edited (see apply_lscm_uv_overrides()):
|
|
// - negative: one unwrapped vertex of the current unwrap, as lscm_uv_override_key(index). This is what
|
|
// the editor stores, so a seam vertex dragged in one island leaves its copies in the neighbouring
|
|
// islands where they are. Like `islands`, meaningful only against the unwrap it was made on.
|
|
// - zero or positive: a mesh vertex, every unwrapped copy of it (how edits were stored before).
|
|
std::vector<std::pair<int, Vec2f>> lscm_uv_overrides;
|
|
|
|
// How this layer folds into the displacement accumulated by the layers below it. Ignored for
|
|
// the lowest-slot painted layer, which has nothing beneath it to combine with (the GUI shows
|
|
// it as the "Base" layer and hides the control).
|
|
TextureBlendMode blend_mode = TextureBlendMode::Add;
|
|
|
|
// Colour this layer's painted area from the texture's own colours, on top of displacing by its
|
|
// height. Only meaningful when the texture actually has colour (DecodedHeightTexture::has_color()):
|
|
// the shipped library is grayscale, so this does nothing there.
|
|
//
|
|
// Colour lands in the volume's mmu_segmentation_facets - the same per-triangle filament assignment
|
|
// the MMU paint gizmo writes - so its resolution is the *mesh's*, not the image's, and a triangle
|
|
// gets exactly one filament. That is why the adaptive subdivision has a colour criterion of its
|
|
// own (see subdivide_mesh_adaptive()): without triangles along a colour boundary there is nothing
|
|
// for the boundary to be drawn on.
|
|
bool color_enabled = false;
|
|
|
|
bool empty() const { return !image_data || image_data->empty(); }
|
|
|
|
template<class Archive> void save(Archive &ar) const
|
|
{
|
|
std::string blob = image_data ? std::string(image_data->begin(), image_data->end()) : std::string();
|
|
ar(slot, name, path, path_in_3mf, blob, depth_mm, tiling_scale, rotation_deg, offset, invert, tile_enabled,
|
|
static_cast<int>(tile_method), static_cast<int>(projection_method), lscm_seam_angle_deg, islands,
|
|
static_cast<int>(blend_mode), midlevel, island_padding_mm, lscm_seam_edges, view_project_right,
|
|
view_project_up, smoothing, edge_smoothing, edge_smoothing_amount, auto_connect_islands, island_groups,
|
|
lscm_uv_overrides, view_project_projective, view_project_matrix, color_enabled);
|
|
}
|
|
template<class Archive> void load(Archive &ar)
|
|
{
|
|
std::string blob;
|
|
int tile_method_int = 0;
|
|
int projection_method_int = 0;
|
|
int blend_mode_int = 0;
|
|
ar(slot, name, path, path_in_3mf, blob, depth_mm, tiling_scale, rotation_deg, offset, invert, tile_enabled,
|
|
tile_method_int, projection_method_int, lscm_seam_angle_deg, islands, blend_mode_int, midlevel,
|
|
island_padding_mm, lscm_seam_edges, view_project_right, view_project_up, smoothing, edge_smoothing,
|
|
edge_smoothing_amount, auto_connect_islands, island_groups, lscm_uv_overrides, view_project_projective,
|
|
view_project_matrix, color_enabled);
|
|
image_data = blob.empty() ? nullptr : std::make_shared<std::vector<unsigned char>>(blob.begin(), blob.end());
|
|
tile_method = static_cast<TextureTileMethod>(tile_method_int);
|
|
projection_method = static_cast<TextureProjectionMethod>(projection_method_int);
|
|
blend_mode = static_cast<TextureBlendMode>(blend_mode_int);
|
|
}
|
|
};
|
|
|
|
|
|
// Settings that apply to the whole layer stack rather than to one layer, held per ModelVolume next
|
|
// to texture_displacement_layers and consumed by build_texture_displacement().
|
|
struct TextureDisplacementOptions
|
|
{
|
|
// Whether the painted patch's *border* vertices - the ones also used by unpainted triangles -
|
|
// are displaced along with the rest, or pinned flat.
|
|
//
|
|
// Pinning them was originally justified as keeping the patch from tearing away from the
|
|
// surrounding surface. That reasoning no longer applies: since the bake became
|
|
// topology-preserving it only ever *moves* the input's own vertices, so a border vertex is one
|
|
// vertex shared by both regions and moving it simply tilts the unpainted triangles that use it -
|
|
// nothing can come apart. What pinning actually does is clamp the outermost ring of the relief to
|
|
// zero, which on a fully painted face collapses the pattern into a ring of steep ramps right at
|
|
// the edge (the "it doesn't extrude at the border" artifact). Displacing it is the default;
|
|
// pinning is kept for the case where the relief must not spill past the paint at all.
|
|
bool displace_border = true;
|
|
|
|
// Optional Laplacian relaxation of the displaced surface, run after all layers have been folded
|
|
// in - a post-process, not a texture filter (TextureDisplacementLayer::smoothing blurs the height
|
|
// map instead, before it is ever sampled). Rounds off the hard steps a bitmap height map leaves
|
|
// behind. Restricted to vertices the displacement actually moved, so the rest of the model keeps
|
|
// its exact geometry. `smooth_strength` in [0, 1] is how far each pass moves a vertex toward the
|
|
// average of its neighbours.
|
|
bool smooth_enabled = false;
|
|
float smooth_strength = 0.3f;
|
|
int smooth_iterations = 2;
|
|
|
|
// Hold the painted patch's outermost ring of vertices out of the smoothing. Those vertices sit
|
|
// next to unpainted ones that are pinned by definition, so relaxing them drags the rim of the
|
|
// relief back down toward the undisplaced surface - the pattern looks half-melted exactly where it
|
|
// meets the edge, however crisp the rest of it is. Excluding them keeps the border extruded at
|
|
// full depth and smooths only the interior. On by default; turn it off to soften the outer edge
|
|
// deliberately (which is a blunter version of the per-layer edge-smoothing falloff).
|
|
bool smooth_skip_border = true;
|
|
|
|
// Which bake pipeline. On (the default): refine, remove slivers, displace and simplify in one run,
|
|
// nothing to prepare first. Off, the classic path: the mesh is prepared first (remesh, adaptive
|
|
// subdivision, step cut) and then displaced vertex by vertex, keeping the topology - which is what
|
|
// colour needs, since the per-facet assignment has nothing stable to attach to once the topology is
|
|
// rebuilt. Projects saved with the classic path keep it: the flag is stored per volume.
|
|
bool pipeline_v2 = true;
|
|
// Refinement edge length, mm. 0 (the default) means automatic: chosen from the texture's texel
|
|
// size and sharpness and the model's size, see recommend_v2_resolution(). A saved project with an
|
|
// explicit value keeps it.
|
|
float v2_refine_mm = 0.f;
|
|
bool v2_regularize = false;
|
|
// Simplification target in thousands of triangles. -1 (the default) means automatic, from the
|
|
// same recommendation; 0 skips simplification, which is worth comparing on its own.
|
|
int v2_max_triangles_k = -1;
|
|
// Slide vertices onto the texture's own edges before displacing. Displacement moves vertices along
|
|
// the normal only, so without this a step in the image is reproduced wherever the triangle grid
|
|
// happens to fall, as a staircase rather than a straight wall.
|
|
bool v2_relocate = false;
|
|
// Data-dependent edge flips before displacement (see TextureBakeFlip.hpp). On by default and not a
|
|
// user setting; deliberately left out of serialize() so project files are unaffected.
|
|
bool v2_flip_edges = true;
|
|
|
|
// Colour, all of which belongs to the stack rather than to any one layer: it is about how the
|
|
// printer will realise the colours, not about which image they came from.
|
|
|
|
// Interleave pairs of filaments to get colours between them - so four loaded filaments offer far
|
|
// more than four colours. Whether a given layer's colours actually use mixes is decided from its
|
|
// image (TextureDetail::flat_colors): a texture of flat colours prints in single filaments, a
|
|
// photograph or gradient in mixes. Off forces single filaments everywhere.
|
|
bool color_mix_enabled = true;
|
|
// The most mixes the palette may offer. Every mix a bake paints with becomes a mixed filament slot,
|
|
// so this is also the most slots one bake can add. The mixes themselves are picked from the
|
|
// texture's colours, those that improve the match the most coming first.
|
|
int color_mix_count = 8;
|
|
// Majority-filter passes over the assigned colours. See TextureColorRequest::despeckle_passes -
|
|
// this is the control for it, and 2 is enough to clear the salt-and-pepper an image with detail
|
|
// finer than the mesh leaves behind, without eating features that are genuinely a facet wide.
|
|
int color_despeckle = 2;
|
|
|
|
template<class Archive> void serialize(Archive &ar)
|
|
{
|
|
ar(displace_border, smooth_enabled, smooth_strength, smooth_iterations, smooth_skip_border,
|
|
pipeline_v2, v2_refine_mm, v2_regularize, v2_max_triangles_k,
|
|
v2_relocate, color_mix_enabled, color_despeckle, color_mix_count);
|
|
}
|
|
};
|
|
|
|
// How much detail a height texture carries: central differences of the grey image, the mean gradient
|
|
// and the share of texels steeper than 30 grey levels, mapped to how many texels one mesh edge may
|
|
// span (1 for a hard-edged image, 4 for a smooth one). Cached per image, like the decode.
|
|
struct TextureDetail
|
|
{
|
|
float mean_gradient = 0.f;
|
|
float sharp_fraction = 0.f;
|
|
float pixels_per_edge = 4.f;
|
|
// How much of the image its eight most common colours cover (8 levels per channel), and the
|
|
// verdict: a "flat-colour" image (tiles, logos, camouflage) whose colours should each print in a
|
|
// single filament, versus a photograph or gradient where interleaved filament mixes pay off.
|
|
float flat_share = 0.f;
|
|
bool flat_colors = false;
|
|
};
|
|
TextureDetail analyze_texture_detail(const TextureDisplacementLayer &layer);
|
|
|
|
// The default pipeline's automatic resolution and budget, when the options leave them at "auto":
|
|
// edge = the model's world-space diagonal / 250, clamped to [0.05, 5] mm and rounded up to 0.01;
|
|
// budget 750 k. The texel size of the finest layer and its sharpness class are
|
|
// reported alongside for the panel. `edge_mm` is 0 for an empty mesh.
|
|
struct V2Resolution
|
|
{
|
|
float edge_mm = 0.f;
|
|
int budget_k = 0;
|
|
float texel_mm = 0.f;
|
|
float pixels_per_edge = 0.f;
|
|
bool budget_bound = false; // the edge came from the triangle cap, not from the texture
|
|
};
|
|
V2Resolution recommend_v2_resolution(const indexed_triangle_set &mesh,
|
|
const std::vector<TextureDisplacementLayer> &layers,
|
|
const Transform3d &volume_to_world = Transform3d::Identity());
|
|
|
|
// Decoded height (and, for a colour source image, colour) samples, independent of any GUI/OpenGL
|
|
// texture object so they can be evaluated from a background bake Job as well as from GUI-side
|
|
// preview code.
|
|
struct DecodedHeightTexture
|
|
{
|
|
std::vector<uint8_t> pixels; // height: row-major, top-to-bottom, one byte per pixel
|
|
// Colour: the same grid, three bytes per pixel, or empty when the source image was grayscale.
|
|
// A grayscale height map has no colour to give - `pixels` is not a colour, it is a height - so
|
|
// has_color() is what the whole colour feature keys off: a layer set to colour a model with a
|
|
// grayscale texture on it simply colours nothing.
|
|
std::vector<uint8_t> rgb;
|
|
int width = 0;
|
|
int height = 0;
|
|
|
|
bool empty() const { return width <= 0 || height <= 0 || pixels.empty(); }
|
|
bool has_color() const { return !empty() && rgb.size() == size_t(width) * size_t(height) * 3; }
|
|
// Bilinearly sampled height in [0, 1] at a normalized uv coordinate. When tile_enabled is false,
|
|
// a uv outside [0, 1) samples as 0 - the texture simply is not there, rather than its border
|
|
// row/column being smeared outward forever (which is what clamping the coordinate would do, and
|
|
// was a real reported bug). Callers rely on this to get a hard edge: it is how the projection
|
|
// frame's border becomes the edge of the displacement.
|
|
float sample(const Vec2f &uv, bool tile_enabled = true, TextureTileMethod tile_method = TextureTileMethod::Repeat) const;
|
|
// The same sample, in colour: linear RGB components in [0, 1]. Outside a non-tiled placement, and
|
|
// for a grayscale source, this is (0, 0, 0) - callers pair it with has_color() and with the
|
|
// height's own coverage rather than trying to read "no texture here" out of the colour itself.
|
|
Vec3f sample_color(const Vec2f &uv, bool tile_enabled = true,
|
|
TextureTileMethod tile_method = TextureTileMethod::Repeat) const;
|
|
|
|
// Where a uv lands on the texel grid: the four texels of the bilinear tap and their weights.
|
|
// Shared by sample() and sample_color(), so a layer's height and its colour can never end up
|
|
// read from different places in the image. False means the uv is outside a non-tiled placement -
|
|
// no texture there at all (see sample()).
|
|
struct TexelTap
|
|
{
|
|
int x0 = 0, y0 = 0, x1 = 0, y1 = 0;
|
|
float tx = 0.f, ty = 0.f;
|
|
};
|
|
bool texel_tap(const Vec2f &uv, bool tile_enabled, TextureTileMethod tile_method, TexelTap &out) const;
|
|
};
|
|
|
|
// Decode a layer's raw image bytes into sampleable height data, plus colour when the source has any.
|
|
// Both 8-bit grayscale PNGs (the shipped library, and anything imported before colour was kept) and
|
|
// colour PNGs are accepted; for a colour source the height is its luminance, using the same
|
|
// coefficients wxImage::ConvertToGreyscale() uses, so a texture imported as colour displaces exactly
|
|
// as it did when the importer flattened it to grey on the way in. Returns an empty
|
|
// DecodedHeightTexture if image_data is empty or is not a PNG at all.
|
|
DecodedHeightTexture decode_height_texture(const TextureDisplacementLayer &layer);
|
|
|
|
// decode_height_texture(layer).has_color(), answered from the decode cache rather than from a copy of the
|
|
// texture - cheap enough to ask every frame. Smoothing does not change it, so the raw decode is what is read.
|
|
bool height_texture_has_color(const TextureDisplacementLayer &layer);
|
|
|
|
// Maps a linear RGB colour in [0, 1] to an index into the caller's palette, or -1 for "no colour".
|
|
//
|
|
// Deliberately a callback rather than a function here: matching a colour to a filament is a
|
|
// *perceptual* question (CIEDE2000 over CIELAB), and that machinery - slic3r/Utils/ColorSpaceConvert
|
|
// and GuiColor - lives on the GUI side along with the list of filaments actually loaded. libslic3r
|
|
// samples the image and decides *where* colour changes; the GUI decides *which* filament each colour
|
|
// is. See GLGizmoTextureDisplacement::make_palette_quantizer().
|
|
using ColorQuantizeFn = std::function<int(const Vec3f &)>;
|
|
|
|
|
|
// One printable colour: either a loaded filament on its own, or a blend of two of them realised by
|
|
// interleaving, which the slicer does per print layer. Plain data, so it can be captured into a job.
|
|
struct PrintableColor
|
|
{
|
|
Vec3f rgb = Vec3f::Zero(); // what it looks like; for a mix, the colour its mixed filament slot shows
|
|
int a = 0; // filament index
|
|
int b = 0; // the second filament; == a for a pure entry
|
|
int num = 1; // a's share of the interleave, out of `den`
|
|
int den = 1;
|
|
bool is_mix() const { return a != b; }
|
|
// a's share in percent, the form a mixed filament slot is created from.
|
|
int a_percent() const { return int(std::lround(100.0 * double(num) / double(den))); }
|
|
};
|
|
|
|
// Everything needed to colour a mesh, captured on the main thread and handed to a job. An empty
|
|
// palette means nothing is colouring, which is the state every one of these paths starts in.
|
|
struct TextureColorSettings
|
|
{
|
|
std::vector<PrintableColor> palette;
|
|
std::vector<PrintableColor> palette_pure; // the filaments alone, for flat-colour images
|
|
int despeckle_passes = 2;
|
|
|
|
bool empty() const { return palette.empty(); }
|
|
};
|
|
|
|
// Raw dominant-axis planar projection of `position` (in mm, not yet scaled/rotated/offset by any
|
|
// layer), dropping the axis position that best aligns with `normal`. Exposed on its own (rather
|
|
// than only inline inside project_texture_displacement_uv()) so GUI code - the on-canvas
|
|
// "adjust texture placement" gizmo - can map a dragged 3D point into the exact same 2D space
|
|
// tiling_scale/rotation_deg/offset operate in, without duplicating the axis-selection logic.
|
|
Vec2f project_planar(const Vec3f &position, const Vec3f &normal);
|
|
|
|
// Applies a layer's tiling_scale/rotation_deg/offset to an already-projected planar coordinate
|
|
// (in mm, dominant-axis planar, cylindrical, spherical, or CGAL LSCM output - any of them, all
|
|
// share this same final step). Exposed separately so build_texture_displacement() can route CGAL
|
|
// 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).
|
|
// `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 -
|
|
// when the point lies behind the projector or on its plane (w <= 0), where there is no meaningful uv
|
|
// and dividing would produce a mirrored or infinite coordinate. Callers treat that as "no height".
|
|
bool project_uv_projective(const std::array<float, 12> &m, const Vec3f &position, Vec2f &uv);
|
|
|
|
// Sample a layer's height texture at a mesh-local position, honouring the layer's projection
|
|
// method, tiling scale, rotation, offset and tiling mode. Returns a height in [0, 1].
|
|
//
|
|
// This returns a *height* rather than a UV because TextureProjectionMethod::Triplanar is a blend
|
|
// of three separate axis projections and therefore takes three texture samples per vertex - there
|
|
// is no single UV that represents it. The other methods do map to one UV internally.
|
|
// - `normal` is this specific vertex's own normal; used only by Triplanar (for its blend weights).
|
|
// - `patch_center`/`patch_axis` describe the painted patch as a whole (its centroid, and - for
|
|
// Cylindrical only - the wrap axis); used only by the Cylindrical/Spherical methods.
|
|
// - `lscm_uv`, when non-null, is this vertex's precomputed LSCM coordinate and takes precedence
|
|
// over `layer.projection_method` (LSCM is a single per-patch solve, not a per-vertex formula,
|
|
// so build_texture_displacement() computes it once up front and passes it in here).
|
|
// patch_center/patch_axis are cheap to compute once per patch and passed through unchanged for
|
|
// every vertex rather than being re-derived per call.
|
|
float sample_layer_height(const DecodedHeightTexture &texture, const TextureDisplacementLayer &layer,
|
|
const Vec3f &position, const Vec3f &normal,
|
|
const Vec3f &patch_center = Vec3f::Zero(), const Vec3f &patch_axis = Vec3f::UnitZ(),
|
|
const Vec2f *lscm_uv = nullptr);
|
|
|
|
// The same sample, in colour, through the identical projection/tiling/placement path - so a layer's
|
|
// colour lands on the model exactly where its relief does, whatever projection it is using. Returns
|
|
// false (leaving `out` untouched) when the texture has no colour, or when the point falls outside a
|
|
// non-tiled placement, or behind a projective "from view" projector: all three mean "this layer does
|
|
// not colour this point", which is different from "this layer colours it black".
|
|
bool sample_layer_color(const DecodedHeightTexture &texture, const TextureDisplacementLayer &layer,
|
|
const Vec3f &position, const Vec3f &normal, Vec3f &out,
|
|
const Vec3f &patch_center = Vec3f::Zero(), const Vec3f &patch_axis = Vec3f::UnitZ(),
|
|
const Vec2f *lscm_uv = nullptr);
|
|
|
|
// 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
|
|
// wherever the layer is actually painted. Returns false (leaving the outputs untouched) if the
|
|
// layer has nothing painted yet.
|
|
bool compute_layer_paint_anchor(const indexed_triangle_set &base_mesh,
|
|
const TriangleSelector::TriangleSplittingData &facet_data,
|
|
Vec3f &anchor_pos,
|
|
Vec3f &anchor_normal);
|
|
|
|
// Extracts the currently painted patch from a volume's base mesh + stored facet data - the same
|
|
// extraction build_texture_displacement() and compute_layer_paint_anchor() each do internally via
|
|
// TriangleSelector::get_facets_strict(ENFORCER). Returns an empty mesh if nothing is painted.
|
|
// Exposed so GUI code (the LSCM "UV editor" preview pane) can get the same patch build_texture_
|
|
// displacement() would act on, without duplicating the deserialize/get_facets_strict boilerplate.
|
|
indexed_triangle_set extract_painted_patch(const indexed_triangle_set &base_mesh,
|
|
const TriangleSelector::TriangleSplittingData &facet_data);
|
|
|
|
// A patch flattened into 2D. The patch is first split into charts along edges sharper than
|
|
// `seam_angle_deg` (see LSCM_DEFAULT_SEAM_ANGLE_DEG), each chart is flattened independently, the
|
|
// charts are scaled to their true mm size and packed side by side.
|
|
//
|
|
// A vertex sitting on a seam belongs to several charts at once and therefore has a *different* UV
|
|
// in each of them, so this cannot be a plain "one UV per patch vertex" array: seam vertices are
|
|
// duplicated, once per chart touching them. `indices` is the patch's own triangle list re-indexed
|
|
// onto that duplicated vertex set, and `source_vertex` maps each duplicate back to the patch vertex
|
|
// it came from.
|
|
struct PatchUnwrap
|
|
{
|
|
std::vector<Vec2f> uvs; // one per unwrapped vertex, in mm
|
|
std::vector<int> source_vertex; // unwrapped vertex -> index into patch.vertices
|
|
std::vector<int> vertex_chart; // unwrapped vertex -> chart (island) id
|
|
// The patch's triangles re-indexed into `uvs` - but *grouped by chart*, not left in the patch's
|
|
// own order: the charts are flattened one at a time and then concatenated. `source_face` is the
|
|
// map back, so anything that needs UVs per triangle corner (as opposed to per vertex) can place
|
|
// them against its own triangle list. See compute_lscm_corner_uvs().
|
|
std::vector<stl_triangle_vertex_indices> indices;
|
|
std::vector<int> source_face; // unwrapped triangle -> index into patch.indices
|
|
// Per chart, the centroid of its uvs - the point a TextureIsland's rotation turns about.
|
|
std::vector<Vec2f> chart_centroid;
|
|
// Edges belonging to exactly one triangle: the outline of each island. Indices into `uvs`. This
|
|
// is what the UV editor draws highlighted, so the boundaries the seam angle cut are visible.
|
|
std::vector<std::pair<int, int>> boundary_edges;
|
|
int chart_count = 0;
|
|
|
|
bool empty() const { return indices.empty(); }
|
|
};
|
|
|
|
// Applies an island's hand placement (scale + rotation about its own centroid, then offset) to one
|
|
// unwrapped UV. A chart with no entry in `islands` is left exactly where the packing put it.
|
|
Vec2f apply_island_transform(const Vec2f &uv, int chart, const PatchUnwrap &unwrap, const std::vector<TextureIsland> &islands);
|
|
|
|
// The same transform as a 2x3 affine matrix (columns: x basis, y basis, translation), for callers
|
|
// that would otherwise apply it to every vertex of an island one at a time. The UV editor renders
|
|
// each island through this as a uniform, which is what lets a drag move an island without touching
|
|
// its vertex buffer at all.
|
|
Eigen::Matrix<float, 2, 3> island_transform_matrix(int chart, const PatchUnwrap &unwrap, const std::vector<TextureIsland> &islands);
|
|
|
|
// Lays the unwrap's charts out as connected nets: charts that share a mesh edge are unfolded so
|
|
// their shared edge coincides (a cube -> its six faces joined along a spanning tree of edges, the rest
|
|
// left as free borders). Charts stay separate islands, so their borders still show and any of them can
|
|
// still be moved by hand afterwards. Each net grows from the largest chart not yet placed; a chart whose
|
|
// triangles would overlap the net stays out of it and starts a net of its own. The nets are then packed
|
|
// side by side. Returns one placement per chart. See the gizmo's auto-connect option.
|
|
std::vector<TextureIsland> compute_connected_net(const PatchUnwrap &unwrap);
|
|
|
|
// The placement that unfolds `child` onto `parent` along their shared mesh edge, honouring `parent`'s
|
|
// current placement in `islands`. Returns false if the two charts share no edge. Backs the manual
|
|
// "Join" command; compute_connected_net() does the same thing across a whole spanning tree.
|
|
bool join_chart_placement(const PatchUnwrap &unwrap, const std::vector<TextureIsland> &islands,
|
|
int child, int parent, TextureIsland &out_child);
|
|
|
|
// Unwraps `patch` as described above. Charts that are flat (within a degree) are projected onto
|
|
// their own tangent plane directly, which is both exact and far cheaper than a solve; only genuinely
|
|
// curved charts go through CGAL's LSCM parameterizer (MeshBoolean::cgal::parameterize_lscm()). A
|
|
// chart that LSCM cannot flatten at all (it is not a topological disk - closed, or with a hole)
|
|
// falls back to that same tangent-plane projection.
|
|
//
|
|
// `padding_mm` is the gap the packing leaves between islands; negative means auto (see
|
|
// TextureDisplacementLayer::island_padding_mm). `seam_edges` are extra edges to cut along regardless
|
|
// of angle (manual/auto seams), in the patch's own vertex-index space (which is the mesh's, since the
|
|
// patch carries the whole vertex array - see get_facets_strict()).
|
|
//
|
|
// Results are cached, keyed on the patch's geometry, the seam angle, the padding and the seam edges:
|
|
// nothing else about a layer (depth, tiling, rotation, offset, texture, island placement) changes the
|
|
// unwrap, so dragging any of those sliders must not pay for a re-solve.
|
|
PatchUnwrap compute_patch_unwrap(const indexed_triangle_set &patch, float seam_angle_deg = LSCM_DEFAULT_SEAM_ANGLE_DEG,
|
|
float padding_mm = -1.f, const std::vector<std::pair<int, int>> &seam_edges = {});
|
|
|
|
// One UV per patch vertex, **for displacement only**. Displacement is inherently per-vertex - a
|
|
// vertex has exactly one position, so it can only be pushed out by one height - which means a seam
|
|
// vertex has to settle on a single one of its charts' UVs (the first, arbitrarily). That is not a
|
|
// compromise in the result: the surface stays watertight either way, since neighbouring vertices
|
|
// each move along their own normals and nothing depends on the UVs agreeing across the seam.
|
|
//
|
|
// Anything that samples or draws per *triangle* must use compute_lscm_corner_uvs() instead. This
|
|
// collapse is wrong for those: a triangle at a seam that the island layout did not join gets handed
|
|
// a neighbouring island's placement, which showed up as a single skewed triangle per face and as
|
|
// every island's texture following the lowest-numbered island when it was dragged.
|
|
//
|
|
// Returns an empty vector if the patch has no triangles. Takes the whole layer because it applies
|
|
// both the layer's seam angle and its hand-placed islands.
|
|
std::vector<Vec2f> compute_lscm_uvs(const indexed_triangle_set &patch, const TextureDisplacementLayer &layer);
|
|
|
|
// Three UVs per patch triangle (corner 0, 1, 2 of triangle i at index 3i..3i+2), in the patch's own
|
|
// triangle order. Unlike compute_lscm_uvs() this keeps a seam vertex's separate per-chart copies: a
|
|
// triangle belongs to exactly one chart and is given that chart's UVs, which is what every consumer
|
|
// that works per triangle rather than per vertex needs - the fast preview's flat mesh, the checker
|
|
// overlay and the bake's per-facet colour.
|
|
//
|
|
// Returns an empty vector if the patch has no triangles or the unwrap carries no source_face map.
|
|
std::vector<Vec2f> compute_lscm_corner_uvs(const indexed_triangle_set &patch, const TextureDisplacementLayer &layer);
|
|
|
|
// The TextureDisplacementLayer::lscm_uv_overrides key for one unwrapped vertex (an index into PatchUnwrap::uvs).
|
|
inline int lscm_uv_override_key(int unwrapped_vertex) { return -(unwrapped_vertex + 1); }
|
|
|
|
// Writes the overrides into `unwrap.uvs`: mesh-vertex keys onto every copy of their vertex, then unwrapped-vertex
|
|
// keys onto their one copy. Returns, per unwrapped vertex, whether an override set it.
|
|
std::vector<bool> apply_lscm_uv_overrides(PatchUnwrap &unwrap, const std::vector<std::pair<int, Vec2f>> &overrides);
|
|
|
|
// One paint mask (as stored by ModelVolume::texture_displacement_facets) per possible layer slot.
|
|
using TextureDisplacementFacetsData = std::array<TriangleSelector::TriangleSplittingData, TEXTURE_DISPLACEMENT_MAX_LAYERS>;
|
|
|
|
// Bake all painted texture-displacement layers into `base_mesh`'s geometry, restricted to the
|
|
// painted area(s) only (the rest of the mesh is left untouched). Returns the mesh unchanged if
|
|
// nothing is painted or no layer has a usable texture.
|
|
//
|
|
// **Topology-preserving**: the returned mesh has exactly `base_mesh`'s vertices and triangles, in
|
|
// the same order - only the positions of displaced vertices differ. Every layer's paint mask is
|
|
// evaluated against `base_mesh` directly, and each vertex accumulates a single signed displacement
|
|
// (in mm) that all the layers covering it fold into, in slot order, via their TextureBlendMode.
|
|
// The vertex is then moved once, along its base-mesh normal, by that accumulated total.
|
|
//
|
|
// This replaced an earlier design that instead applied the layers *sequentially*, re-meshing after
|
|
// each one and carrying the next layer's paint mask onto the result with
|
|
// TriangleSelector::remap_painting(). That was the cause of a real "the second texture is never
|
|
// applied" bug: remapping a mask onto a mesh whose vertices had just been displaced out from under
|
|
// it routinely produced an empty bitstream, and the layer was then silently skipped. It is also
|
|
// what forced the per-layer vertex duplication and the final its_compactify_vertices() pass. The
|
|
// accumulate-then-displace formulation has neither problem, is substantially faster (no remap, no
|
|
// welding, one pass over the mesh), and - because the output keeps the input's exact vertex
|
|
// indexing - lets the GUI overlay a preview on the base mesh without any index translation.
|
|
//
|
|
// A vertex used by even one *unpainted* triangle of a layer's mask sits on that layer's boundary.
|
|
// Whether it moves is TextureDisplacementOptions::displace_border; see that field for why displacing
|
|
// it is safe (and the default). Either way the *direction* every vertex moves in is the area-weighted
|
|
// normal of the triangles that are painted in at least one layer - not of the whole mesh - so a
|
|
// border vertex travels along the painted surface's own normal instead of a blend with whatever
|
|
// unpainted geometry meets it there. Without that, the rim of a fully painted face would displace
|
|
// along the 45 degrees bisector it shares with the side wall and flare outwards. Interior vertices
|
|
// have every incident triangle painted, so for them the two are the same normal.
|
|
//
|
|
// Takes plain copied data rather than a ModelVolume reference so it is safe to call from a
|
|
// background thread (e.g. a bake Job's process() method) on a snapshot captured on the main
|
|
// thread, without touching the live Model concurrently with the UI.
|
|
//
|
|
// Known limitation: this does not attempt to remap texture-displacement paint data across
|
|
// topology-changing operations performed outside this gizmo (e.g. ModelObject::split(),
|
|
// 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%).
|
|
//
|
|
// `color`, when given, also reports which filament each triangle should print in - see
|
|
// TextureColorRequest.
|
|
struct TextureColorRequest
|
|
{
|
|
// RGB -> palette index. Supplied by the GUI, which owns both the perceptual matching and the list
|
|
// of filaments actually loaded (see ColorQuantizeFn).
|
|
ColorQuantizeFn quantize;
|
|
// The same over the loaded filaments alone, no mixes. Optional; when given, a layer whose image is
|
|
// made of flat colours (TextureDetail::flat_colors) is matched with this one, so a tile or a logo
|
|
// prints in single filaments while a photograph on another layer may still use mixes.
|
|
ColorQuantizeFn quantize_pure;
|
|
// Majority-filter passes over the *perceived* colour, before any interleaving is resolved.
|
|
//
|
|
// Sampling a detailed image once per triangle leaves salt-and-pepper wherever the image's own
|
|
// detail is finer than the mesh: two neighbouring facets land either side of some contour and flip
|
|
// colour independently. Replacing each facet's colour with the most common one among itself and
|
|
// its edge neighbours removes exactly that, and leaves any feature wider than a facet alone. 0
|
|
// turns it off.
|
|
int despeckle_passes = 0;
|
|
// After the despeckle: connected patches of one colour smaller than this (mm^2) are recoloured
|
|
// to whatever borders them most - see merge_small_color_regions(). The despeckle only reaches
|
|
// 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 *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()).
|
|
std::vector<uint8_t> *out_triangle = nullptr;
|
|
};
|
|
|
|
// Recolours connected patches of one colour whose area is under `min_area_mm2` to the colour that
|
|
// borders them most (by shared edge length). Colour is per triangle, -1 = none (never merged into,
|
|
// never merged away). Removes the confetti a detailed image leaves on a fine mesh - thousands of
|
|
// one-facet zones, which the slicer's multi-material segmentation cannot digest. Patches are
|
|
// processed smallest-first, reading their neighbours' current colour, so a chain of tiny islands
|
|
// collapses into its surroundings rather than into each other.
|
|
void merge_small_color_regions(const indexed_triangle_set &mesh, std::vector<int> &color, float min_area_mm2);
|
|
|
|
// Where the volume sits on the plate: its instance transform times its own volume transform, i.e.
|
|
// mesh coordinates -> world millimetres.
|
|
//
|
|
// Every number the user sets is in real millimetres on the printed part - "Depth (mm)", "Tile size
|
|
// (mm)" - and the build plate is a world plane, so the bake runs in world space and transforms the
|
|
// result back. Doing it in the volume's own coordinates instead made a scaled instance stretch both
|
|
// the relief depth and the tiling by the scale factor, and under a non-uniform scale it also
|
|
// displaced along the wrong direction: a mesh normal maps to the world normal through the inverse
|
|
// transpose, not through the transform itself, so the relief leaned. Identity - the default - is
|
|
// exactly the old behaviour and is what an untransformed volume gives.
|
|
// What a bake spent, for whoever wants to report it. Only the default pipeline fills it in; the
|
|
// classic path moves the vertices the mesh already has and has nothing to say here.
|
|
struct TextureBakeStats
|
|
{
|
|
// What the refinement produced, before simplification.
|
|
size_t triangles_refined = 0;
|
|
// What the result had to fit into: the budget for what this bake refines, plus the triangles it
|
|
// only preserves (an earlier bake's relief, which this one does not paint).
|
|
size_t triangles_budget = 0;
|
|
// The refined mesh did not fit, and removing its flat faces was not enough, so the simplification
|
|
// had to take detail out of it: the result carries less of the texture than the resolution asked for.
|
|
bool budget_limited = false;
|
|
};
|
|
|
|
indexed_triangle_set build_texture_displacement(const indexed_triangle_set &base_mesh,
|
|
const std::vector<TextureDisplacementLayer> &layers,
|
|
const TextureDisplacementFacetsData &facets_data,
|
|
const TextureDisplacementOptions &options = {},
|
|
const DisplacementProgressFn &progress = {},
|
|
const TextureColorRequest *color = nullptr,
|
|
const Transform3d &volume_to_world = Transform3d::Identity(),
|
|
// When given and enabled, receives the mesh after each
|
|
// stage, already brought back into `base_mesh`'s frame.
|
|
BakeStageRecorder *debug = nullptr,
|
|
// When given, receives what the bake spent.
|
|
TextureBakeStats *stats = nullptr);
|
|
|
|
// `volume`'s mesh coordinates -> world millimetres: its first instance's transform times its own.
|
|
// The mesh is shared by every instance, so a multi-instance object can only be baked for one of
|
|
// them; the first is what the gizmo edits against. Identity when the volume has no object yet.
|
|
Transform3d texture_displacement_volume_to_world(const ModelVolume &volume);
|
|
// The frame the bake and the previews project the texture in: `volume_to_world` with its translation
|
|
// removed, i.e. world orientation and scale about the volume's own origin. See build_texture_displacement().
|
|
Transform3d texture_displacement_bake_frame(const Transform3d &volume_to_world);
|
|
|
|
// Area-weighted vertex normals of `its` - the directions the bake both projects and displaces along.
|
|
std::vector<Vec3f> texture_displacement_vertex_normals(const indexed_triangle_set &its);
|
|
|
|
// The frame the Cylindrical and Spherical projections wrap around: `patch`'s triangle-corner centroid,
|
|
// the world axis *least* aligned with the average of `vertex_normals` over those corners (a cylinder's
|
|
// own axis is perpendicular to its outward radial normal), and that average normal itself.
|
|
//
|
|
// Results come out in whatever frame `patch` is given in. The bake calls this with the patch already in
|
|
// the bake frame (see texture_displacement_bake_frame()), so a preview that wants to reproduce the
|
|
// bake's projection must too, or it wraps the texture around a different centre. Corners past the end
|
|
// of `vertex_normals` - the ones a brush stroke appended - contribute to the centroid but carry no
|
|
// normal, exactly as the bake's own loops skip them.
|
|
void texture_displacement_patch_frame(const indexed_triangle_set &patch,
|
|
const std::vector<Vec3f> &vertex_normals,
|
|
Vec3f ¢er, Vec3f &axis, Vec3f &average_normal);
|
|
|
|
// Convenience overload for main-thread callers: extracts the mesh/layers/paint data/options from
|
|
// `volume` and forwards to the overload above.
|
|
indexed_triangle_set build_texture_displacement(const ModelVolume &volume);
|
|
|
|
// Laplacian relaxation of `mesh` in place, restricted to the vertices flagged in `movable` (sized to
|
|
// the mesh's vertex count; anything else is held exactly where it is and still acts as an anchor for
|
|
// its neighbours). Each of `iterations` passes moves a movable vertex a `strength` fraction of the
|
|
// way to the average of the vertices it shares an edge with, computed from the positions at the
|
|
// start of that pass so the result does not depend on vertex order.
|
|
//
|
|
// Topology-preserving like the bake itself, so it composes with it: this is what "smooth the relief
|
|
// 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, 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
|
|
// where the displaced surface has *curvature* worth spending triangles on. Called serially from the
|
|
// subdivider, so it only needs to be safe on the calling thread.
|
|
using HeightFieldSampler = std::function<float(const Vec3f &pos, const Vec3f &normal)>;
|
|
|
|
// Builds a sampler of the *combined* (all-layers) displacement height in mm at an arbitrary surface
|
|
// point, for feature-adaptive subdivision. It mirrors build_texture_displacement()'s per-layer setup
|
|
// (decode, patch centroid, cylinder axis, blend order, "lowest layer folds additively") but evaluates
|
|
// per point instead of per vertex. Two deliberate simplifications, both erring toward *more* detail
|
|
// (safe - over-refinement is never a crack): every sampleable layer is evaluated at every point (no
|
|
// per-point paint-mask test, so a point sees all layers' textures, not only the ones painted there),
|
|
// and edge-smoothing's boundary falloff is ignored. LSCM layers have no per-point UV and are skipped.
|
|
// Returns a null sampler (bool false) when no layer can be sampled - the caller then falls back to
|
|
// uniform adaptive subdivision.
|
|
// Which filament the texture stack would put at a point, as a palette index (or -1 for "no colour
|
|
// here"). The colour analogue of HeightFieldSampler, and used the same way: to decide where the
|
|
// adaptive subdivision needs triangles. Colour lands per *facet*, so a colour boundary is a step the
|
|
// mesh can only draw if there are edges along it - the chord-error test that drives the height
|
|
// refinement is blind to it, exactly as it is blind to the paint's own border.
|
|
using ColorFieldSampler = std::function<int(const Vec3f &pos, const Vec3f &normal)>;
|
|
|
|
// The colour counterpart of make_combined_displacement_sampler(), over the same layers, and skipping
|
|
// the same ones (LSCM has no per-point UV). Layers without color_enabled, and layers whose texture is
|
|
// grayscale, contribute nothing; a higher slot wins over a lower one where they overlap, matching the
|
|
// bake. Returns null when no layer can colour anything, in which case there is nothing to refine for.
|
|
ColorFieldSampler make_combined_color_sampler(const indexed_triangle_set &base_mesh,
|
|
const std::vector<TextureDisplacementLayer> &layers,
|
|
const TextureDisplacementFacetsData &facets_data,
|
|
ColorQuantizeFn quantize,
|
|
ColorQuantizeFn quantize_pure = nullptr);
|
|
|
|
HeightFieldSampler make_combined_displacement_sampler(const indexed_triangle_set &base_mesh,
|
|
const std::vector<TextureDisplacementLayer> &layers,
|
|
const TextureDisplacementFacetsData &facets_data);
|
|
|
|
// Uniformly subdivides `mesh` (every triangle recursively split into 4 via edge midpoints, using a
|
|
// shared cache so a midpoint is computed once and reused by both triangles on either side of that
|
|
// edge) until every edge is at or below max_edge_length_mm, or max_iterations passes have run,
|
|
// whichever comes first (bounding the worst-case triangle-count explosion on a very fine target).
|
|
//
|
|
// This exists so a low-poly input model can still get fine-grained texture displacement detail -
|
|
// build_texture_displacement() can only ever move existing vertices, so a patch with only a
|
|
// handful of vertices to begin with cannot show much detail no matter the texture's resolution.
|
|
//
|
|
// Deliberately whole-mesh and uniform, not limited to a painted patch: subdividing only part of a
|
|
// mesh while leaving the rest untouched creates a classic T-junction/cracking problem where the
|
|
// denser and sparser regions meet (the finer side has edge midpoints the coarser side doesn't
|
|
// know about). Uniform, whole-mesh subdivision has no such seam and stays manifold, at the cost of
|
|
// applying everywhere rather than just where texture detail is actually wanted - meant to be run
|
|
// once, deliberately, before painting (see the gizmo's "Subdivide model" button), not automatically
|
|
// during baking.
|
|
indexed_triangle_set subdivide_mesh_uniform(const indexed_triangle_set &mesh, float max_edge_length_mm, int max_iterations = 6);
|
|
|
|
// Adaptive subdivision by Rivara longest-edge bisection, restricted to a region.
|
|
//
|
|
// Unlike subdivide_mesh_uniform() this only densifies where asked - `refine_region` (indexed by
|
|
// input-triangle index; empty or all-false => no-op) flags the triangles allowed to drive refinement
|
|
// - so a small painted patch on a large model does not quadruple the whole model's triangle count.
|
|
// It is nonetheless *conformal*: it never leaves a T-junction/crack at the boundary between the
|
|
// refined and coarse regions (the trap that made subdivide_mesh_uniform() deliberately whole-mesh).
|
|
//
|
|
// A triangle wants refining while it is over at least one of these, whichever applies:
|
|
// - length: its longest edge exceeds `target_edge_length_mm` (a *baseline* - it applies in feature
|
|
// mode too, and is what stops a coarse triangle from being declared flat merely because
|
|
// the four points the chord test samples happened to land at similar heights on a
|
|
// high-frequency texture: the classic aliasing stall);
|
|
// - feature: (only when `sampler` is set and `chord_tolerance_mm > 0`) the *displaced* surface
|
|
// departs from the flat triangle by more than `chord_tolerance_mm`, measured as the max
|
|
// over the three edge midpoints AND the centroid of |sampled displacement - the flat
|
|
// triangle's barycentric interpolation|. Sampling the interior, not just edge midpoints,
|
|
// is what catches a hill that sits inside a triangle. This is a *curvature* test: it is
|
|
// exactly zero on a plane or a linear ramp (barycentric interpolation is exact there, so
|
|
// those stay coarse - the case a gradient criterion would over-refine) and large on a
|
|
// hill/ridge/noise.
|
|
// `min_edge_length_mm` is a hard floor under both: no triangle whose longest edge is already at or
|
|
// below it is ever refined, which is also what guarantees termination across a sharp texture step
|
|
// (where the chord error never falls below the tolerance no matter how fine the mesh gets).
|
|
//
|
|
// Refinement runs to completion, not for a fixed number of passes: triangles are taken worst-first
|
|
// from a max-heap keyed by how many times over its criteria each one is, so a run that hits the
|
|
// `max_triangles` budget has spent it on the largest errors rather than wherever a sweep happened to
|
|
// reach. The budget is the only bound on a pathological height field; stopping on it leaves a
|
|
// perfectly valid, still-conformal mesh.
|
|
//
|
|
// How it stays crack-free: only "terminal" edges are ever bisected - an edge that is the longest edge
|
|
// of *both* triangles sharing it (or a boundary edge that is the longest of its one triangle).
|
|
// Bisecting such an edge splits both its triangles 1->2 around the same new midpoint, so a hanging
|
|
// node is never created. The edge to split for a triangle that wants refining is found by Rivara
|
|
// longest-edge propagation (LEPP): walk to the longest edge of ever-longer-edged neighbours until a
|
|
// terminal edge is reached, and bisect that. Edge length strictly increases along the path (ties
|
|
// broken by a mesh-vertex key, which both sides of an edge compute identically), so the walk cannot
|
|
// cycle, and Rivara's result is that repeating it refines the original triangle in a bounded number
|
|
// of bisections - closing the propagation gap a plain per-edge test leaves behind. The transition
|
|
// triangles this pulls in just outside the region are the graded band that makes the size change
|
|
// conformal; they are a bounded cost paid once, not a per-pass tax.
|
|
//
|
|
// If `out_source` is non-null it is resized to the output triangle count and out_source[i] receives
|
|
// 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.
|
|
//
|
|
// `color`, with a positive `color_edge_length_mm`, adds a third criterion inside the painted area: a
|
|
// triangle whose corners, edge midpoints and centroid do not all map to the *same* filament straddles
|
|
// a colour boundary, and is refined by plain edge length down to that target. Length rather than any
|
|
// error measure, for the same reason the border band uses length - the thing being fixed is the size
|
|
// of the triangles spanning a step, not the curvature of anything - and because a step's error never
|
|
// falls however fine the mesh gets, so only a length target (floored by min_edge_length_mm) is
|
|
// guaranteed to terminate. Without this a colour boundary lands on whatever triangles the *height*
|
|
// happened to need, which on a flat surface is none at all.
|
|
//
|
|
// `progress`, when given, is called with a 0..100 percentage of the triangle budget spent; returning
|
|
// false stops the refinement early. What it hands back then is still a complete, conformal mesh - the
|
|
// loop only ever finishes whole bisections - so a caller that wants to discard it has to do so itself.
|
|
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 border_edge_length_mm = 0.f,
|
|
const DisplacementProgressFn &progress = nullptr,
|
|
const ColorFieldSampler &color = nullptr,
|
|
float color_edge_length_mm = 0.f,
|
|
// Step mode, for a mesh that cut_mesh_at_steps() will cut
|
|
// next: a triangle one of whose edges crosses a *sharp*
|
|
// step of the height field (a jump of 40 % of the relief
|
|
// between two samples at the sample spacing, across the
|
|
// mid-level) gets a chord error of zero, since the step is
|
|
// the cutter's to reproduce and refining it only carpets
|
|
// the edge of the pattern. Features that no edge crosses
|
|
// yet still refine until one does. No effect outside
|
|
// feature mode.
|
|
bool split_multi_crossings = false);
|
|
|
|
// Cuts `mesh` along the height field's mid-level contour wherever the field steps sharply across it,
|
|
// and doubles the seam, so that displacing the result produces a vertical wall at the step instead of
|
|
// a ramp across whichever triangle the step happened to fall in.
|
|
//
|
|
// This is what a binary height map - a grid, stripes, a knurl, wood grain as black-and-white bands -
|
|
// needs, and what refinement alone cannot give it. Refining a triangle that straddles a step never
|
|
// brings the chord error under any tolerance: the surface has a discontinuity, and a finer triangle only
|
|
// makes the ramp narrower. Every refinement level then leaves a band of its own size along every step,
|
|
// and the triangle budget ends up spent on carpeting the edges of the pattern, one to two orders of
|
|
// magnitude more triangles than the pattern needs, while the ramps are still visible.
|
|
//
|
|
// What it does, in order:
|
|
// - Samples the field at the vertices along the bake's normals and takes the mid-level as the contour.
|
|
// A vertex that sits inside a step's blend is nudged along the surface, down its own side's slope,
|
|
// until it samples a pure value, so no vertex bakes to a half height.
|
|
// - Marches every painted edge for crossings of the mid-level (sampled at half `step_width_mm`, then
|
|
// bisected). Both triangles at an edge see the same crossings, which keeps the result conformal. A
|
|
// crossing is *sharp* when the field changes by half its range within `step_width_mm`.
|
|
// - Traces the contour inside each triangle on a local lattice (marching triangles) and keeps the
|
|
// polylines that join one crossing to another; a closed loop inside a triangle is left to
|
|
// refinement. Each polyline is simplified and kept clear of its neighbours.
|
|
// - Doubles the seam: every polyline vertex and every crossing gets a copy on each side, moved
|
|
// `seam_gap_mm` apart across the contour to where each samples a pure value of its own side. The
|
|
// regions between the seams are ear-clipped; the strip between the two copies is triangulated flat
|
|
// and becomes the wall once the high side is displaced. A triangle whose contour could not be traced
|
|
// is split at its crossings without a seam, so the neighbours still meet it without a T-junction.
|
|
//
|
|
// Nothing is cut unless the texture is a step texture: most vertex heights sit at one of two levels,
|
|
// most crossings are sharp, and the features are wider than a couple of step widths (a noisy or a
|
|
// smooth relief is passed through unchanged, as is a mesh with nothing painted). If the cut would leave
|
|
// more than a trace of inverted triangles the input is returned unchanged as well.
|
|
//
|
|
// `region` flags the triangles that may be cut (the painted ones, same encoding as
|
|
// subdivide_mesh_adaptive(), any non-zero value). An edge may carry any number of crossings; two closer
|
|
// than `max(min_feature_mm, seam_gap_mm)` are a feature too thin to carry a seam and are dropped as a
|
|
// pair, leaving the surface flat there. `out_source` receives the input triangle each output triangle
|
|
// descends from, so paint carries over; `out_cut_count` the number of input triangles that were cut
|
|
// (0 means the mesh came back unchanged).
|
|
indexed_triangle_set cut_mesh_at_steps(const indexed_triangle_set &mesh, const std::vector<uint8_t> ®ion,
|
|
const HeightFieldSampler &sampler, float step_width_mm,
|
|
float seam_gap_mm, float min_feature_mm = 0.f,
|
|
std::vector<int> *out_source = nullptr, size_t *out_cut_count = nullptr);
|
|
|
|
// The cutter's verdict alone: whether cut_mesh_at_steps() with the same arguments would cut anything,
|
|
// judged the same way (two levels, sharp crossings, features wider than the step) but without the
|
|
// vertex nudge or any tracing, so it is cheap enough to ask on the coarse mesh. The prepare path asks
|
|
// it *before* refining: a subdivision run in step mode leaves the steps alone for the cutter, which is
|
|
// only right if the cutter is then going to cut them.
|
|
bool texture_has_steps_to_cut(const indexed_triangle_set &mesh, const std::vector<uint8_t> ®ion,
|
|
const HeightFieldSampler &sampler, float step_width_mm, float seam_gap_mm,
|
|
float min_feature_mm = 0.f);
|
|
|
|
// The recipe for getting a mesh ready to receive displacement: even out the triangle density, then
|
|
// refine it where the texture bends. Either stage is skipped when its target is <= 0. Pure data, and
|
|
// the whole of it, so the preparation can be handed to a background job instead of running on the UI
|
|
// thread - see GLGizmoTextureDisplacement::prepare_mesh().
|
|
struct TextureDisplacementPrepareParams
|
|
{
|
|
// Isotropic remesh (CGAL). The target is clamped against the part's own surface area before it is
|
|
// used, so a value that would produce millions of triangles cannot be asked for by accident.
|
|
float remesh_edge_mm = 0.f;
|
|
float remesh_sharp_deg = 0.f; // 0 = do not protect sharp edges
|
|
|
|
// Adaptive (Rivara) subdivision of the painted area. See subdivide_mesh_adaptive().
|
|
float subdiv_target_mm = 0.f; // "Max edge": the length baseline, and the only criterion when
|
|
// subdiv_feature is off
|
|
float subdiv_detail_mm = 0.f; // "Detail": chord tolerance, feature mode only
|
|
float subdiv_min_edge_mm = 0.f; // "Min edge": the floor under both, feature mode only
|
|
float subdiv_border_mm = 0.f; // "Edge detail": the band straddling the paint's edge, 0 = off
|
|
bool subdiv_feature = false; // follow texture curvature, not just edge length
|
|
int subdiv_added_triangles = 0; // budget, *added* to the mesh's own count
|
|
// Edge length triangles straddling a *colour* boundary are refined to, 0 = do not look at colour.
|
|
// Separate from the height criteria because colour lands per facet: a flat surface carrying a
|
|
// sharp colour edge needs triangles along that edge even though its height is perfectly smooth.
|
|
float subdiv_color_edge_mm = 0.f;
|
|
// Cut the mesh along sharp steps in the texture after refining, so they bake as walls rather than
|
|
// ramps. See cut_mesh_at_steps(). Off leaves the old ramp behaviour.
|
|
bool cut_steps = false;
|
|
};
|
|
|
|
// What a preparation run produced. An empty `mesh` means there was nothing to do and the caller must
|
|
// commit nothing - which is not a failure: a mesh that is already even needs no remesh, and one that
|
|
// is already fine enough for the texture needs no subdivision.
|
|
struct TextureDisplacementPrepareResult
|
|
{
|
|
indexed_triangle_set mesh;
|
|
TextureDisplacementFacetsData masks;
|
|
// The remesh landed but no layer's paint survived being carried onto it. Nothing is committed:
|
|
// everything downstream is driven by that paint, so baking on would bake a flat mesh.
|
|
bool paint_lost = false;
|
|
};
|
|
|
|
} // namespace Slic3r
|
|
|
|
#endif // slic3r_TextureDisplacement_hpp_
|