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/IndexedAttribute.h>
19#include <lagrange/Logger.h>
20#include <lagrange/SurfaceMeshTypes.h>
21#include <lagrange/cast_attribute.h>
22#include <lagrange/compute_uv_orientation.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#include <lagrange/utils/fmt/format.h>
44#include <lagrange/utils/fmt/join.h>
45// clang-format on
46
47#include <Eigen/Sparse>
48
49#include <random>
50
51namespace lagrange::texproc {
52
53namespace {
54
55using namespace MishaK;
56
57// Using `Point` directly leads to ambiguity with Apple Accelerate types.
58template <typename T, unsigned N>
59using Vector = MishaK::Point<T, N>;
60
61// The dimension of the embedding space
62static const unsigned int Dim = 3;
63
64// The dimension of the manifold
65static const unsigned int K = 2;
66
67// The linear solver
68using Solver = lagrange::solver::SolverLDLT<Eigen::SparseMatrix<double>>;
69
70} // namespace
71
72enum class RequiresIndexedTexcoords { Yes, No };
73enum class CheckFlippedUV { Yes, No };
74
75namespace mesh_utils {
76
77template <unsigned int NumChannels, typename ValueType>
78void set_grid(
79 image::experimental::View3D<ValueType> texture,
80 RegularGrid<K, Vector<double, NumChannels>>& grid)
81{
82 unsigned int num_channels = static_cast<unsigned int>(texture.extent(2));
83 if (num_channels != NumChannels) la_debug_assert("Number of channels don't match");
84
85 // Copy the texture data into the texture grid
86 grid.resize(texture.extent(0), texture.extent(1));
87 for (unsigned int j = 0; j < grid.res(1); j++) {
88 for (unsigned int i = 0; i < grid.res(0); i++) {
89 for (unsigned int c = 0; c < NumChannels; c++) {
90 grid(i, j)[c] = texture(i, j, c);
91 }
92 }
93 }
94}
95
96template <unsigned int NumChannels, typename ValueType>
97void set_raw_view(
98 const RegularGrid<K, Vector<double, NumChannels>>& grid,
99 image::experimental::View3D<ValueType> texture)
100{
101 // Copy the texture grid data back into the texture
102 for (unsigned int j = 0; j < grid.res(1); j++) {
103 for (unsigned int i = 0; i < grid.res(0); i++) {
104 for (unsigned int c = 0; c < NumChannels; c++) {
105 texture(i, j, c) = grid(i, j)[c];
106 }
107 }
108 }
109}
110
111template <typename ValueType>
112void set_raw_view(
113 const RegularGrid<K, double>& grid,
114 image::experimental::View3D<ValueType> texture)
115{
116 // Copy the texture grid data back into the texture
117 for (unsigned int j = 0; j < grid.res(1); j++) {
118 for (unsigned int i = 0; i < grid.res(0); i++) {
119 texture(i, j, 0) = grid(i, j);
120 }
121 }
122}
123
124template <unsigned int NumChannels>
125void clamp_out_of_range(
127 const MishaK::TSP::GradientDomain<double>& gd,
128 std::pair<double, double> range = {0.0, 1.0},
129 bool mark_out_of_range = false)
130{
131 size_t num_interior_out_of_range = 0;
132 size_t num_exterior_out_of_range = 0;
133
136 for (unsigned int c = 0; c < NumChannels; c++) {
137 red[c] = (c == 0 ? 1.0 : 0.0);
138 green[c] = (c == 1 ? 1.0 : 0.0);
139 }
140
141 auto is_out_of_range = [&range](Vector<double, NumChannels> p) {
142 constexpr double eps = 1e-6;
143 for (unsigned int c = 0; c < NumChannels; c++) {
144 if (p[c] < range.first - eps || p[c] > range.second + eps) {
145 return true;
146 }
147 }
148 return false;
149 };
150
151 for (size_t n = 0; n < gd.numNodes(); ++n) {
152 const bool is_strictly_out = is_out_of_range(x[n]);
153 for (unsigned int c = 0; c < NumChannels; c++) {
154 x[n][c] = std::clamp(x[n][c], range.first, range.second);
155 }
156 if (is_strictly_out) {
157 if (gd.isCovered(n)) {
158 num_interior_out_of_range++;
159 if (mark_out_of_range) {
160 x[n] = red;
161 }
162 } else {
163 num_exterior_out_of_range++;
164 if (mark_out_of_range) {
165 x[n] = green;
166 }
167 }
168 }
169 }
170 if (num_interior_out_of_range || num_exterior_out_of_range) {
171 logger().info(
172 "{} interior and {} exterior texels were out of range and have been clamped.",
173 num_interior_out_of_range,
174 num_exterior_out_of_range);
175 }
176}
177
178//
179// Jitters texel coordinates to avoid creating rank-deficient systems when a texture vertex falls
180// exactly on a texel center.
181//
182// Consider the case when a (boundary) texture vertex falls at integer location (i,j). The code
183// "activates" all texels supported on that vertex . Depending on how you handle open/closed
184// intervals (and taking into account issues of rounding), in principle you could activate any of
185// 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
186// the vertex. If it is also the case that all the adjacent texture vertices are on one side, this
187// could lead to problems.
188//
189// For example, if the vertices are all to the right of i, then the texels {i-1}x[j-1,j+1] will not
190// be supported anywhere on the chart and the associated entries in its mass-matrix row will all be
191// zero. And, unless that DoF is removed, this causes the linear system to be rank deficient,
192// resulting in issues for the numerical factorization.
193//
194// This problem is removed by slightly jittering texture coordinates to move them off the texture
195// lattice edges, so that a given texture vertex can be assumed to always have four well-defined
196// texels supporting it.
197//
198// @note Another alternative is to use a small cutoff distance to avoid activating texels that
199// have almost no support when visiting a seam texture vertex.
200//
201template <typename Scalar>
202void jitter_texture(
203 span<Scalar> texcoords_buffer,
204 unsigned int width,
205 unsigned int height,
206 double epsilon = 1e-4)
207{
208 if (std::abs(epsilon) < std::numeric_limits<double>().denorm_min()) {
209 return;
210 }
211
212 Scalar jitter_scale = static_cast<Scalar>(epsilon / std::max<unsigned int>(width, height));
213 std::mt19937 gen;
214 std::uniform_real_distribution<Scalar> dist(-jitter_scale, jitter_scale);
215 for (auto& x : texcoords_buffer) {
216 x += dist(gen);
217 }
218}
219
220// Add combinatorial stiffness regularization
221template <typename Scalar>
222Eigen::SparseMatrix<Scalar> laplacian_regularization(Eigen::SparseMatrix<Scalar> S, Scalar weight)
223{
224 if (std::abs(weight) > std::numeric_limits<Scalar>().denorm_min()) {
225 la_runtime_assert(S.rows() == S.cols());
226 la_runtime_assert(S.nonZeros() >= S.rows());
227 la_runtime_assert(weight > Scalar(0));
228
229 tbb::parallel_for(size_t(0), size_t(S.outerSize()), [&S, weight](size_t c) {
230 size_t count = 0;
231 for (typename Eigen::SparseMatrix<Scalar>::InnerIterator it(S, c); it; ++it) {
232 if (it.row() != it.col()) {
233 it.valueRef() -= weight;
234 ++count;
235 }
236 }
237 for (typename Eigen::SparseMatrix<Scalar>::InnerIterator it(S, c); it; ++it) {
238 if (it.row() == it.col()) {
239 it.valueRef() += weight * count;
240 }
241 }
242 });
243 }
244
245 return S;
246}
247
248template <typename Scalar, typename Index>
249struct MeshWrapper
250{
251 MeshWrapper(const SurfaceMesh<Scalar, Index>& mesh_)
252 : mesh(mesh_)
253 {}
254
255 size_t num_simplices() const { return static_cast<size_t>(mesh.get_num_facets()); }
256 size_t num_vertices() const { return static_cast<size_t>(mesh.get_num_vertices()); }
257 size_t num_texcoords() const { return texcoords.size() / K; }
258
259 Vector<double, Dim> vertex(size_t i) const
260 {
262 for (unsigned int d = 0; d < Dim; d++) {
263 p[d] = static_cast<double>(vertices[i * Dim + d]);
264 }
265 return p;
266 }
267
268 Vector<double, K> texcoord(size_t i) const
269 {
271 for (unsigned int k = 0; k < K; k++) {
272 q[k] = static_cast<double>(texcoords[i * K + k]);
273 }
274 return q;
275 }
276
277 Vector<double, K> vflipped_texcoord(size_t i) const
278 {
280 for (unsigned int k = 0; k < K; k++) {
281 q[k] = static_cast<double>(texcoords[i * K + k]);
282 }
283 q[1] = 1.0 - q[1];
284 return q;
285 }
286
287 int vertex_index(size_t f, unsigned int k) const
288 {
289 return static_cast<int>(vertex_indices[f * (K + 1) + k]);
290 }
291
292 int texture_index(size_t f, unsigned int k) const
293 {
294 switch (texture_element) {
295 case AttributeElement::Indexed: return static_cast<int>(texture_indices[f * (K + 1) + k]);
296 case AttributeElement::Vertex: return static_cast<int>(vertex_indices[f * (K + 1) + k]);
297 case AttributeElement::Corner: return static_cast<int>(f * (K + 1) + k);
298 default: la_debug_assert("Unsupported texture element type"); return 0;
299 }
300 }
301
302 Simplex<double, K, K> simplex_texcoords(size_t f) const
303 {
304 Simplex<double, K, K> s;
305 for (unsigned int k = 0; k <= K; k++) {
306 s[k] = texcoord(texture_index(f, k));
307 }
308 return s;
309 }
310
311 Simplex<double, K, K> vflipped_simplex_texcoords(size_t f) const
312 {
313 Simplex<double, K, K> s;
314 for (unsigned int k = 0; k <= K; k++) {
315 s[k] = vflipped_texcoord(texture_index(f, k));
316 }
317 return s;
318 }
319
320 Simplex<double, Dim, K> simplex_vertices(size_t f) const
321 {
322 Simplex<double, Dim, K> s;
323 for (unsigned int k = 0; k <= K; k++) {
324 s[k] = vertex(vertex_index(f, k));
325 }
326 return s;
327 }
328
329 SimplexIndex<K> facet_indices(size_t f) const
330 {
331 SimplexIndex<K> simplex;
332 for (unsigned int k = 0; k <= K; ++k) {
333 simplex[k] = static_cast<int>(vertex_indices[f * (K + 1) + k]);
334 }
335 return simplex;
336 }
337
339 span<const Scalar> vertices;
340 span<Scalar> texcoords;
341 span<const Index> vertex_indices;
342 span<const Index> texture_indices;
344};
345
346template <typename Scalar, typename Index>
347MeshWrapper<Scalar, Index> create_mesh_wrapper(
348 const SurfaceMesh<Scalar, Index>& mesh_in,
349 RequiresIndexedTexcoords requires_indexed_texcoords,
350 CheckFlippedUV check_flipped_uv)
351{
352 MeshWrapper wrapper(mesh_in);
353 SurfaceMesh<Scalar, Index>& _mesh = wrapper.mesh;
354
356
357 // Get the texcoord id (and set the texcoords if they weren't already)
358 AttributeId texcoord_id;
359
360 // If the mesh comes with UVs
361 if (auto res = find_matching_attribute(_mesh, AttributeUsage::UV)) {
362 texcoord_id = res.value();
363 } else {
364 la_runtime_assert(false, "Requires uv coordinates.");
365 }
366 // Make sure the UV coordinate type is the same as that of the vertices
367 if (!_mesh.template is_attribute_type<Scalar>(texcoord_id)) {
368 logger().warn(
369 "Input uv coordinates do not have the same scalar type as the input points. Casting "
370 "attribute.");
371 texcoord_id = cast_attribute_in_place<Scalar>(_mesh, texcoord_id);
372 }
373
374 // Make sure the UV coordinates are indexed
375 if (requires_indexed_texcoords == RequiresIndexedTexcoords::Yes &&
377 logger().warn("UV coordinates are not indexed. Welding.");
378 texcoord_id = map_attribute_in_place(_mesh, texcoord_id, AttributeElement::Indexed);
379 weld_indexed_attribute(_mesh, texcoord_id);
380 }
381
382 // Make sure that the number of corners is equal to (K+1) times the number of simplices
384 _mesh.get_num_corners() == _mesh.get_num_facets() * (K + 1),
385 "Number of corners doesn't match the number of simplices");
386
387 if (check_flipped_uv == CheckFlippedUV::Yes) {
388 const std::string uv_name(_mesh.get_attribute_name(texcoord_id));
389 UVOrientationOptions orient_options;
390 orient_options.uv_attribute_name = uv_name;
391 const auto orient_counts = compute_uv_orientation(_mesh, orient_options);
392 if (orient_counts.negative > 0) {
393 throw Error(format(
394 "The input mesh has {} flipped UV triangle(s). Please fix the input mesh "
395 "before proceeding.",
396 orient_counts.negative));
397 }
398 }
399
400 wrapper.vertices = _mesh.get_vertex_to_position().get_all();
401 wrapper.vertex_indices = _mesh.get_corner_to_vertex().get_all();
402 if (_mesh.is_attribute_indexed(texcoord_id)) {
403 auto& uv_attr = _mesh.template ref_indexed_attribute<Scalar>(texcoord_id);
404 wrapper.texcoords = uv_attr.values().ref_all();
405 wrapper.texture_indices = uv_attr.indices().get_all();
406 wrapper.texture_element = AttributeElement::Indexed;
407 } else {
408 auto& uv_attr = _mesh.template ref_attribute<Scalar>(texcoord_id);
409 wrapper.texcoords = uv_attr.ref_all();
410 wrapper.texture_indices = {};
411 wrapper.texture_element = uv_attr.get_element_type();
412 }
413
414 return wrapper;
415}
416
417// Pad input texture to ensure that texture coordinates fall within the rectangle defined by the
418// _centers_ of the corner texels.
419template <typename Scalar, typename Index>
420Padding create_padding(MeshWrapper<Scalar, Index>& wrapper, unsigned int width, unsigned int height)
421{
422 static_assert(sizeof(std::array<Scalar, 2>) == 2 * sizeof(Scalar));
424 reinterpret_cast<std::array<Scalar, 2>*>(wrapper.texcoords.data()),
425 wrapper.num_texcoords());
426 Padding padding;
427 padding = Padding::init<Scalar>(width, height, texcoords);
428 padding.pad(width, height, texcoords);
429 return padding;
430}
431
432} // namespace mesh_utils
433
434} // namespace lagrange::texproc
AttributeElement get_element_type() const
Gets the attribute element type.
Definition Attribute.h:122
lagrange::span< const ValueType > get_all() const
Returns a read-only view of the buffer spanning num elements x num channels.
Definition Attribute.cpp:526
A general purpose polygonal mesh class.
Definition SurfaceMesh.h:73
std::string_view get_attribute_name(AttributeId id) const
Retrieve attribute name from its id.
Definition SurfaceMesh.cpp:358
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:1274
bool is_attribute_indexed(std::string_view name) const
Determines whether the specified attribute is indexed.
Definition SurfaceMesh.cpp:1233
const Attribute< Index > & get_corner_to_vertex() const
Gets a read-only reference to the corner -> vertex id attribute.
Definition SurfaceMesh.cpp:1397
Index get_num_facets() const
Retrieves the number of facets.
Definition SurfaceMesh.h:694
const Attribute< Scalar > & get_vertex_to_position() const
Gets a read-only reference to the vertex -> positions attribute.
Definition SurfaceMesh.cpp:1385
Index get_num_corners() const
Retrieves the number of corners.
Definition SurfaceMesh.h:701
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:292
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:533
UVOrientationCount compute_uv_orientation(SurfaceMesh< Scalar, Index > &mesh, const UVOrientationOptions &options={})
Compute a per-facet orientation attribute using Shewchuk's exact orient2D predicate.
Definition compute_uv_orientation.cpp:96
Eigen::Matrix< Scalar, Eigen::Dynamic, 1 > Vector
Type alias for one-dimensional column Eigen vectors.
Definition views.h:79
#define la_runtime_assert(...)
Runtime assertion check.
Definition assert.h:175
#define la_debug_assert(...)
Debug assertion check.
Definition assert.h:195
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
std::string_view uv_attribute_name
Input UV attribute name.
Definition compute_uv_orientation.h:30