DeepSDFStruct 1.7.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (165) hide show
  1. DeepSDFStruct/SDF.py +1734 -0
  2. DeepSDFStruct/__init__.py +66 -0
  3. DeepSDFStruct/deep_sdf/__init__.py +69 -0
  4. DeepSDFStruct/deep_sdf/create_screenshots_from_plyfiles.py +65 -0
  5. DeepSDFStruct/deep_sdf/data.py +258 -0
  6. DeepSDFStruct/deep_sdf/metrics/__init__.py +14 -0
  7. DeepSDFStruct/deep_sdf/metrics/mesh_to_analytical.py +24 -0
  8. DeepSDFStruct/deep_sdf/models.py +197 -0
  9. DeepSDFStruct/deep_sdf/networks/__init__.py +8 -0
  10. DeepSDFStruct/deep_sdf/networks/analytic_round_cross.py +105 -0
  11. DeepSDFStruct/deep_sdf/networks/deep_sdf_decoder.py +155 -0
  12. DeepSDFStruct/deep_sdf/networks/hierarchical_deep_sdf_decoder.py +139 -0
  13. DeepSDFStruct/deep_sdf/networks/hierarchical_positional_sdf_decoder.py +190 -0
  14. DeepSDFStruct/deep_sdf/networks/resnet_positional_sdf_decoder.py +108 -0
  15. DeepSDFStruct/deep_sdf/nn_utils.py +70 -0
  16. DeepSDFStruct/deep_sdf/plotting.py +143 -0
  17. DeepSDFStruct/deep_sdf/reconstruction.py +165 -0
  18. DeepSDFStruct/deep_sdf/training.py +840 -0
  19. DeepSDFStruct/deep_sdf/workspace.py +363 -0
  20. DeepSDFStruct/design_of_experiments.py +235 -0
  21. DeepSDFStruct/flexicubes/__init__.py +15 -0
  22. DeepSDFStruct/flexicubes/flexicubes.py +1005 -0
  23. DeepSDFStruct/flexicubes/tables.py +2338 -0
  24. DeepSDFStruct/flexisquares/__init__.py +15 -0
  25. DeepSDFStruct/flexisquares/flexisquares.py +967 -0
  26. DeepSDFStruct/flexisquares/tables.py +63 -0
  27. DeepSDFStruct/lattice_structure.py +254 -0
  28. DeepSDFStruct/local_shapes.py +226 -0
  29. DeepSDFStruct/mesh.py +1184 -0
  30. DeepSDFStruct/optimization.py +357 -0
  31. DeepSDFStruct/parametrization.py +215 -0
  32. DeepSDFStruct/pretrained_models.py +128 -0
  33. DeepSDFStruct/sampling.py +670 -0
  34. DeepSDFStruct/sdf_operations.py +990 -0
  35. DeepSDFStruct/sdf_primitives.py +1416 -0
  36. DeepSDFStruct/splinepy_unitcells/__init__.py +35 -0
  37. DeepSDFStruct/splinepy_unitcells/chi_3D.py +1617 -0
  38. DeepSDFStruct/splinepy_unitcells/cross_lattice.py +165 -0
  39. DeepSDFStruct/splinepy_unitcells/double_lattice_extruded.py +298 -0
  40. DeepSDFStruct/splinepy_unitcells/snappy_3d.py +544 -0
  41. DeepSDFStruct/torch_spline.py +446 -0
  42. DeepSDFStruct/trained_models/analytic_round_cross/LatentCodes/latest.pth +0 -0
  43. DeepSDFStruct/trained_models/analytic_round_cross/ModelParameters/latest.pth +0 -0
  44. DeepSDFStruct/trained_models/analytic_round_cross/OptimizerParameters/latest.pth +0 -0
  45. DeepSDFStruct/trained_models/analytic_round_cross/specs.json +45 -0
  46. DeepSDFStruct/trained_models/chi_and_cross/LatentCodes/latest.pth +0 -0
  47. DeepSDFStruct/trained_models/chi_and_cross/Logs.pth +0 -0
  48. DeepSDFStruct/trained_models/chi_and_cross/ModelParameters/latest.pth +0 -0
  49. DeepSDFStruct/trained_models/chi_and_cross/OptimizerParameters/latest.pth +0 -0
  50. DeepSDFStruct/trained_models/chi_and_cross/specs.json +76 -0
  51. DeepSDFStruct/trained_models/primitives/LatentCodes/latest.pth +0 -0
  52. DeepSDFStruct/trained_models/primitives/ModelParameters/latest.pth +0 -0
  53. DeepSDFStruct/trained_models/primitives/OptimizerParameters/latest.pth +0 -0
  54. DeepSDFStruct/trained_models/primitives/specs.json +46 -0
  55. DeepSDFStruct/trained_models/primitives_2d/LatentCodes/latest.pth +0 -0
  56. DeepSDFStruct/trained_models/primitives_2d/ModelParameters/latest.pth +0 -0
  57. DeepSDFStruct/trained_models/primitives_2d/OptimizerParameters/latest.pth +0 -0
  58. DeepSDFStruct/trained_models/primitives_2d/specs.json +46 -0
  59. DeepSDFStruct/trained_models/round_cross/LatentCodes/latest.pth +0 -0
  60. DeepSDFStruct/trained_models/round_cross/Logs.pth +0 -0
  61. DeepSDFStruct/trained_models/round_cross/ModelParameters/latest.pth +0 -0
  62. DeepSDFStruct/trained_models/round_cross/OptimizerParameters/latest.pth +0 -0
  63. DeepSDFStruct/trained_models/round_cross/specs.json +45 -0
  64. DeepSDFStruct/trained_models/test_experiment/LatentCodes/latent_code_data_map.json +106 -0
  65. DeepSDFStruct/trained_models/test_experiment/specs.json +45 -0
  66. DeepSDFStruct/trained_models/test_experiment/training_summary.json +10 -0
  67. DeepSDFStruct/trained_models/test_experiment_hierarchical/LatentCodes/latent_code_data_map.json +106 -0
  68. DeepSDFStruct/trained_models/test_experiment_hierarchical/specs.json +45 -0
  69. DeepSDFStruct/trained_models/test_experiment_hierarchical/training_summary.json +10 -0
  70. DeepSDFStruct/trained_models/test_experiment_homogenization/LatentCodes/latent_code_data_map.json +5006 -0
  71. DeepSDFStruct/trained_models/test_experiment_homogenization/LatentCodes/latest.pth +0 -0
  72. DeepSDFStruct/trained_models/test_experiment_homogenization/Logs.png +0 -0
  73. DeepSDFStruct/trained_models/test_experiment_homogenization/ModelParameters/latest.pth +0 -0
  74. DeepSDFStruct/trained_models/test_experiment_homogenization/OptimizerParameters/latest.pth +0 -0
  75. DeepSDFStruct/trained_models/test_experiment_homogenization/specs.json +83 -0
  76. DeepSDFStruct/trained_models/test_experiment_homogenization/training_summary.json +10 -0
  77. DeepSDFStruct/utils.py +96 -0
  78. DeepSDFStruct/visualization/latent_viewer.py +211 -0
  79. DeepSDFStruct/visualization/latent_viewer_cli.py +8 -0
  80. benchmarks/generate_sdf_showcase.py +622 -0
  81. benchmarks/sdf_from_mesh_benchmark.py +52 -0
  82. benchmarks/sdf_from_mesh_comparison.md +33 -0
  83. benchmarks/sdf_showcase/operations/boolean/difference_sphere_box.png +0 -0
  84. benchmarks/sdf_showcase/operations/boolean/union_sphere_box.png +0 -0
  85. benchmarks/sdf_showcase/operations/transformations/circular_array_sphere.png +0 -0
  86. benchmarks/sdf_showcase/operations/transformations/dilate_sphere.png +0 -0
  87. benchmarks/sdf_showcase/operations/transformations/mirror_sphere.png +0 -0
  88. benchmarks/sdf_showcase/operations/transformations/repeat_sphere.png +0 -0
  89. benchmarks/sdf_showcase/operations/transformations/revolve_circle.png +0 -0
  90. benchmarks/sdf_showcase/operations/transformations/shell_sphere.png +0 -0
  91. benchmarks/sdf_showcase/operations/transformations/twist_torus.png +0 -0
  92. benchmarks/sdf_showcase/primitives/2D/circle.png +0 -0
  93. benchmarks/sdf_showcase/primitives/2D/equilateral_triangle.png +0 -0
  94. benchmarks/sdf_showcase/primitives/2D/hexagon.png +0 -0
  95. benchmarks/sdf_showcase/primitives/2D/polygon_pentagon.png +0 -0
  96. benchmarks/sdf_showcase/primitives/2D/rectangle.png +0 -0
  97. benchmarks/sdf_showcase/primitives/2D/rounded_rectangle.png +0 -0
  98. benchmarks/sdf_showcase/primitives/3D/box.png +0 -0
  99. benchmarks/sdf_showcase/primitives/3D/capped_cone.png +0 -0
  100. benchmarks/sdf_showcase/primitives/3D/capped_cylinder.png +0 -0
  101. benchmarks/sdf_showcase/primitives/3D/capsule.png +0 -0
  102. benchmarks/sdf_showcase/primitives/3D/cone.png +0 -0
  103. benchmarks/sdf_showcase/primitives/3D/corner_spheres.png +0 -0
  104. benchmarks/sdf_showcase/primitives/3D/cross_ms.png +0 -0
  105. benchmarks/sdf_showcase/primitives/3D/cylinder.png +0 -0
  106. benchmarks/sdf_showcase/primitives/3D/dodecahedron.png +0 -0
  107. benchmarks/sdf_showcase/primitives/3D/ellipsoid.png +0 -0
  108. benchmarks/sdf_showcase/primitives/3D/icosahedron.png +0 -0
  109. benchmarks/sdf_showcase/primitives/3D/octahedron.png +0 -0
  110. benchmarks/sdf_showcase/primitives/3D/pyramid.png +0 -0
  111. benchmarks/sdf_showcase/primitives/3D/rounded_box.png +0 -0
  112. benchmarks/sdf_showcase/primitives/3D/rounded_cone.png +0 -0
  113. benchmarks/sdf_showcase/primitives/3D/rounded_cylinder.png +0 -0
  114. benchmarks/sdf_showcase/primitives/3D/sphere.png +0 -0
  115. benchmarks/sdf_showcase/primitives/3D/tetrahedron.png +0 -0
  116. benchmarks/sdf_showcase/primitives/3D/torus.png +0 -0
  117. benchmarks/sdf_showcase/primitives/3D/wireframe_box.png +0 -0
  118. deepsdfstruct-1.7.2.dist-info/METADATA +44 -0
  119. deepsdfstruct-1.7.2.dist-info/RECORD +165 -0
  120. deepsdfstruct-1.7.2.dist-info/WHEEL +5 -0
  121. deepsdfstruct-1.7.2.dist-info/licenses/LICENSE +208 -0
  122. deepsdfstruct-1.7.2.dist-info/licenses/NOTICE +67 -0
  123. deepsdfstruct-1.7.2.dist-info/scm_file_list.json +178 -0
  124. deepsdfstruct-1.7.2.dist-info/scm_version.json +8 -0
  125. deepsdfstruct-1.7.2.dist-info/top_level.txt +4 -0
  126. docs/Makefile +20 -0
  127. docs/api_reference.rst +6 -0
  128. docs/conf.py +60 -0
  129. docs/index.rst +136 -0
  130. docs/make.bat +35 -0
  131. docs/qr_code_github.png +0 -0
  132. docs/qr_code_paper.png +0 -0
  133. docs/readme_images/example_output_01.png +0 -0
  134. docs/readme_images/example_output_02.png +0 -0
  135. docs/readme_images/example_output_03.png +0 -0
  136. docs/readme_images/example_output_04.png +0 -0
  137. tests/data/chairs/1005.obj +282 -0
  138. tests/data/chairs/1024.obj +222 -0
  139. tests/data/chairs/README.md +1 -0
  140. tests/data/circular_balls_test_case.stl +0 -0
  141. tests/data/cone.stl +1794 -0
  142. tests/data/example_disconnectd_mesh.inp +1784 -0
  143. tests/data/example_line_mesh.vtk +0 -0
  144. tests/data/stanford_bunny.stl +0 -0
  145. tests/data/sweep_test_case.stl +0 -0
  146. tests/test_DOE.py +49 -0
  147. tests/test_elongate_sdf.py +195 -0
  148. tests/test_flexisquares.py +187 -0
  149. tests/test_generate_dataset.py +76 -0
  150. tests/test_lattice_evaluation.py +137 -0
  151. tests/test_mesh_export.py +137 -0
  152. tests/test_mesh_functions.py +49 -0
  153. tests/test_networks.py +71 -0
  154. tests/test_pretrained_models.py +40 -0
  155. tests/test_reconstruction.py +101 -0
  156. tests/test_sdf_from_mesh.py +52 -0
  157. tests/test_sdf_functions.py +117 -0
  158. tests/test_sdf_primitives.py +668 -0
  159. tests/test_sdf_trimesh_comparison.py +482 -0
  160. tests/test_splinepy_unitcells.py +64 -0
  161. tests/test_structural_optimization.py +178 -0
  162. tests/test_torch_spline.py +216 -0
  163. tests/test_train_model.py +110 -0
  164. tests/test_training_data_ids.py +41 -0
  165. tests/tmp_outputs/temp_file.txt +0 -0
