levelset
triwarp.levelset
Surfaces from scalar fields, and the surface operations whose answer is a different surface.
marching_cubes is the primitive: the iso level set of a
dense lattice, as a triangle mesh. It is the extraction tail of every implicit-surface pipeline in
the package -- proximity.signed_distance_grid,
voxels.to_field,
reconstruction.screened_poisson and
reconstruction.resample_uniform all end in it -- which
is why it lives here with its consumers rather than with the point-cloud reconstructors: none of
those callers is reconstructing from a cloud, they are extracting a level set.
offset_mesh moves a closed surface a fixed distance along its own
normal direction, in the only way that stays well defined where the surface curves back on itself:
through the signed distance field, whose level set at d is exactly the set of points at distance
d -- so it is marching_cubes over a shifted SDF. That is why an offset lives here rather
than in triwarp.remesh -- it is not a vertex displacement, and its output
topology is not the input's. A sphere offset inward past its radius vanishes; a thin plate offset
outward merges into one shell. Both are correct, and no per-vertex method produces either.
thicken_mesh is the topology-preserving counterpart and the one
member here that is not a level-set operation -- it earns its place by being the contrast. For
the common case where an open surface has to become a solid of known thickness and the input's own
triangulation should survive, it extrudes along the vertex normals and closes the rim, so the output
is the input plus a copy plus a band. Nothing is resampled and no field is built, which is exactly
what offset_mesh cannot promise and exactly why the two are shelved together: the choice between
them is the choice between keeping the triangulation and keeping the distance. It can self-intersect
where the thickness exceeds the local radius of curvature, and it does not guard against that --
triwarp.validation.face_self_intersecting_mask
names the condition exactly, and repairing it is a separate operation.
For the fields these consume, see
triwarp.proximity.signed_distance_grid; for the binary
occupancy lattice, triwarp.voxels.
marching_cubes
marching_cubes(
field: Array3dFloat32,
iso: float = 0.0,
*,
bounds: tuple[vec3, vec3] | None = None,
) -> tuple[wp.array[wp.vec3], wp.array[wp.int32]]
Extract the iso level set of a dense scalar lattice as a triangle mesh.
The extraction tail of every implicit-surface pipeline, exposed on its own so a caller with a
field of their own — an SDF, an occupancy volume, a simulation state — does not have to route it
through reconstruction.screened_poisson to get a
surface out. It is what
reconstruction.resample_uniform is built from.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
field
|
Array3dFloat32
|
|
required |
iso
|
float
|
Level to extract. Defaults to |
0.0
|
bounds
|
tuple[vec3, vec3] | None
|
|
None
|
Returns:
| Name | Type | Description |
|---|---|---|
vertices |
array[vec3]
|
Level-set vertices on |
faces |
array[int32]
|
Flat |
Raises:
| Type | Description |
|---|---|
TypeError
|
If |
ValueError
|
If any of |
See Also
reconstruction.resample_uniform
reconstruction.screened_poisson
triwarp.proximity.signed_distance_on_mesh
triwarp.voxels.to_field
triwarp.voxels.grid_points
Notes
A thin wrapper over Warp's own warp.MarchingCubes, so the triangulation, its vertex
deduplication and its handling of the ambiguous cube cases are Warp's rather than triwarp's. The
consequence worth knowing is that the result is not guaranteed manifold at an ambiguous
cell, and can carry duplicate vertices where two cells agree on a crossing —
reconstruction.resample_uniform runs
triwarp.repair over it for exactly that reason.
Source code in triwarp/levelset.py
78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 | |
offset_mesh
offset_mesh(
vertices: array[vec3],
faces: array[int32],
distance: float,
voxel_size: float | None = None,
*,
sign_mode: Literal["parity", "winding"] = "winding",
bounds: tuple[vec3, vec3] | None = None,
) -> tuple[wp.array[wp.vec3], wp.array[wp.int32]]
Offset a surface by a signed distance, through the level set of its distance field.
Positive grows the solid, negative shrinks it. The result is the exact set of points at distance
distance from the input, sampled at voxel_size -- so it handles the cases a per-vertex
displacement cannot: a shrink that makes a thin feature disappear, a growth that merges two
nearby sheets into one, and any offset of a surface with concave regions, where neighbouring
vertices moving along their own normals would cross.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
vertices
|
array[vec3]
|
|
required |
faces
|
array[int32]
|
Length- |
required |
distance
|
float
|
Signed offset. Positive is outward. |
required |
voxel_size
|
float | None
|
Lattice spacing for the distance field. |
None
|
sign_mode
|
Literal['parity', 'winding']
|
Forwarded to
|
'winding'
|
bounds
|
tuple[vec3, vec3] | None
|
|
None
|
Returns:
| Type | Description |
|---|---|
tuple[array[vec3], array[int32]]
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
RuntimeError
|
If |
Examples:
grown_v, grown_f = tw.levelset.offset_mesh(v, f, 0.1)
Notes
The defaults are the whole value of this function over composing the two calls it makes. The
field has to extend past the level set being extracted or that level set is clipped by the
lattice boundary, so the padding is ceil(distance / voxel_size) + 2 cells for an outward
offset; and the spacing has to resolve the band, which is what ties it to distance rather
than to the mesh. Getting either wrong yields a plausible surface with a hole in it, which is
why they are computed here rather than left to the caller. A derived voxel_size is also
bounded so that the lattice including its padding stays allocatable, which coarsens a very
large outward offset rather than refusing it; pass voxel_size to override that.
The output is a resampled surface: its triangulation is the marching-cubes lattice's, not the
input's, and its vertex count is set by voxel_size rather than by the input's. Where the
input's own triangulation must survive, the operation wanted is
thicken_mesh, not this.
Accuracy is the lattice's, improved by marching cubes' linear interpolation across a cell.
See Also
thicken_mesh
The topology-preserving shell, when the input triangulation should survive.
triwarp.proximity.signed_distance_grid
The field this extracts a level set from, when the field itself is wanted.
triwarp.levelset.marching_cubes
The extraction, and where the triangulation's own caveats live.
Source code in triwarp/levelset.py
152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 | |
thicken_mesh
thicken_mesh(
vertices: array[vec3],
faces: array[int32],
thickness: float,
*,
outside: float = 0.0,
) -> tuple[wp.array[wp.vec3], wp.array[wp.int32]]
Turn a surface into a solid shell of given thickness, keeping the input's triangulation.
The counterpart of offset_mesh for the case where the answer
should still be this mesh: every vertex is displaced along its own angle-weighted normal, a
reversed copy of the surface becomes the shell's other side, and the two are joined along every
boundary edge by a quad band. So the output is the input's connectivity twice over plus the band
-- 2 * n_faces + 2 * n_boundary_edges triangles -- and a per-vertex attribute follows
through by duplication, which no resampled offset allows.
On a closed surface there is no boundary to band, so the result is two nested shells: a solid with a cavity, which is what thickening a closed surface means.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
vertices
|
array[vec3]
|
|
required |
faces
|
array[int32]
|
Length- |
required |
thickness
|
float
|
Distance the shell extends inward, opposite the vertex normals. Must be positive. |
required |
outside
|
float
|
Distance the shell also extends outward, so the input surface ends up |
0.0
|
Returns:
| Type | Description |
|---|---|
tuple[array[vec3], array[int32]]
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
RuntimeError
|
If |
Examples:
shell_v, shell_f = tw.levelset.thicken_mesh(v, f, 0.05)
Notes
It can self-intersect, and it does not check. Displacing along vertex normals folds the
surface wherever thickness exceeds the local radius of curvature -- the inward layer of a
tube thickened past its own radius passes through itself -- and no per-vertex method avoids
that.
The condition is exactly
triwarp.validation.face_self_intersecting_mask,
so it is detectable in one call; where it happens, the operation wanted is a level-set
offset_mesh, which cannot self-intersect by construction. Not
guarding is deliberate: the guard would be a whole-mesh intersection test on every call, and the
caller who needs it can run the one that names the faces.
The normals are angle-weighted (the pseudonormal), which is the weighting that makes the
displacement independent of how the incident triangles happen to be subdivided;
triwarp.vertices documents the three choices.
See Also
offset_mesh
The resampling offset, for a shell that must not self-intersect.
vertices.vertex_normals at weighting="angle"
The displacement direction.
triwarp.boundary.oriented_boundary_edges
Where the band's orientation comes from.
Source code in triwarp/levelset.py
277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 | |