Lagrange
Loading...
Searching...
No Matches
mesh_utils.h
1/*
2 * Copyright 2025 Adobe. All rights reserved.
3 * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4 * you may not use this file except in compliance with the License. You may obtain a copy
5 * of the License at http://www.apache.org/licenses/LICENSE-2.0
6 *
7 * Unless required by applicable law or agreed to in writing, software distributed under
8 * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9 * OF ANY KIND, either express or implied. See the License for the specific language
10 * governing permissions and limitations under the License.
11 */
12#pragma once
13
14#include "Padding.h"
15
16#include <lagrange/Attribute.h>
17#include <lagrange/AttributeTypes.h>
18#include <lagrange/ExactPredicatesShewchuk.h>
19#include <lagrange/IndexedAttribute.h>
20#include <lagrange/Logger.h>
21#include <lagrange/SurfaceMeshTypes.h>
22#include <lagrange/cast_attribute.h>
23#include <lagrange/find_matching_attributes.h>
24#include <lagrange/map_attribute.h>
25#include <lagrange/solver/DirectSolver.h>
26#include <lagrange/triangulate_polygonal_facets.h>
27#include <lagrange/utils/Error.h>
28#include <lagrange/utils/fmt_eigen.h>
29#include <lagrange/views.h>
30#include <lagrange/weld_indexed_attribute.h>
31
32// Include before any TextureSignalProcessing header to override their threadpool implementation.
33#include "ThreadPool.h"
34#define MULTI_THREADING_INCLUDED
35using namespace lagrange::texproc::threadpool;
36
37// clang-format off
38#include <lagrange/utils/warnoff.h>
39#include <Misha/RegularGrid.h>
40#include <Src/PreProcessing.h>
41#include <Src/GradientDomain.h>
42#include <lagrange/utils/warnon.h>
43// clang-format on
44
45#include <Eigen/Sparse>
46
47#include <random>
48
49namespace lagrange::texproc {
50
51namespace {
52
53using namespace MishaK;
54
55// Using `Point` directly leads to ambiguity with Apple Accelerate types.
56template <typename T, unsigned N>
57using Vector = MishaK::Point<T, N>;
58
59// The dimension of the embedding space
60static const unsigned int Dim = 3;
61
62// The dimension of the manifold
63static const unsigned int K = 2;
64
65// The linear solver
66using Solver = lagrange::solver::SolverLDLT<Eigen::SparseMatrix<double>>;
67
68} // namespace
69
70enum class RequiresIndexedTexcoords { Yes, No };
71enum class CheckFlippedUV { Yes, No };
72
73namespace mesh_utils {
74
75template <unsigned int NumChannels, typename ValueType>
76void set_grid(
77 image::experimental::View3D<ValueType> texture,
78 RegularGrid<K, Vector<double, NumChannels>>& grid)
79{
80 unsigned int num_channels = static_cast<unsigned int>(texture.extent(2));
81 if (num_channels != NumChannels) la_debug_assert("Number of channels don't match");
82
83 // Copy the texture data into the texture grid
84 grid.resize(texture.extent(0), texture.extent(1));
85 for (unsigned int j = 0; j < grid.res(1); j++) {
86 for (unsigned int i = 0; i < grid.res(0); i++) {
87 for (unsigned int c = 0; c < NumChannels; c++) {
88 grid(i, j)[c] = texture(i, j, c);
89 }
90 }
91 }
92}
93
94template <unsigned int NumChannels, typename ValueType>
95void set_raw_view(
96 const RegularGrid<K, Vector<double, NumChannels>>& grid,
97 image::experimental::View3D<ValueType> texture)
98{
99 // Copy the texture grid data back into the texture
100 for (unsigned int j = 0; j < grid.res(1); j++) {
101 for (unsigned int i = 0; i < grid.res(0); i++) {
102 for (unsigned int c = 0; c < NumChannels; c++) {
103 texture(i, j, c) = grid(i, j)[c];
104 }
105 }
106 }
107}
108
109template <typename ValueType>
110void set_raw_view(
111 const RegularGrid<K, double>& grid,
112 image::experimental::View3D<ValueType> texture)
113{
114 // Copy the texture grid data back into the texture
115 for (unsigned int j = 0; j < grid.res(1); j++) {
116 for (unsigned int i = 0; i < grid.res(0); i++) {
117 texture(i, j, 0) = grid(i, j);
118 }
119 }
120}
121
122template <unsigned int NumChannels>
123void clamp_out_of_range(
125 const MishaK::TSP::GradientDomain<double>& gd,
126 std::pair<double, double> range = {0.0, 1.0},
127 bool mark_out_of_range = false)
128{
129 size_t num_interior_out_of_range = 0;
130 size_t num_exterior_out_of_range = 0;
131
134 for (unsigned int c = 0; c < NumChannels; c++) {
135 red[c] = (c == 0 ? 1.0 : 0.0);
136 green[c] = (c == 1 ? 1.0 : 0.0);
137 }
138
139 auto is_out_of_range = [&range](Vector<double, NumChannels> p) {
140 constexpr double eps = 1e-6;
141 for (unsigned int c = 0; c < NumChannels; c++) {
142 if (p[c] < range.first - eps || p[c] > range.second + eps) {
143 return true;
144 }
145 }
146 return false;
147 };
148
149 for (size_t n = 0; n < gd.numNodes(); ++n) {
150 const bool is_strictly_out = is_out_of_range(x[n]);
151 for (unsigned int c = 0; c < NumChannels; c++) {
152 x[n][c] = std::clamp(x[n][c], range.first, range.second);
153 }
154 if (is_strictly_out) {
155 if (gd.isCovered(n)) {
156 num_interior_out_of_range++;
157 if (mark_out_of_range) {
158 x[n] = red;
159 }
160 } else {
161 num_exterior_out_of_range++;
162 if (mark_out_of_range) {
163 x[n] = green;
164 }
165 }
166 }
167 }
168 if (num_interior_out_of_range || num_exterior_out_of_range) {
169 logger().info(
170 "{} interior and {} exterior texels were out of range and have been clamped.",
171 num_interior_out_of_range,
172 num_exterior_out_of_range);
173 }
174}
175
176template <typename Scalar, typename Index>
177void check_for_flipped_uv(const SurfaceMesh<Scalar, Index>& mesh, AttributeId id)
178{
179 auto uv_mesh =
180 [&]() -> std::pair<ConstRowMatrixView<Scalar>, std::optional<ConstRowMatrixView<Index>>> {
181 if (mesh.is_attribute_indexed(id)) {
182 const auto& uv_attr = mesh.template get_indexed_attribute<Scalar>(id);
183 auto uv_values = matrix_view(uv_attr.values());
184 auto uv_indices = reshaped_view(uv_attr.indices(), 3);
185 return {uv_values, uv_indices};
186 } else {
187 const auto& uv_attr = mesh.template get_attribute<Scalar>(id);
189 uv_attr.get_element_type() == AttributeElement::Vertex ||
190 uv_attr.get_element_type() == AttributeElement::Corner,
191 "UV attribute must be per-vertex or per-corner.");
192 auto uv_values = matrix_view(uv_attr);
193 return {
194 uv_values,
195 uv_attr.get_element_type() == AttributeElement::Vertex
196 ? std::nullopt
197 : std::optional<ConstRowMatrixView<Index>>(facet_view(mesh))};
198 }
199 }();
200
201 auto uv_index = [&](Index f, unsigned int k) {
202 if (uv_mesh.second.has_value()) {
203 return (*uv_mesh.second)(f, k);
204 } else {
205 return f * 3 + k;
206 }
207 };
208
209 ExactPredicatesShewchuk predicates;
210 for (Index f = 0; f < mesh.get_num_facets(); ++f) {
211 Eigen::RowVector2d p0 = uv_mesh.first.row(uv_index(f, 0)).template cast<double>();
212 Eigen::RowVector2d p1 = uv_mesh.first.row(uv_index(f, 1)).template cast<double>();
213 Eigen::RowVector2d p2 = uv_mesh.first.row(uv_index(f, 2)).template cast<double>();
214 auto r = predicates.orient2D(p0.data(), p1.data(), p2.data());
215 if (r <= 0) {
216 throw Error(
217 fmt::format(
218 "The input mesh has flipped UVs:\n p0=({:.3g})\n p1=({:.3g})\n p2=("
219 "{:.3g})\n"
220 "Please fix the input mesh before proceeding.",
221 fmt::join(p0, ", "),
222 fmt::join(p1, ", "),
223 fmt::join(p2, ", ")));
224 }
225 }
226}
227
228//
229// Jitters texel coordinates to avoid creating rank-deficient systems when a texture vertex falls
230// exactly on a texel center.
231//
232// Consider the case when a (boundary) texture vertex falls at integer location (i,j). The code
233// "activates" all texels supported on that vertex . Depending on how you handle open/closed
234// intervals (and taking into account issues of rounding), in principle you could activate any of
235// the 9 texels in [i-1,i+1]x[j-1,j+1]. But of these 9 only the center one is actually supported on
236// the vertex. If it is also the case that all the adjacent texture vertices are on one side, this
237// could lead to problems.
238//
239// For example, if the vertices are all to the right of i, then the texels {i-1}x[j-1,j+1] will not
240// be supported anywhere on the chart and the associated entries in its mass-matrix row will all be
241// zero. And, unless that DoF is removed, this causes the linear system to be rank deficient,
242// resulting in issues for the numerical factorization.
243//
244// This problem is removed by slightly jittering texture coordinates to move them off the texture
245// lattice edges, so that a given texture vertex can be assumed to always have four well-defined
246// texels supporting it.
247//
248// @note Another alternative is to use a small cutoff distance to avoid activating texels that
249// have almost no support when visiting a seam texture vertex.
250//
251template <typename Scalar>
252void jitter_texture(
253 span<Scalar> texcoords_buffer,
254 unsigned int width,
255 unsigned int height,
256 double epsilon = 1e-4)
257{
258 if (std::abs(epsilon) < std::numeric_limits<double>().denorm_min()) {
259 return;
260 }
261
262 Scalar jitter_scale = static_cast<Scalar>(epsilon / std::max<unsigned int>(width, height));
263 std::mt19937 gen;
264 std::uniform_real_distribution<Scalar> dist(-jitter_scale, jitter_scale);
265 for (auto& x : texcoords_buffer) {
266 x += dist(gen);
267 }
268}
269
270// Add combinatorial stiffness regularization
271template <typename Scalar>
272Eigen::SparseMatrix<Scalar> laplacian_regularization(Eigen::SparseMatrix<Scalar> S, Scalar weight)
273{
274 if (std::abs(weight) > std::numeric_limits<Scalar>().denorm_min()) {
275 la_runtime_assert(S.rows() == S.cols());
276 la_runtime_assert(S.nonZeros() >= S.rows());
277 la_runtime_assert(weight > Scalar(0));
278
279 tbb::parallel_for(size_t(0), size_t(S.outerSize()), [&S, weight](size_t c) {
280 size_t count = 0;
281 for (typename Eigen::SparseMatrix<Scalar>::InnerIterator it(S, c); it; ++it) {
282 if (it.row() != it.col()) {
283 it.valueRef() -= weight;
284 ++count;
285 }
286 }
287 for (typename Eigen::SparseMatrix<Scalar>::InnerIterator it(S, c); it; ++it) {
288 if (it.row() == it.col()) {
289 it.valueRef() += weight * count;
290 }
291 }
292 });
293 }
294
295 return S;
296}
297
298template <typename Scalar, typename Index>
299struct MeshWrapper
300{
301 MeshWrapper(const SurfaceMesh<Scalar, Index>& mesh_)
302 : mesh(mesh_)
303 {}
304
305 size_t num_simplices() const { return static_cast<size_t>(mesh.get_num_facets()); }
306 size_t num_vertices() const { return static_cast<size_t>(mesh.get_num_vertices()); }
307 size_t num_texcoords() const { return texcoords.size() / K; }
308
309 Vector<double, Dim> vertex(size_t i) const
310 {
312 for (unsigned int d = 0; d < Dim; d++) {
313 p[d] = static_cast<double>(vertices[i * Dim + d]);
314 }
315 return p;
316 }
317
318 Vector<double, K> texcoord(size_t i) const
319 {
321 for (unsigned int k = 0; k < K; k++) {
322 q[k] = static_cast<double>(texcoords[i * K + k]);
323 }
324 return q;
325 }
326
327 Vector<double, K> vflipped_texcoord(size_t i) const
328 {
330 for (unsigned int k = 0; k < K; k++) {
331 q[k] = static_cast<double>(texcoords[i * K + k]);
332 }
333 q[1] = 1.0 - q[1];
334 return q;
335 }
336
337 int vertex_index(size_t f, unsigned int k) const
338 {
339 return static_cast<int>(vertex_indices[f * (K + 1) + k]);
340 }
341
342 int texture_index(size_t f, unsigned int k) const
343 {
344 switch (texture_element) {
345 case AttributeElement::Indexed: return static_cast<int>(texture_indices[f * (K + 1) + k]);
346 case AttributeElement::Vertex: return static_cast<int>(vertex_indices[f * (K + 1) + k]);
347 case AttributeElement::Corner: return static_cast<int>(f * (K + 1) + k);
348 default: la_debug_assert("Unsupported texture element type"); return 0;
349 }
350 }
351
352 Simplex<double, K, K> simplex_texcoords(size_t f) const
353 {
354 Simplex<double, K, K> s;
355 for (unsigned int k = 0; k <= K; k++) {
356 s[k] = texcoord(texture_index(f, k));
357 }
358 return s;
359 }
360
361 Simplex<double, K, K> vflipped_simplex_texcoords(size_t f) const
362 {
363 Simplex<double, K, K> s;
364 for (unsigned int k = 0; k <= K; k++) {
365 s[k] = vflipped_texcoord(texture_index(f, k));
366 }
367 return s;
368 }
369
370 Simplex<double, Dim, K> simplex_vertices(size_t f) const
371 {
372 Simplex<double, Dim, K> s;
373 for (unsigned int k = 0; k <= K; k++) {
374 s[k] = vertex(vertex_index(f, k));
375 }
376 return s;
377 }
378
379 SimplexIndex<K> facet_indices(size_t f) const
380 {
381 SimplexIndex<K> simplex;
382 for (unsigned int k = 0; k <= K; ++k) {
383 simplex[k] = static_cast<int>(vertex_indices[f * (K + 1) + k]);
384 }
385 return simplex;
386 }
387
389 span<const Scalar> vertices;
390 span<Scalar> texcoords;
391 span<const Index> vertex_indices;
392 span<const Index> texture_indices;
394};
395
396template <typename Scalar, typename Index>
397MeshWrapper<Scalar, Index> create_mesh_wrapper(
398 const SurfaceMesh<Scalar, Index>& mesh_in,
399 RequiresIndexedTexcoords requires_indexed_texcoords,
400 CheckFlippedUV check_flipped_uv)
401{
402 MeshWrapper wrapper(mesh_in);
403 SurfaceMesh<Scalar, Index>& _mesh = wrapper.mesh;
404
406
407 // Get the texcoord id (and set the texcoords if they weren't already)
408 AttributeId texcoord_id;
409
410 // If the mesh comes with UVs
411 if (auto res = find_matching_attribute(_mesh, AttributeUsage::UV)) {
412 texcoord_id = res.value();
413 } else {
414 la_runtime_assert(false, "Requires uv coordinates.");
415 }
416 // Make sure the UV coordinate type is the same as that of the vertices
417 if (!_mesh.template is_attribute_type<Scalar>(texcoord_id)) {
418 logger().warn(
419 "Input uv coordinates do not have the same scalar type as the input points. Casting "
420 "attribute.");
421 texcoord_id = cast_attribute_in_place<Scalar>(_mesh, texcoord_id);
422 }
423
424 // Make sure the UV coordinates are indexed
425 if (requires_indexed_texcoords == RequiresIndexedTexcoords::Yes &&
427 logger().warn("UV coordinates are not indexed. Welding.");
428 texcoord_id = map_attribute_in_place(_mesh, texcoord_id, AttributeElement::Indexed);
429 weld_indexed_attribute(_mesh, texcoord_id);
430 }
431
432 // Make sure that the number of corners is equal to (K+1) time sthe number of simplices
434 _mesh.get_num_corners() == _mesh.get_num_facets() * (K + 1),
435 "Numer of corners doesn't match the number of simplices");
436
437 if (check_flipped_uv == CheckFlippedUV::Yes) {
438 check_for_flipped_uv(_mesh, texcoord_id);
439 }
440
441 wrapper.vertices = _mesh.get_vertex_to_position().get_all();
442 wrapper.vertex_indices = _mesh.get_corner_to_vertex().get_all();
443 if (_mesh.is_attribute_indexed(texcoord_id)) {
444 auto& uv_attr = _mesh.template ref_indexed_attribute<Scalar>(texcoord_id);
445 wrapper.texcoords = uv_attr.values().ref_all();
446 wrapper.texture_indices = uv_attr.indices().get_all();
447 wrapper.texture_element = AttributeElement::Indexed;
448 } else {
449 auto& uv_attr = _mesh.template ref_attribute<Scalar>(texcoord_id);
450 wrapper.texcoords = uv_attr.ref_all();
451 wrapper.texture_indices = {};
452 wrapper.texture_element = uv_attr.get_element_type();
453 }
454
455 return wrapper;
456}
457
458// Pad input texture to ensure that texture coordinates fall within the rectangle defined by the
459// _centers_ of the corner texels.
460template <typename Scalar, typename Index>
461Padding create_padding(MeshWrapper<Scalar, Index>& wrapper, unsigned int width, unsigned int height)
462{
463 static_assert(sizeof(std::array<Scalar, 2>) == 2 * sizeof(Scalar));
465 reinterpret_cast<std::array<Scalar, 2>*>(wrapper.texcoords.data()),
466 wrapper.num_texcoords());
467 Padding padding;
468 padding = Padding::init<Scalar>(width, height, texcoords);
469 padding.pad(width, height, texcoords);
470 return padding;
471}
472
473} // namespace mesh_utils
474
475} // namespace lagrange::texproc
AttributeElement get_element_type() const
Gets the attribute element type.
Definition Attribute.h:101
lagrange::span< const ValueType > get_all() const
Returns a read-only view of the buffer spanning num elements x num channels.
Definition Attribute.cpp:525
virtual short orient2D(double p1[2], double p2[2], double p3[2]) const
Exact 2D orientation test.
Definition ExactPredicatesShewchuk.cpp:37
A general purpose polygonal mesh class.
Definition SurfaceMesh.h:66
const AttributeBase & get_attribute_base(std::string_view name) const
Gets a read-only reference to the base class of attribute given its name.
Definition SurfaceMesh.cpp:1277
bool is_attribute_indexed(std::string_view name) const
Determines whether the specified attribute is indexed.
Definition SurfaceMesh.cpp:1236
const Attribute< Index > & get_corner_to_vertex() const
Gets a read-only reference to the corner -> vertex id attribute.
Definition SurfaceMesh.cpp:1400
Index get_num_facets() const
Retrieves the number of facets.
Definition SurfaceMesh.h:687
const Attribute< Scalar > & get_vertex_to_position() const
Gets a read-only reference to the vertex -> positions attribute.
Definition SurfaceMesh.cpp:1388
Index get_num_corners() const
Retrieves the number of corners.
Definition SurfaceMesh.h:694
Definition Padding.h:57
LA_CORE_API spdlog::logger & logger()
Retrieves the current logger.
Definition Logger.cpp:40
AttributeId map_attribute_in_place(SurfaceMesh< Scalar, Index > &mesh, AttributeId id, AttributeElement new_element)
Map attribute values to a different element type.
Definition map_attribute.cpp:291
uint32_t AttributeId
Identified to be used to access an attribute.
Definition AttributeFwd.h:73
AttributeElement
Type of element to which the attribute is attached.
Definition AttributeFwd.h:26
@ UV
Mesh attribute must have exactly 2 channels.
Definition AttributeFwd.h:62
@ Scalar
Mesh attribute must have exactly 1 channel.
Definition AttributeFwd.h:56
@ Value
Values that are not attached to a specific element.
Definition AttributeFwd.h:42
@ Indexed
Indexed mesh attributes.
Definition AttributeFwd.h:45
@ Corner
Per-corner mesh attributes.
Definition AttributeFwd.h:37
@ Vertex
Per-vertex mesh attributes.
Definition AttributeFwd.h:28
AttributeId cast_attribute_in_place(SurfaceMesh< Scalar, Index > &mesh, AttributeId attribute_id)
Cast an attribute in place to a different value type.
Definition cast_attribute.cpp:68
std::optional< AttributeId > find_matching_attribute(const SurfaceMesh< Scalar, Index > &mesh, const AttributeMatcher &options)
Finds the first attribute with the specified usage/element type/number of channels.
Definition find_matching_attributes.cpp:37
void triangulate_polygonal_facets(SurfaceMesh< Scalar, Index > &mesh, const TriangulationOptions &options={})
Triangulate polygonal facets of a mesh using a prescribed set of rules.
Definition triangulate_polygonal_facets.cpp:520
SurfaceMesh< ToScalar, ToIndex > cast(const SurfaceMesh< FromScalar, FromIndex > &source_mesh, const AttributeFilter &convertible_attributes={}, std::vector< std::string > *converted_attributes_names=nullptr)
Cast a mesh to a mesh of different scalar and/or index type.
ConstRowMatrixView< ValueType > matrix_view(const Attribute< ValueType > &attribute)
Returns a read-only view of a given attribute in the form of an Eigen matrix.
Definition views.cpp:35
Eigen::Matrix< Scalar, Eigen::Dynamic, 1 > Vector
Type alias for one-dimensional column Eigen vectors.
Definition views.h:79
ConstRowMatrixView< ValueType > reshaped_view(const Attribute< ValueType > &attribute, size_t num_cols)
Returns a read-only view of a given single-channel attribute in the form of an Eigen matrix with a pr...
Definition views.cpp:71
ConstRowMatrixView< Index > facet_view(const SurfaceMesh< Scalar, Index > &mesh)
Returns a read-only view of a mesh facets in the form of an Eigen matrix.
Definition views.cpp:170
const Eigen::Map< const RowMatrix< Scalar >, Eigen::Unaligned > ConstRowMatrixView
Type alias for row-major const matrix view.
Definition views.h:75
#define la_runtime_assert(...)
Runtime assertion check.
Definition assert.h:174
#define la_debug_assert(...)
Debug assertion check.
Definition assert.h:194
internal::Range< Index > range(Index end)
Returns an iterable object representing the range [0, end).
Definition range.h:176
::nonstd::span< T, Extent > span
A bounds-safe view for sequences of objects.
Definition span.h:27
@ Error
Throw an error if collision is detected.
Definition MappingPolicy.h:24