@@ -0,0 +1,967 @@
1
+ """
2
+ FlexiSquares Implementation
3
+ ===========================
4
+
5
+ This module contains the core implementation of the FlexiSquares algorithm,
6
+ a differentiable 2D mesh extraction method adapted from FlexiCubes.
7
+
8
+ FlexiSquares improves upon traditional Marching Squares by allowing gradient-
9
+ based optimization of the extracted contours, making it suitable for inverse
10
+ design and shape optimization problems.
11
+
12
+ Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES.
13
+ Licensed under the Apache License, Version 2.0.
14
+ """
15
+
16
+ # Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES.
17
+ # All rights reserved.
18
+ #
19
+ # Licensed under the Apache License, Version 2.0 (the "License");
20
+ # you may not use this file except in compliance with the License.
21
+ # You may obtain a copy of the License at
22
+ #
23
+ # http://www.apache.org/licenses/LICENSE-2.0
24
+ #
25
+ # Unless required by applicable law or agreed to in writing, software
26
+ # distributed under the License is distributed on an "AS IS" BASIS,
27
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
28
+ # See the License for the specific language governing permissions and
29
+ # limitations under the License.
30
+ import torch
31
+ import logging
32
+
33
+
34
+ import DeepSDFStruct
35
+ from DeepSDFStruct.flexisquares.tables import num_vd_table, dmc_table, tet_table
36
+
37
+ logger = logging.getLogger(DeepSDFStruct.__name__)
38
+
39
+ __all__ = ["FlexiSquares"]
40
+
41
+
42
+ class FlexiSquares:
43
+ """
44
+ Implements the 2D variant of the flexicubes method, adapted as
45
+ flexisquares, for extracting contour meshes from scalar fields.
46
+ This class uses lookup tables and indexed operations to perform
47
+ differentiable isocontour extraction from signed distance fields
48
+ defined on 2D grids.
49
+
50
+ Flexisquares is a differentiable variant of the Dual Marching
51
+ Squares (DMS) algorithm. It improves geometric fidelity by
52
+ optimizing surface representations using gradient-based methods.
53
+
54
+ During initialization, the class loads precomputed lookup tables
55
+ and converts them into PyTorch tensors on the specified device.
56
+
57
+ Attributes:
58
+ device (str): Computational device, usually "cuda" or "cpu".
59
+ dmc_table (torch.Tensor): Dual Marching Squares (DMS) table
60
+ encoding the edges associated with each dual vertex in 16
61
+ possible Marching Squares configurations.
62
+ num_vd_table (torch.Tensor): Table holding the number of dual
63
+ vertices for each configuration.
64
+ tet_table (torch.Tensor): Lookup table used during triangle
65
+ generation inside the contour.
66
+ cube_corners (torch.Tensor): Positions of the four corners of
67
+ a unit square in 2D space.
68
+ cube_corners_idx (torch.Tensor): Corner indices encoded as
69
+ binary powers to compute case IDs for Marching Squares.
70
+ cube_edges (torch.Tensor): Edge connections in a square,
71
+ listed in pairs of corner indices.
72
+ """
73
+
74
+ def __init__(self, device="cuda", qef_reg_scale=1e-3, weight_scale=0.99):
75
+
76
+ self.device = device
77
+ self.dmc_table = torch.tensor(
78
+ dmc_table, dtype=torch.long, device=device, requires_grad=False
79
+ )
80
+ self.num_vd_table = torch.tensor(
81
+ num_vd_table, dtype=torch.long, device=device, requires_grad=False
82
+ )
83
+
84
+ self.tet_table = torch.tensor(
85
+ tet_table, dtype=torch.long, device=device, requires_grad=False
86
+ )
87
+ self.quad_split_1 = torch.tensor(
88
+ [0, 1, 2, 0, 2, 3], dtype=torch.long, device=device, requires_grad=False
89
+ )
90
+ self.quad_split_2 = torch.tensor(
91
+ [0, 1, 3, 3, 1, 2], dtype=torch.long, device=device, requires_grad=False
92
+ )
93
+ self.quad_split_train = torch.tensor(
94
+ [0, 1, 1, 2, 2, 3, 3, 0],
95
+ dtype=torch.long,
96
+ device=device,
97
+ requires_grad=False,
98
+ )
99
+
100
+ self.cube_corners = torch.tensor(
101
+ [[0, 0], [1, 0], [0, 1], [1, 1]], device=device
102
+ )
103
+ # Each corner corresponds to a binary bit (for the 2^4 possible inside/outside cases):
104
+ self.cube_corners_idx = torch.pow(2, torch.arange(4, requires_grad=False))
105
+
106
+ # (0,1) ─ 2 ─ (1,1)
107
+ # | |
108
+ # 0 1
109
+ # | |
110
+ # (0,0) ─ 3 ─ (1,0)
111
+
112
+ # (2) ─ 2 ─ (3)
113
+ # | |
114
+ # 0 1
115
+ # | |
116
+ # (0) ─ 3 ─ (1)
117
+
118
+ # Edges are:
119
+ # e0: bottom → top along left side → (0,0)-(0,1) → corners (0,2)
120
+ # e1: bottom → top along right side → (1,0)-(1,1) → corners (1,3)
121
+ # e2: left → right along bottom → (0,0)-(1,0) → corners (0,1)
122
+ # e3: left → right along top → (0,1)-(1,1) → corners (2,3)
123
+
124
+ self.cube_edges = torch.tensor(
125
+ [0, 2, 1, 3, 2, 3, 0, 1], dtype=torch.long, device=device
126
+ )
127
+ # should be equivalently, reshaped as pairs:
128
+ # self.cube_edges = torch.tensor(
129
+ # [[0, 2], [1, 3], [0, 1], [2, 3]], dtype=torch.long, device=device
130
+ # )
131
+
132
+ self.edge_dir_table = torch.tensor(
133
+ [1, 1, 0, 0], dtype=torch.long, device=device
134
+ )
135
+
136
+ self.dir_faces_table = torch.tensor(
137
+ [[[0, 1], [2, 3]], [[0, 2], [1, 3]]], # x-dir edges # y-dir edges
138
+ dtype=torch.long,
139
+ device=device,
140
+ )
141
+ # not sure which version is correct
142
+ self.adj_pairs_2233 = torch.tensor(
143
+ [0, 1, 1, 2, 2, 3, 3, 0], dtype=torch.long, device=device
144
+ )
145
+ self.adj_pairs_3322 = torch.tensor(
146
+ [0, 1, 1, 3, 3, 2, 2, 0], dtype=torch.long, device=device
147
+ )
148
+ self.qef_reg_scale = qef_reg_scale
149
+ self.weight_scale = weight_scale
150
+
151
+ def construct_voxel_grid(
152
+ self, resolution, bounds=None
153
+ ) -> tuple[torch.tensor, torch.tensor]:
154
+ """
155
+ Generates a 2D grid of vertices and their square indices given
156
+ the specified resolution.
157
+
158
+ Args:
159
+ resolution (int or list[int]): Resolution of the 2D grid.
160
+ If an integer is provided, it is applied to both x and
161
+ y dimensions. If a tuple/list of two integers is given,
162
+ they specify (x_res, y_res).
163
+ bounds (torch.Tensor, optional): 2×2 tensor defining the
164
+ [min, max] bounds in x and y.
165
+ Defaults to [[-0.05, -0.05], [1.05, 1.05]].
166
+
167
+ Returns:
168
+ (torch.Tensor, torch.Tensor): Tuple containing:
169
+ - Vertices (N×2): Scaled vertex coordinates of the
170
+ 2D grid.
171
+ - Squares (F×4): Indices into `vertices` defining each
172
+ square's corner connectivity.
173
+ """
174
+ res = resolution
175
+ base_cube_f = torch.arange(4).to(self.device)
176
+ if isinstance(res, int):
177
+ res = (res, res)
178
+ voxel_grid_template = torch.ones(res, device=self.device)
179
+
180
+ res = torch.tensor([res], device=self.device)
181
+ coords = (
182
+ torch.nonzero(voxel_grid_template).to(torch.get_default_dtype()) / res
183
+ ) # N, 2
184
+ verts = (self.cube_corners.unsqueeze(0) / res + coords.unsqueeze(1)).reshape(
185
+ -1, 2
186
+ )
187
+ cubes = (
188
+ base_cube_f.unsqueeze(0)
189
+ + torch.arange(coords.shape[0], device=self.device).unsqueeze(1) * 4
190
+ ).reshape(-1)
191
+
192
+ verts_rounded = torch.round(verts * 10**5) / (10**5)
193
+ verts_unique, inverse_indices = torch.unique(
194
+ verts_rounded, dim=0, return_inverse=True
195
+ )
196
+ cubes = inverse_indices[cubes.reshape(-1)].reshape(-1, 4)
197
+
198
+ if bounds is None:
199
+ bounds = torch.tensor(
200
+ [[-0.05, -0.05], [1.05, 1.05]],
201
+ device=verts_unique.device,
202
+ dtype=verts_unique.dtype,
203
+ )
204
+ else:
205
+ bounds = torch.as_tensor(
206
+ bounds, device=verts_unique.device, dtype=verts_unique.dtype
207
+ )
208
+ assert bounds.shape == (2, 2), "bounds must have shape [2, 2]"
209
+
210
+ # Scale samples from [0, 1] to the given bounds
211
+ # samples = samples * 1.1 + _torch.tensor([0.5, 0.5, 0.5], device=device)
212
+ samples = verts_unique - 0.5
213
+ verts_scaled = (
214
+ bounds[0]
215
+ + 0.5 * (bounds[1] - bounds[0])
216
+ + (bounds[1] - bounds[0]) * samples
217
+ )
218
+
219
+ tolerance = 1e-6
220
+ torch._assert(
221
+ torch.all(
222
+ verts_scaled.ge(bounds[0] - tolerance)
223
+ & verts_scaled.le(bounds[1] + tolerance)
224
+ ),
225
+ "Samples are out of specified bounds",
226
+ )
227
+ return verts_scaled, cubes
228
+
229
+ # changes mkofler: adapted function signature to match the kaolin package
230
+ # function signature on kaolin package:
231
+ # verts, faces, loss = DeepSDFStruct.flexicubes_reconstructor(voxelgrid_vertices=samples_orig[:, :3],
232
+ # scalar_field=sdf_values,
233
+ # cube_idx=cube_idx,
234
+ # resolution=tuple(N),
235
+ # output_tetmesh=output_tetmesh)
236
+ def __call__(
237
+ self,
238
+ voxelgrid_vertices,
239
+ scalar_field,
240
+ cube_idx,
241
+ beta_fx4=None,
242
+ alpha_fx4=None,
243
+ output_tetmesh=False,
244
+ resolution=None,
245
+ n_smoothing_iterations=5,
246
+ ):
247
+ r"""
248
+ Extracts a differentiable 2D contour mesh from a scalar field using DeepSDFStruct.flexisquares.
249
+
250
+ Converts discrete signed distance fields on a 2D grid into polygonal contours using a
251
+ differentiable Dual Marching Squares process, as described in
252
+ *Flexible Isosurface Extraction for Gradient-Based Mesh Optimization* (adapted to 2D).
253
+
254
+ Args:
255
+ voxelgrid_vertices (torch.Tensor): 2D coordinates of grid vertices (N×2).
256
+ scalar_field (torch.Tensor): Signed distance field values at each vertex.
257
+ Negative values are inside the contour.
258
+ cube_idx (torch.LongTensor): Indices of 4 vertices per grid cell (F×4).
259
+ resolution (int or tuple[int, int]): Resolution of the grid.
260
+ beta_fx4 (torch.Tensor, optional): Edge weights adjusting dual vertex positioning.
261
+ alpha_fx4 (torch.Tensor, optional): Corner weights adjusting interpolation.
262
+ output_tetmesh (bool, optional): Retained for compatibility; in 2D, returns triangles only.
263
+
264
+
265
+ Returns:
266
+ (torch.Tensor, torch.LongTensor, torch.Tensor): Tuple of:
267
+ - Vertices (V×2): Coordinates of contour vertices.
268
+ - Faces (F×3): Triangle indices of the contour mesh.
269
+ - Regularization loss (L_dev): Deviation loss per dual vertex.
270
+
271
+ .. _Flexible Isosurface Extraction for Gradient-Based Mesh Optimization:
272
+ https://research.nvidia.com/labs/toronto-ai/DeepSDFStruct.flexicubes/
273
+ .. _Manifold Dual Contouring:
274
+ https://people.engr.tamu.edu/schaefer/research/dualsimp_tvcg.pdf
275
+ """
276
+ x_nx2 = voxelgrid_vertices
277
+ s_n = scalar_field
278
+ cube_fx4 = cube_idx
279
+ surf_cubes, occ_fx4 = self._identify_surf_cubes(s_n, cube_fx4)
280
+ if surf_cubes.sum() == 0:
281
+ return (
282
+ torch.zeros((0, 3), device=self.device),
283
+ (
284
+ torch.zeros((0, 4), dtype=torch.long, device=self.device)
285
+ if output_tetmesh
286
+ else torch.zeros((0, 3), dtype=torch.long, device=self.device)
287
+ ),
288
+ torch.zeros((0), device=self.device),
289
+ )
290
+ beta_fx4, alpha_fx4 = self._normalize_weights(beta_fx4, alpha_fx4, surf_cubes)
291
+
292
+ case_ids = self._get_case_id(occ_fx4, surf_cubes)
293
+
294
+ surf_edges, idx_map, edge_counts, surf_edges_mask = self._identify_surf_edges(
295
+ s_n, cube_fx4, surf_cubes
296
+ )
297
+
298
+ vd, L_dev, vd_idx_map = self._compute_vd(
299
+ x_nx2, surf_edges, s_n, case_ids, beta_fx4, alpha_fx4, idx_map
300
+ )
301
+ boundary_vertices, boundary_lines, s_edges, edge_indices = (
302
+ self._extract_zero_contour(
303
+ s_n, surf_edges, vd, edge_counts, idx_map, vd_idx_map, surf_edges_mask
304
+ )
305
+ )
306
+ if not output_tetmesh:
307
+ return boundary_vertices, boundary_lines, L_dev
308
+ else:
309
+ self.check_open_mesh(boundary_lines)
310
+ all_vertices, triangles = self._triangulate(
311
+ x_nx2,
312
+ s_n,
313
+ cube_fx4,
314
+ boundary_vertices,
315
+ boundary_lines,
316
+ surf_edges,
317
+ s_edges,
318
+ case_ids,
319
+ edge_indices,
320
+ surf_cubes,
321
+ )
322
+ vertices_smoothed = self._apply_laplacian_smoothing(
323
+ all_vertices,
324
+ triangles,
325
+ boundary_lines,
326
+ iterations=n_smoothing_iterations,
327
+ )
328
+ return vertices_smoothed, triangles, L_dev
329
+
330
+ def _apply_laplacian_smoothing(
331
+ self, vertices, triangles, boundary_vertices, iterations=5, lamb=0.5
332
+ ):
333
+ """
334
+ Vectorized Laplacian smoothing using PyTorch sparse matrices.
335
+ """
336
+ logger.info(f"applying {iterations} iterations of laplacian smoothing")
337
+ num_total = vertices.shape[0]
338
+ device = vertices.device
339
+
340
+ unique_boundary = torch.unique(boundary_vertices.long())
341
+ is_interior = torch.ones(num_total, 1, device=device, dtype=torch.bool)
342
+ is_interior[unique_boundary] = False
343
+
344
+ i, j, k = triangles[:, 0], triangles[:, 1], triangles[:, 2]
345
+
346
+ edges = torch.cat(
347
+ [
348
+ torch.stack([i, j], dim=1),
349
+ torch.stack([j, i], dim=1),
350
+ torch.stack([j, k], dim=1),
351
+ torch.stack([k, j], dim=1),
352
+ torch.stack([k, i], dim=1),
353
+ torch.stack([i, k], dim=1),
354
+ ],
355
+ dim=0,
356
+ ) # shape: (num_edges, 2)
357
+
358
+ # Remove duplicate edges
359
+ edges = torch.unique(edges, dim=0)
360
+
361
+ src = edges[:, 0] # neighbor index
362
+ dst = edges[:, 1] # vertex to accumulate into
363
+
364
+ degree = torch.zeros(num_total, device=device, dtype=vertices.dtype)
365
+ one = torch.ones_like(dst, dtype=vertices.dtype)
366
+ degree.scatter_add_(0, dst, one)
367
+ degree_inv = 1.0 / torch.clamp(degree, min=1.0)
368
+
369
+ v_smoothed = vertices.clone()
370
+
371
+ for _ in range(iterations):
372
+ neighbor_sum = torch.zeros_like(v_smoothed) # (N, 3)
373
+
374
+ neighbor_sum.scatter_add_(
375
+ 0, dst.unsqueeze(1).expand(-1, v_smoothed.shape[1]), v_smoothed[src]
376
+ )
377
+
378
+ neighbor_avg = neighbor_sum * degree_inv.unsqueeze(1)
379
+
380
+ v_update = (1 - lamb) * v_smoothed + lamb * neighbor_avg
381
+ v_smoothed = torch.where(is_interior, v_update, v_smoothed)
382
+
383
+ return v_smoothed
384
+
385
+ def _compute_reg_loss(self, vd, ue, edge_group_to_vd, vd_num_edges):
386
+ """
387
+ Regularizer L_dev as in Equation 8
388
+ """
389
+ dist = torch.norm(
390
+ ue - torch.index_select(input=vd, index=edge_group_to_vd, dim=0), dim=-1
391
+ )
392
+ mean_l2 = torch.zeros_like(vd[:, 0])
393
+ mean_l2 = (mean_l2).index_add_(
394
+ 0, edge_group_to_vd, dist
395
+ ) / vd_num_edges.squeeze(1).to(torch.get_default_dtype())
396
+ mad = (
397
+ dist - torch.index_select(input=mean_l2, index=edge_group_to_vd, dim=0)
398
+ ).abs()
399
+ return mad
400
+
401
+ def _normalize_weights(self, beta_fx4, alpha_fx4, surf_cubes):
402
+ """
403
+ Normalizes the given weights to be non-negative. If input weights are None, it creates and returns a set of weights of ones.
404
+ """
405
+ n_cubes = surf_cubes.shape[0]
406
+
407
+ if beta_fx4 is not None:
408
+ beta_fx4 = torch.tanh(beta_fx4) * self.weight_scale + 1
409
+ else:
410
+ beta_fx4 = torch.ones((n_cubes, 4), device=self.device)
411
+
412
+ if alpha_fx4 is not None:
413
+ alpha_fx4 = torch.tanh(alpha_fx4) * self.weight_scale + 1
414
+ else:
415
+ alpha_fx4 = torch.ones((n_cubes, 4), device=self.device)
416
+
417
+ return beta_fx4[surf_cubes], alpha_fx4[surf_cubes]
418
+
419
+ @torch.no_grad()
420
+ def _get_case_id(self, occ_fx4, surf_cubes):
421
+ """
422
+ Obtains the ID of topology cases based on cell corner occupancy. This function resolves the
423
+ ambiguity in the Dual Marching Cubes (DMC) configurations as described in Section 1.3 of the
424
+ supplementary material. It should be noted that this function assumes a regular grid.
425
+ """
426
+ case_ids = (
427
+ occ_fx4[surf_cubes] * self.cube_corners_idx.to(self.device).unsqueeze(0)
428
+ ).sum(-1)
429
+
430
+ return case_ids
431
+
432
+ @torch.no_grad()
433
+ def _identify_surf_edges(self, s_n, cube_fx4, surf_cubes):
434
+ """
435
+ Finds edges of active squares that intersect the contour by checking for opposite signs
436
+ of the scalar field at edge endpoints.
437
+ Assigns unique indices to surface-intersecting edges and produces a mapping
438
+ from square edges to unique edge IDs.
439
+ """
440
+ occ_n = s_n < 0
441
+ all_edges = cube_fx4[surf_cubes][:, self.cube_edges].reshape(-1, 2)
442
+
443
+ unique_edges, _idx_map, counts = torch.unique(
444
+ all_edges, dim=0, return_inverse=True, return_counts=True
445
+ )
446
+
447
+ unique_edges = unique_edges.long()
448
+ mask_edges = occ_n[unique_edges.reshape(-1)].reshape(-1, 2).sum(-1) == 1
449
+
450
+ surf_edges_mask = mask_edges[_idx_map]
451
+ counts = counts[_idx_map]
452
+
453
+ mapping = (
454
+ torch.ones(
455
+ (unique_edges.shape[0]), dtype=torch.long, device=cube_fx4.device
456
+ )
457
+ * -1
458
+ )
459
+ mapping[mask_edges] = torch.arange(mask_edges.sum(), device=cube_fx4.device)
460
+ # Shaped as [number of cubes x 4 edges per cube]. This is later used to map a cube edge to the unique index
461
+ # for a surface-intersecting edge. Non-surface-intersecting edges are marked with -1.
462
+ idx_map = mapping[_idx_map]
463
+ surf_edges = unique_edges[mask_edges]
464
+ return surf_edges, idx_map, counts, surf_edges_mask
465
+
466
+ @torch.no_grad()
467
+ def _identify_surf_cubes(self, s_n, cube_fx4):
468
+ """
469
+ Identifies grid cubes that intersect with the underlying surface by checking if the signs at
470
+ all corners are not identical.
471
+ """
472
+ occ_n = s_n < 0
473
+ occ_fx4 = occ_n[cube_fx4.reshape(-1)].reshape(-1, 4)
474
+ _occ_sum = torch.sum(occ_fx4, -1)
475
+ surf_cubes = (_occ_sum > 0) & (_occ_sum < 4)
476
+ return surf_cubes, occ_fx4
477
+
478
+ def _linear_interp(self, edges_weight, edges_x):
479
+ """
480
+ Computes the location of zero-crossings on 'edges_x' using linear interpolation with 'edges_weight'.
481
+ """
482
+ edge_dim = edges_weight.dim() - 2
483
+ assert edges_weight.shape[edge_dim] == 2
484
+ edges_weight = torch.cat(
485
+ [
486
+ torch.index_select(
487
+ input=edges_weight,
488
+ index=torch.tensor([1], device=self.device),
489
+ dim=edge_dim,
490
+ ),
491
+ -torch.index_select(
492
+ input=edges_weight,
493
+ index=torch.tensor([0], device=self.device),
494
+ dim=edge_dim,
495
+ ),
496
+ ],
497
+ edge_dim,
498
+ )
499
+ denominator = edges_weight.sum(edge_dim)
500
+ ue = (edges_x * edges_weight).sum(edge_dim) / denominator
501
+ return ue
502
+
503
+ def _compute_vd(
504
+ self, x_nx2, surf_edges, s_n, case_ids, beta_fx4, alpha_fx4, idx_map
505
+ ):
506
+ """
507
+ Computes the location of dual vertices as described in Section 4.2
508
+ """
509
+ alpha_nx4x2 = torch.index_select(
510
+ input=alpha_fx4, index=self.cube_edges, dim=1
511
+ ).reshape(-1, 4, 2)
512
+ surf_edges_x = torch.index_select(
513
+ input=x_nx2, index=surf_edges.reshape(-1), dim=0
514
+ ).reshape(-1, 2, 2)
515
+ surf_edges_s = torch.index_select(
516
+ input=s_n, index=surf_edges.reshape(-1), dim=0
517
+ ).reshape(-1, 2, 1)
518
+ zero_crossing = self._linear_interp(surf_edges_s, surf_edges_x)
519
+
520
+ idx_map = idx_map.reshape(-1, 4)
521
+
522
+ edge_group, edge_group_to_vd, edge_group_to_cube, vd_num_edges = (
523
+ [],
524
+ [],
525
+ [],
526
+ [],
527
+ )
528
+
529
+ num_vd = self.num_vd_table[case_ids]
530
+ # This is the "Source of Truth" for ordering
531
+ v_offsets = torch.cumsum(num_vd, dim=0) - num_vd
532
+ total_num_vd = num_vd.sum().item()
533
+
534
+ # total_num_vd = 0
535
+ vd = torch.zeros((total_num_vd, 2), device=self.device)
536
+ beta_sum = torch.zeros((total_num_vd, 1), device=self.device)
537
+ vd_num_edges = torch.zeros((total_num_vd, 1), device=self.device)
538
+
539
+ vd_idx_map = torch.zeros(
540
+ (case_ids.shape[0], 4),
541
+ dtype=torch.long,
542
+ device=self.device,
543
+ requires_grad=False,
544
+ )
545
+ all_edge_groups = []
546
+ all_edge_to_vd = []
547
+ all_edge_to_cube = []
548
+ all_vd_num_edges = []
549
+ for num in torch.unique(num_vd):
550
+ cur_cubes_mask = num_vd == num
551
+ num_selected = cur_cubes_mask.sum()
552
+ batch_v_starts = v_offsets[cur_cubes_mask]
553
+ # curr_num_vd = cur_cubes.sum() * num
554
+ curr_edge_group = self.dmc_table[case_ids[cur_cubes_mask], :num].reshape(
555
+ num_selected, -1
556
+ )
557
+ # Create global vertex IDs for this batch
558
+ # local_ids: [0, 1, 0, 1...] for num=2
559
+ local_ids = torch.arange(num, device=self.device).repeat(num_selected)
560
+ # global_ids: [start0, start0, start1, start1...]
561
+ global_starts = batch_v_starts.repeat_interleave(num)
562
+
563
+ # This aligns the dual vertex ID with your triangulation offsets
564
+ curr_edge_group_to_vd = (
565
+ (global_starts + local_ids).reshape(-1, 1).repeat(1, 2).reshape(-1)
566
+ )
567
+
568
+ # Mapping cubes to their global indices
569
+ curr_edge_group_to_cube = torch.arange(
570
+ case_ids.shape[0], device=self.device
571
+ )[cur_cubes_mask].repeat_interleave(num * 2)
572
+
573
+ curr_mask = curr_edge_group.reshape(-1) != -1
574
+ curr_vd_edge_counts = curr_mask.reshape(-1, 2).sum(-1, keepdims=True)
575
+ all_vd_num_edges.append(curr_vd_edge_counts)
576
+ all_edge_groups.append(curr_edge_group.reshape(-1)[curr_mask])
577
+ all_edge_to_vd.append(curr_edge_group_to_vd[curr_mask])
578
+ all_edge_to_cube.append(curr_edge_group_to_cube[curr_mask])
579
+
580
+ edge_group = torch.cat(all_edge_groups)
581
+ edge_group_to_vd = torch.cat(all_edge_to_vd)
582
+ edge_group_to_cube = torch.cat(all_edge_to_cube)
583
+ vd_num_edges = torch.cat(all_vd_num_edges)
584
+
585
+ vd = torch.zeros((total_num_vd, 2), device=self.device)
586
+ beta_sum = torch.zeros((total_num_vd, 1), device=self.device)
587
+
588
+ idx_group = torch.gather(
589
+ input=idx_map.reshape(-1), dim=0, index=edge_group_to_cube * 4 + edge_group
590
+ )
591
+
592
+ # remove the idx_group>0, should not be needed
593
+ x_group = torch.index_select(
594
+ input=surf_edges_x, index=idx_group.reshape(-1), dim=0
595
+ ).reshape(-1, 2, 2)
596
+ s_group = torch.index_select(
597
+ input=surf_edges_s, index=idx_group.reshape(-1), dim=0
598
+ ).reshape(-1, 2, 1)
599
+
600
+ zero_crossing_group = torch.index_select(
601
+ input=zero_crossing, index=idx_group.reshape(-1), dim=0
602
+ ).reshape(-1, 2)
603
+
604
+ alpha_group = torch.index_select(
605
+ input=alpha_nx4x2.reshape(-1, 2),
606
+ dim=0,
607
+ index=edge_group_to_cube * 4 + edge_group,
608
+ ).reshape(-1, 2, 1)
609
+ ue_group = self._linear_interp(s_group * alpha_group, x_group)
610
+
611
+ beta_group = torch.gather(
612
+ input=beta_fx4.reshape(-1), dim=0, index=edge_group_to_cube * 4 + edge_group
613
+ ).reshape(-1, 1)
614
+ beta_sum = beta_sum.index_add_(0, index=edge_group_to_vd, source=beta_group)
615
+ vd = (
616
+ vd.index_add_(0, index=edge_group_to_vd, source=ue_group * beta_group)
617
+ / beta_sum
618
+ )
619
+ L_dev = self._compute_reg_loss(
620
+ vd, zero_crossing_group, edge_group_to_vd, vd_num_edges
621
+ )
622
+
623
+ # v_idx = torch.arange(vd.shape[0], device=self.device) # + total_num_vd
624
+
625
+ vd_idx_map = vd_idx_map.reshape(-1).scatter(
626
+ dim=0, index=edge_group_to_cube * 4 + edge_group, src=edge_group_to_vd
627
+ )
628
+ # for vert in vd:
629
+ # ax.scatter(vert[0], vert[1], marker="x", color="lightblue")
630
+
631
+ return vd, L_dev, vd_idx_map
632
+
633
+ def _extract_zero_contour(
634
+ self, s_n, surf_edges, vd, edge_counts, idx_map, vd_idx_map, surf_edges_mask
635
+ ):
636
+ """
637
+ Connects two adjacent dual vertices around an active square to form a contour.
638
+ """
639
+
640
+ # for e_idx, e in enumerate(surf_edges):
641
+ # v0, v1 = x_nx2[e]
642
+ # color = "red" if edge_counts[surf_edges_mask][e_idx] == 1 else "green"
643
+ # mid = (v0 + v1) / 2
644
+ # ax.plot([v0[0], v1[0]], [v0[1], v1[1]], color=color, linewidth=1.5)
645
+ # # ax.text(
646
+ # # mid[0], mid[1], str(edge_counts[e_idx].item()), color=color, fontsize=7
647
+ # # )
648
+ # fig.savefig("sdf.png", dpi=1000, bbox_inches="tight")
649
+ with torch.no_grad():
650
+ group_mask = (
651
+ edge_counts == 2
652
+ ) & surf_edges_mask # surface edges shared by 2 cubes.
653
+ group = idx_map.reshape(-1)[group_mask]
654
+ vd_idx = vd_idx_map[group_mask]
655
+ edge_indices, indices = torch.sort(group, stable=True)
656
+ quad_vd_idx = vd_idx[indices].reshape(-1, 2)
657
+
658
+ # Ensure all face directions point towards the positive SDF to maintain consistent winding.
659
+ s_edges = s_n[
660
+ surf_edges[edge_indices.reshape(-1, 2)[:, 0]].reshape(-1)
661
+ ].reshape(-1, 2)
662
+ flip_mask = s_edges[:, 0] > 0
663
+ quad_vd_idx[flip_mask] = quad_vd_idx[flip_mask][:, [0, 1]]
664
+ quad_vd_idx[~flip_mask] = quad_vd_idx[~flip_mask][:, [1, 0]]
665
+
666
+ return vd, quad_vd_idx, s_edges, edge_indices
667
+
668
+ def _triangulate(
669
+ self,
670
+ x_nx2,
671
+ s_n,
672
+ square_fx4,
673
+ surf_vertices,
674
+ edges,
675
+ surf_edges_global,
676
+ s_edges,
677
+ case_ids,
678
+ edge_indices,
679
+ surf_cubes,
680
+ ):
681
+ """
682
+ Triangulates the interior surface to produce a triangular mesh, adopted from 3D as described in Section 4.5.
683
+ """
684
+ # occupancy field per query point
685
+ occ_n = s_n < 0
686
+ # occupancy field for each square
687
+ occ_fx4 = occ_n[square_fx4.reshape(-1)].reshape(-1, 4)
688
+ # sum of occupancy
689
+ # == 1: 1 corner inside
690
+ # == 2: 2 corners inside
691
+ # == 3: 3 corners inside
692
+ # == 4: 4 corners inside
693
+ occ_sum = torch.sum(occ_fx4, -1)
694
+ # get number of vd, most have 1, but the edge cases have 2
695
+ # the fully filled and completely empty squares have none
696
+ vd_counts = self.num_vd_table[case_ids]
697
+ v_offsets = torch.cumsum(vd_counts, dim=0) - vd_counts
698
+ tris_list = []
699
+ """
700
+ The first step is to connect all surface edges to the interior vertices
701
+ """
702
+
703
+ inside_verts = x_nx2[occ_n.reshape(-1)]
704
+ mapping_inside_verts = (
705
+ torch.ones((occ_n.shape[0]), dtype=torch.long, device=self.device) * -1
706
+ )
707
+ mapping_inside_verts[occ_n.reshape(-1)] = (
708
+ torch.arange(occ_n.sum(), device=self.device) + surf_vertices.shape[0]
709
+ )
710
+
711
+ s_edges = s_n[surf_edges_global].reshape(-1, 2) # signed distance at each end
712
+ inside_mask = s_edges < 0
713
+ inside_verts_idx = mapping_inside_verts[surf_edges_global[inside_mask]]
714
+
715
+ tris_surface = torch.cat([edges, inside_verts_idx.unsqueeze(-1)], -1)
716
+ tris_list.append(tris_surface)
717
+ vertices = torch.cat([surf_vertices, inside_verts])
718
+ # plot_triangles(
719
+ # "tris_inside_after_surf.png",
720
+ # triangles=tris_surface,
721
+ # vertices=vertices,
722
+ # x_nx2=x_nx2,
723
+ # s_n=s_n,
724
+ # )
725
+
726
+ # =====================================================================
727
+ #
728
+ # The next step is to find all quads that are fully inside (occsum==4)
729
+ # and connect the center to the edges
730
+ #
731
+ # =====================================================================
732
+
733
+ quads_occ4_center = (
734
+ x_nx2[square_fx4[occ_sum == 4].reshape(-1)].reshape(-1, 4, 2).mean(1)
735
+ )
736
+
737
+ quad_occ4_center_x = x_nx2[square_fx4[occ_sum == 4]].mean(1)
738
+ center_idx = (
739
+ torch.arange(quad_occ4_center_x.shape[0], device=self.device)
740
+ + surf_vertices.shape[0]
741
+ + inside_verts.shape[0]
742
+ )
743
+ index_occ4 = torch.tensor([[0, 1], [1, 3], [3, 2], [2, 0]], device=self.device)
744
+ quads_occ4 = square_fx4[occ_sum == 4]
745
+ centers_occ4_x4 = center_idx.unsqueeze(1).expand(-1, 4).reshape(-1, 1)
746
+
747
+ outside_edges_occ4 = mapping_inside_verts[quads_occ4[:, index_occ4]]
748
+ # and connect it to the center node:
749
+ # each triangle = [center, corner1, corner2]
750
+ tris_inside_occ4 = torch.cat(
751
+ [centers_occ4_x4, outside_edges_occ4.reshape(-1, 2)], dim=1
752
+ )
753
+ vertices = torch.cat([surf_vertices, inside_verts, quads_occ4_center])
754
+ tris_list.append(tris_inside_occ4)
755
+ # plot_triangles(
756
+ # "tris_inside_after_4.png",
757
+ # triangles=tris_inside_occ4,
758
+ # vertices=vertices,
759
+ # x_nx2=x_nx2,
760
+ # s_n=s_n,
761
+ # )
762
+ # =====================================================================
763
+ #
764
+ # The next step is to find all quads where three corners are inside
765
+ #
766
+ # =====================================================================
767
+
768
+ surf_vert_ids_occ3 = v_offsets[occ_sum[surf_cubes] == 3]
769
+ case_ids_occ3 = case_ids[occ_sum[surf_cubes] == 3]
770
+ quads_occ3 = square_fx4[occ_sum == 3]
771
+ index_occ3 = self.tet_table[case_ids_occ3]
772
+ K_occ3 = index_occ3.shape[1] # number of triangles per quad
773
+
774
+ # Expand quads so we can gather per-row
775
+ quads_expanded_occ3 = quads_occ3.unsqueeze(1).expand(-1, K_occ3, -1)
776
+
777
+ outside_edges_occ3 = mapping_inside_verts[
778
+ torch.gather(quads_expanded_occ3, 2, index_occ3)
779
+ ]
780
+ # and connect it to the center node:
781
+ # each triangle = [center, corner1, corner2]
782
+ centers = surf_vert_ids_occ3.unsqueeze(1).expand(-1, K_occ3) # (Q, K)
783
+ tris_inside_occ3 = torch.cat(
784
+ [centers.reshape(-1, 1), outside_edges_occ3.reshape(-1, 2)], dim=1
785
+ )
786
+ tris_list.append(tris_inside_occ3)
787
+ # plot_triangles(
788
+ # "tris_inside_after_3.png",
789
+ # triangles=tris_inside_occ3,
790
+ # vertices=vertices,
791
+ # x_nx2=x_nx2,
792
+ # s_n=s_n,
793
+ # )
794
+ # =====================================================================
795
+ #
796
+ # The next step is to find all quads where two corners are inside
797
+ # non edge cases (i.e. all except 6 and 9)
798
+ # =====================================================================
799
+ mask_occ2_standard = torch.logical_and(vd_counts == 1, occ_sum[surf_cubes] == 2)
800
+ if mask_occ2_standard.any():
801
+ surf_vert_ids_occ2_standard = v_offsets[mask_occ2_standard]
802
+ case_ids_occ2_standard = case_ids[mask_occ2_standard]
803
+ quads_occ2_standard = square_fx4[surf_cubes][mask_occ2_standard] # (Q,4)
804
+ quads_expanded_occ2_standard = quads_occ2_standard.unsqueeze(1).expand(
805
+ -1, 2, -1
806
+ )
807
+ index_occ2_standard = self.tet_table[case_ids_occ2_standard] # (Q, 2, 2)
808
+
809
+ valid_mask_occ2_standard = index_occ2_standard[..., 0] != -1 # (Q, 2)
810
+
811
+ outside_edges_occ2_standard = torch.gather(
812
+ quads_expanded_occ2_standard[valid_mask_occ2_standard, :],
813
+ 1,
814
+ index_occ2_standard[valid_mask_occ2_standard],
815
+ )
816
+
817
+ outside_edges_occ2_standard = mapping_inside_verts[
818
+ outside_edges_occ2_standard
819
+ ]
820
+
821
+ centers_occ2_standard = surf_vert_ids_occ2_standard.unsqueeze(1) # (Q, K)
822
+ tris_inside_occ2_standard = torch.cat(
823
+ [
824
+ centers_occ2_standard.reshape(-1, 1),
825
+ outside_edges_occ2_standard.reshape(-1, 2),
826
+ ],
827
+ dim=1,
828
+ )
829
+ tris_list.append(tris_inside_occ2_standard)
830
+ # plot_triangles(
831
+ # "tris_inside_after_2_standard.png",
832
+ # triangles=tris_inside_occ2_standard,
833
+ # vertices=vertices,
834
+ # x_nx2=x_nx2,
835
+ # s_n=s_n,
836
+ # )
837
+ # =====================================================================
838
+ #
839
+ # The next step is to find all quads where two corners are inside
840
+ # edge cases (i.e. 6 and 9)
841
+ # =====================================================================
842
+ mask_occ2_edge = torch.logical_and(vd_counts == 2, occ_sum[surf_cubes] == 2)
843
+ if mask_occ2_edge.any():
844
+ center_1_odd2_edge = (v_offsets[mask_occ2_edge]).unsqueeze(1).repeat(1, 2)
845
+ center_2_odd2_edge = center_1_odd2_edge + 1
846
+ case_ids_occ2_edge = case_ids[mask_occ2_edge]
847
+ # occ sum = on the global level, mask_occ2_edge = on surface level
848
+ quads_occ2_edge = square_fx4[surf_cubes][mask_occ2_edge] # (Q,4)
849
+ quads_expanded_occ2_edge = quads_occ2_edge.unsqueeze(1).expand(-1, 2, -1)
850
+ index_occ2_edge = self.tet_table[case_ids_occ2_edge] # (Q, 2, 2)
851
+
852
+ valid_mask_occ2_edge = index_occ2_edge[..., 0] != -1 # (Q, 2)
853
+
854
+ outside_edges_occ2_edge = torch.gather(
855
+ quads_expanded_occ2_edge[valid_mask_occ2_edge, :],
856
+ 1,
857
+ index_occ2_edge[valid_mask_occ2_edge],
858
+ )
859
+
860
+ outside_edges_occ2_edge = mapping_inside_verts[outside_edges_occ2_edge]
861
+
862
+ tris_inside_occ2_edge = torch.cat(
863
+ [
864
+ center_1_odd2_edge.reshape(-1, 1),
865
+ center_2_odd2_edge.reshape(-1, 1),
866
+ outside_edges_occ2_edge.reshape(-1, 1),
867
+ ],
868
+ dim=1,
869
+ )
870
+ # plot_triangles(
871
+ # "tris_inside_after_2_edge.png",
872
+ # triangles=tris_inside_occ2_edge,
873
+ # vertices=vertices,
874
+ # x_nx2=x_nx2,
875
+ # s_n=s_n,
876
+ # )
877
+
878
+ tris_list.append(tris_inside_occ2_edge)
879
+ tris = torch.cat(tris_list)
880
+ # fix orientation
881
+ p0 = vertices[tris[:, 0]]
882
+ p1 = vertices[tris[:, 1]]
883
+ p2 = vertices[tris[:, 2]]
884
+
885
+ cross = (p1[:, 0] - p0[:, 0]) * (p2[:, 1] - p0[:, 1]) - (
886
+ p1[:, 1] - p0[:, 1]
887
+ ) * (p2[:, 0] - p0[:, 0])
888
+
889
+ flip = cross < 0
890
+
891
+ # swap columns 1 and 2 where needed
892
+ tris_flipped = tris.clone()
893
+ tris_flipped[flip, 1] = tris[flip, 2]
894
+ tris_flipped[flip, 2] = tris[flip, 1]
895
+
896
+ tris = tris_flipped
897
+
898
+ return vertices, tris
899
+
900
+ def check_open_mesh(self, edges, num_vertices=None):
901
+ # edges shape: (Num_Edges, 2)
902
+ flattened_edges = edges.reshape(-1)
903
+
904
+ # Count occurrences of each vertex ID
905
+ if num_vertices is None:
906
+ num_vertices = flattened_edges.max() + 1
907
+
908
+ counts = torch.bincount(flattened_edges, minlength=num_vertices)
909
+
910
+ # A mesh is open if any vertex is used only once
911
+ open_points = (counts == 1).sum()
912
+ if open_points != 0:
913
+ raise RuntimeError(
914
+ "Unclosed mesh found. Check your bounds or SDF definition!"
915
+ )
916
+
917
+
918
+ # def plot_triangles(filename, triangles, vertices, x_nx2, s_n):
919
+ # import matplotlib.pyplot as plt
920
+
921
+ # fig, ax = plt.subplots()
922
+ # ax.set_aspect(1)
923
+ # for tri in triangles:
924
+ # v0, v1, c = tri
925
+ # p1 = vertices[v0]
926
+ # p2 = vertices[v1]
927
+ # p3 = vertices[c]
928
+ # ax.plot(
929
+ # [p1[0].item(), p2[0].item(), p3[0].item(), p1[0].item()],
930
+ # [p1[1].item(), p2[1].item(), p3[1].item(), p1[1].item()],
931
+ # "-k",
932
+ # )
933
+ # ax.text(
934
+ # p1[0].item(),
935
+ # p1[1].item(),
936
+ # str(v0),
937
+ # color="black",
938
+ # fontsize=8,
939
+ # ha="center",
940
+ # va="center",
941
+ # )
942
+ # ax.text(
943
+ # p2[0].item(),
944
+ # p2[1].item(),
945
+ # str(v1),
946
+ # color="black",
947
+ # fontsize=8,
948
+ # ha="center",
949
+ # va="center",
950
+ # )
951
+ # ax.text(
952
+ # p3[0].item(),
953
+ # p3[1].item(),
954
+ # str(c),
955
+ # color="black",
956
+ # fontsize=8,
957
+ # ha="center",
958
+ # va="center",
959
+ # )
960
+
961
+ # ax.scatter(
962
+ # x_nx2[:, 0].detach().cpu().numpy(),
963
+ # x_nx2[:, 1].detach().cpu().numpy(),
964
+ # c=s_n.detach().cpu().numpy(),
965
+ # cmap="coolwarm",
966
+ # )
967
+ # fig.savefig(filename, dpi=1000)