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