mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-28 03:11:47 +00:00
Add texture projection frame overlay, fix remeshing and subdivision
This commit is contained in:
@@ -29,6 +29,7 @@
|
||||
// For parameterize_lscm()
|
||||
#include <CGAL/Polygon_mesh_processing/border.h>
|
||||
#include <CGAL/Polygon_mesh_processing/connected_components.h>
|
||||
#include <CGAL/Polygon_mesh_processing/detect_features.h>
|
||||
#include <CGAL/Surface_mesh_parameterization/Error_code.h>
|
||||
#include <CGAL/Surface_mesh_parameterization/LSCM_parameterizer_3.h>
|
||||
#include <CGAL/Surface_mesh_parameterization/parameterize.h>
|
||||
@@ -259,7 +260,8 @@ indexed_triangle_set cgal_to_indexed_triangle_set(const CGALMesh &cgalmesh)
|
||||
// Isotropic remeshing
|
||||
// /////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
indexed_triangle_set remesh_isotropic(const indexed_triangle_set &mesh, double target_edge_length, unsigned n_iterations)
|
||||
indexed_triangle_set remesh_isotropic(const indexed_triangle_set &mesh, double target_edge_length,
|
||||
unsigned n_iterations, double sharp_angle_deg)
|
||||
{
|
||||
if (mesh.indices.empty() || target_edge_length <= 0.0)
|
||||
return mesh;
|
||||
@@ -269,14 +271,56 @@ indexed_triangle_set remesh_isotropic(const indexed_triangle_set &mesh, double t
|
||||
if (cgal_mesh.is_empty() || cgal_mesh.number_of_faces() == 0)
|
||||
return mesh;
|
||||
|
||||
// Surface_mesh::add_face() refuses any face that would make the mesh non-manifold and returns a
|
||||
// null descriptor instead. Remeshing a mesh that silently lost faces that way produces holes in
|
||||
// the output, so bail out and let the caller report it rather than hand back a punctured model.
|
||||
if (cgal_mesh.number_of_faces() != mesh.indices.size())
|
||||
return mesh;
|
||||
|
||||
using edge_descriptor = boost::graph_traits<_EpicMesh>::edge_descriptor;
|
||||
try {
|
||||
// Sharp edges and open borders are pinned before remeshing. Without that, the tangential
|
||||
// relaxation pass slides vertices along the surface and rounds every hard feature off - a
|
||||
// cube comes back with wobbly, eroded edges, which is the most visible way "remeshing does
|
||||
// not work properly". protect_constraints() forbids splitting or collapsing them, but it
|
||||
// requires each constrained edge to already be shorter than 4/3 * target, hence the split
|
||||
// first (passing the map so the halves inherit the constraint). This mirrors CGAL's own
|
||||
// isotropic_remeshing example.
|
||||
auto ecm = cgal_mesh.add_property_map<edge_descriptor, bool>("e:is_constrained", false).first;
|
||||
if (sharp_angle_deg > 0.0)
|
||||
CGALProc::detect_sharp_edges(cgal_mesh, sharp_angle_deg, ecm);
|
||||
for (edge_descriptor e : edges(cgal_mesh)) {
|
||||
const auto h = halfedge(e, cgal_mesh);
|
||||
if (is_border(h, cgal_mesh) || is_border(opposite(h, cgal_mesh), cgal_mesh))
|
||||
put(ecm, e, true);
|
||||
}
|
||||
|
||||
std::vector<edge_descriptor> constrained;
|
||||
for (edge_descriptor e : edges(cgal_mesh))
|
||||
if (get(ecm, e))
|
||||
constrained.push_back(e);
|
||||
if (!constrained.empty())
|
||||
CGALProc::split_long_edges(constrained, target_edge_length, cgal_mesh,
|
||||
CGALParams::edge_is_constrained_map(ecm));
|
||||
|
||||
CGALProc::isotropic_remeshing(faces(cgal_mesh), target_edge_length, cgal_mesh,
|
||||
CGALParams::number_of_iterations(n_iterations));
|
||||
CGALParams::number_of_iterations(n_iterations)
|
||||
.edge_is_constrained_map(ecm)
|
||||
.protect_constraints(true));
|
||||
} catch (const std::exception &) {
|
||||
return mesh; // CGAL throws on some non-manifold / degenerate inputs; leave the mesh untouched
|
||||
}
|
||||
if (cgal_mesh.number_of_faces() == 0)
|
||||
return mesh;
|
||||
|
||||
// isotropic_remeshing edits in place, and its edge collapses only *mark* vertices and faces as
|
||||
// removed - the underlying arrays keep the holes until the garbage is collected. That matters
|
||||
// because cgal_to_indexed_triangle_set() numbers its output vertices by iteration order (which
|
||||
// skips removed slots) while reading each face's corner as the raw integer value of the vertex
|
||||
// descriptor (which does not). Past the first collapse the two disagree, so every triangle
|
||||
// points at the wrong vertices, and any descriptor beyond the live vertex count is dropped
|
||||
// together with its triangle. Compacting first makes descriptor == iteration order again.
|
||||
cgal_mesh.collect_garbage();
|
||||
return cgal_to_indexed_triangle_set(cgal_mesh);
|
||||
}
|
||||
|
||||
|
||||
@@ -86,9 +86,11 @@ std::optional<std::vector<Vec2f>> parameterize_lscm(const indexed_triangle_set &
|
||||
// Isotropic remeshing (CGAL): rebuilds the mesh so its triangles are close to a uniform target edge
|
||||
// length, splitting oversized triangles and collapsing undersized ones. Used to even out a model with
|
||||
// wildly varying triangle sizes so texture displacement has a consistent vertex density to work with.
|
||||
// Edges whose dihedral angle exceeds `sharp_angle_deg`, and any open border, are held fixed so hard
|
||||
// features survive instead of being eroded by the relaxation pass; pass 0 to remesh everything.
|
||||
// Returns the input unchanged if remeshing fails (e.g. a non-manifold or self-intersecting input).
|
||||
indexed_triangle_set remesh_isotropic(const indexed_triangle_set &mesh, double target_edge_length,
|
||||
unsigned n_iterations = 3);
|
||||
unsigned n_iterations = 3, double sharp_angle_deg = 40.0);
|
||||
}
|
||||
|
||||
namespace mcut {
|
||||
|
||||
@@ -934,6 +934,19 @@ float blend_displacement(float accumulated, float value, TextureBlendMode mode)
|
||||
}
|
||||
}
|
||||
|
||||
bool project_uv_projective(const std::array<float, 12> &m, const Vec3f &position, Vec2f &uv)
|
||||
{
|
||||
const float x = position.x(), y = position.y(), z = position.z();
|
||||
const float w = m[8] * x + m[9] * y + m[10] * z + m[11];
|
||||
// Strictly greater than zero: at w == 0 the point sits on the projector's plane and maps to
|
||||
// infinity, and at w < 0 it is behind the projector, where dividing yields a plausible-looking
|
||||
// but mirrored uv - the classic way a projected decal reappears on the back of a model.
|
||||
if (!(w > 1e-6f))
|
||||
return false;
|
||||
uv = Vec2f((m[0] * x + m[1] * y + m[2] * z + m[3]) / w, (m[4] * x + m[5] * y + m[6] * z + m[7]) / w);
|
||||
return true;
|
||||
}
|
||||
|
||||
float sample_layer_height(const DecodedHeightTexture &texture, const TextureDisplacementLayer &layer,
|
||||
const Vec3f &position, const Vec3f &normal,
|
||||
const Vec3f &patch_center, const Vec3f &patch_axis, const Vec2f *lscm_uv)
|
||||
@@ -957,6 +970,16 @@ float sample_layer_height(const DecodedHeightTexture &texture, const TextureDisp
|
||||
case TextureProjectionMethod::Spherical:
|
||||
return sample_at(project_spherical(position, patch_center));
|
||||
case TextureProjectionMethod::ViewProjected:
|
||||
if (layer.view_project_projective) {
|
||||
// Exact projective placement written by the projection-frame overlay. Sampled directly,
|
||||
// *without* apply_uv_transform(): the matrix already maps the window's border to the uv
|
||||
// unit square, so the tiling/rotation/offset controls would displace it off the frame
|
||||
// the user just aligned. A point behind the projector has no uv at all -> no height.
|
||||
Vec2f uv;
|
||||
if (!project_uv_projective(layer.view_project_matrix, position, uv))
|
||||
return 0.f;
|
||||
return texture.sample(uv, layer.tile_enabled, layer.tile_method);
|
||||
}
|
||||
// Flat projection onto the captured projector plane. Single-valued per point, so unlike
|
||||
// blended triplanar it is one sample, and it is what "project from view" places.
|
||||
return sample_at(Vec2f(position.dot(layer.view_project_right), position.dot(layer.view_project_up)));
|
||||
@@ -1163,9 +1186,15 @@ indexed_triangle_set build_texture_displacement(const indexed_triangle_set
|
||||
int patch_vertex_count = 0;
|
||||
for (const stl_triangle_vertex_indices &tri : patch.indices)
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
average_normal += vertex_normals[tri[i]];
|
||||
patch_centroid += patch.vertices[tri[i]];
|
||||
const int vi = tri[i];
|
||||
patch_centroid += patch.vertices[size_t(vi)];
|
||||
++patch_vertex_count;
|
||||
// A brush stroke that split a triangle appends new vertices past the base mesh's own
|
||||
// (see the get_facets_strict() note above); vertex_normals is sized to the base mesh,
|
||||
// so those split indices must be skipped here or this reads out of bounds. The main
|
||||
// displacement loop below guards the same way.
|
||||
if (vi < int(vertex_normals.size()))
|
||||
average_normal += vertex_normals[size_t(vi)];
|
||||
}
|
||||
average_normal = (average_normal.norm() > 1e-8f) ? Vec3f(average_normal.normalized()) : Vec3f::UnitZ();
|
||||
patch_centroid = (patch_vertex_count > 0) ? Vec3f(patch_centroid / float(patch_vertex_count)) : Vec3f::Zero();
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#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>
|
||||
|
||||
@@ -218,6 +219,24 @@ struct TextureDisplacementLayer
|
||||
// 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.
|
||||
@@ -255,7 +274,7 @@ struct TextureDisplacementLayer
|
||||
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);
|
||||
lscm_uv_overrides, view_project_projective, view_project_matrix);
|
||||
}
|
||||
template<class Archive> void load(Archive &ar)
|
||||
{
|
||||
@@ -266,7 +285,8 @@ struct TextureDisplacementLayer
|
||||
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);
|
||||
edge_smoothing_amount, auto_connect_islands, island_groups, lscm_uv_overrides, view_project_projective,
|
||||
view_project_matrix);
|
||||
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);
|
||||
@@ -283,8 +303,11 @@ struct DecodedHeightTexture
|
||||
int height = 0;
|
||||
|
||||
bool empty() const { return width <= 0 || height <= 0 || pixels.empty(); }
|
||||
// Bilinearly sampled height in [0, 1] at a normalized uv coordinate. When tile_enabled is
|
||||
// false, uv is clamped to the texture's edge instead of being wrapped/repeated.
|
||||
// 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;
|
||||
};
|
||||
|
||||
@@ -307,6 +330,12 @@ Vec2f project_planar(const Vec3f &position, const Vec3f &normal);
|
||||
// (which only knows how to compute the *analytic* methods from a single vertex + normal).
|
||||
Vec2f apply_uv_transform(const Vec2f &planar, const TextureDisplacementLayer &layer);
|
||||
|
||||
// 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].
|
||||
//
|
||||
|
||||
Reference in New Issue
Block a user