Skip to content

triangles

triwarp.triangles

Quantities carried by one triangle at a time: its normal, area, angles, shape and barycentre.

face_normals_and_areas, face_angles, face_centroids and face_quality are the per-face fields the rest of the package reduces over; face_nondegenerate_mask flags the faces those quantities are meaningless on. barycentric_to_points, points_to_barycentric and closest_point work one query point against one triangle, row by row, without a BVH -- for a query against the whole surface see triwarp.proximity.

CornerNormalWeighting module-attribute

CornerNormalWeighting = Literal['angle', 'area']

Per-corner weight selected by corner_normals.

FaceQualityMetric module-attribute

FaceQualityMetric = Literal[
    "aspect_ratio",
    "radius_ratio",
    "area_max_side",
    "mean_ratio",
    "area",
]

Shape-quality measure selected by face_quality.

face_normals_and_areas

face_normals_and_areas(
    vertices: array[vec3], faces: array[int32]
) -> tuple[wp.array[wp.vec3], wp.array[wp.float32]]

Compute the unit normal and area of each triangle.

Parameters:

Name Type Description Default
vertices array[vec3]

(n_vertices,) mesh vertex positions.

required
faces array[int32]

Length-3 * n_faces wp.int32 triangle index buffer.

required

Returns:

Name Type Description
normals array[vec3]

Length-n_faces unit face normals on vertices.device. Zero when a face is degenerate.

areas array[float32]

Length-n_faces triangle areas on vertices.device.

Raises:

Type Description
RuntimeError

If vertices and faces are not all on one device.

See Also

trimesh.Trimesh.face_normals trimesh.Trimesh.area_faces

Source code in triwarp/triangles.py
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
def face_normals_and_areas(
    vertices: wp.array[wp.vec3], faces: wp.array[wp.int32]
) -> tuple[wp.array[wp.vec3], wp.array[wp.float32]]:
    """
    Compute the unit normal and area of each triangle.

    Parameters
    ----------
    vertices
        ``(n_vertices,)`` mesh vertex positions.
    faces
        Length-``3 * n_faces`` ``wp.int32`` triangle index buffer.

    Returns
    -------
    normals : wp.array[wp.vec3]
        Length-``n_faces`` unit face normals on ``vertices.device``. Zero when a face is
        degenerate.
    areas : wp.array[wp.float32]
        Length-``n_faces`` triangle areas on ``vertices.device``.

    Raises
    ------
    RuntimeError
        If ``vertices`` and ``faces`` are not all on one device.

    See Also
    --------
    [`trimesh.Trimesh.face_normals`][]
    [`trimesh.Trimesh.area_faces`][]
    """
    require_same_device(vertices=vertices, faces=faces)
    f = faces.shape[0] // 3
    out_normal = wp.empty(f, dtype=wp.vec3, device=vertices.device)
    out_area = wp.empty(f, dtype=wp.float32, device=vertices.device)
    wp.launch(
        kernel_triangles.face_normals_and_areas,
        dim=f,
        inputs=[vertices, faces, out_normal, out_area],
        device=vertices.device,
    )
    return out_normal, out_area

face_angles

face_angles(
    vertices: array[vec3], faces: array[int32]
) -> twt.Array2dFloat32

Interior angle at each triangle vertex, in radians.

Parameters:

Name Type Description Default
vertices array[vec3]

(n_vertices,) mesh vertex positions.

required
faces array[int32]

Length-3 * n_faces wp.int32 triangle index buffer.

required

Returns:

Type Description
Array2dFloat32

Shape (n_faces, 3) interior angles on vertices.device, aligned with the corners (i0, i1, i2) of each face.

Raises:

Type Description
RuntimeError

If vertices and faces are not all on one device.

See Also

trimesh.triangles.angles

Source code in triwarp/triangles.py
 71
 72
 73
 74
 75
 76
 77
 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
def face_angles(vertices: wp.array[wp.vec3], faces: wp.array[wp.int32]) -> twt.Array2dFloat32:
    """
    Interior angle at each triangle vertex, in radians.

    Parameters
    ----------
    vertices
        ``(n_vertices,)`` mesh vertex positions.
    faces
        Length-``3 * n_faces`` ``wp.int32`` triangle index buffer.

    Returns
    -------
    twt.Array2dFloat32
        Shape ``(n_faces, 3)`` interior angles on ``vertices.device``, aligned with the
        corners ``(i0, i1, i2)`` of each face.

    Raises
    ------
    RuntimeError
        If ``vertices`` and ``faces`` are not all on one device.

    See Also
    --------
    [`trimesh.triangles.angles`][]
    """
    require_same_device(vertices=vertices, faces=faces)
    f = faces.shape[0] // 3
    out_angle = twt.empty_2d((f, 3), wp.float32, device=vertices.device)
    wp.launch(
        kernel_triangles.angles, dim=f, inputs=[vertices, faces, out_angle], device=vertices.device
    )
    return twt.as_array2d(out_angle, wp.float32)

corner_normals

corner_normals(
    vertices: array[vec3],
    faces: array[int32],
    crease_edges: Array2dInt32 | None = None,
    *,
    weighting: CornerNormalWeighting = "angle",
    twins: array[int32] | None = None,
    n_vertices: int | None = None,
    face_normals: array[vec3] | None = None,
    face_areas: array[float32] | None = None,
) -> twt.Array2dVec3

Per-corner normals: one normal for each (face, corner), averaged over its smooth group.

The third weighting triwarp lacked. A face normal is flat, a vertex normal is smooth everywhere, and neither can render a crease: a cube needs three different normals at each of its corners, one per incident face group. This gives each corner the angle-weighted average of the faces reachable from its own by rotating about its vertex without crossing a crease edge, which is what an OBJ export or a hard-edge renderer needs.

Two exact identities fall out of that definition and are worth knowing, because they are also how it is tested:

  • with no creases, every corner of a vertex gets that vertex's normal under the matching weighting -- vertex_normals at weighting="angle" or vertex_normals at weighting="area";
  • with every edge a crease, every corner gets its own face's normal, whatever the weighting.

Parameters:

Name Type Description Default
vertices array[vec3]

(n_vertices,) mesh vertex positions.

required
faces array[int32]

Length-3 * n_faces wp.int32 triangle index buffer.

required
crease_edges Array2dInt32 | None

(k, 2) wp.int32 vertex pairs to treat as hard edges, in either order per row -- typically crease_edges. None means no creases, which is the fully smooth case above and costs one comparison per rotation step rather than a branch.

None
weighting CornerNormalWeighting

Per-corner weight for the average, named as in triwarp.vertices' four vertex-normal functions: "angle" is the corner angle (Thuerrner & Wuethrich) and "area" is the face's own area, constant across its three corners. Both are offered because the two reference implementations disagree -- the one that computes per-corner normals directly is the area one.

'angle'
twins array[int32] | None

Optional precomputed halfedge_twins.

None
n_vertices int | None

Total vertex count. When None it is inferred with array.index_bound, which costs a host readback.

None
face_normals array[vec3] | None

Optional length-n_faces unit face normals and matching areas from face_normals_and_areas; recomputed together when either is None. Trimesh.face_normals and Trimesh.face_areas cache the pair.

None
face_areas array[float32] | None

See face_normals.

None

Returns:

Type Description
Array2dVec3

(n_faces, 3) unit normals on faces.device; entry (f, k) belongs to corner k of face f. A corner whose weighted sum cancels to zero comes back as the zero vector, matching weighted_vertex_normals.

Raises:

Type Description
TypeError

If crease_edges is given and is not a rank-2 wp.int32 array.

ValueError

If crease_edges is given and does not have two columns, or weighting is not one of the two names above. Also propagated from halfedge_twins when the mesh is not edge-manifold: rotating about a vertex is what this computes, and an edge with three faces has no rotation. Pass twins yourself if you have already resolved that.

RuntimeError

If vertices, faces, crease_edges, twins, face_normals and face_areas are not all on one device.

Examples:

creases = tw.seams.crease_edges(v, f, angle=0.5)
normals = tw.triangles.corner_normals(v, f, creases)
assert normals.shape == (f.shape[0] // 3, 3)
See Also

face_normals_and_areas The flat weighting: one normal per face. vertex_normals at weighting="angle" The smooth weighting this reduces to when there are no creases. crease_edges Produces the hard-edge set.

Source code in triwarp/triangles.py
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
150
151
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
def corner_normals(
    vertices: wp.array[wp.vec3],
    faces: wp.array[wp.int32],
    crease_edges: twt.Array2dInt32 | None = None,
    *,
    weighting: CornerNormalWeighting = "angle",
    twins: wp.array[wp.int32] | None = None,
    n_vertices: int | None = None,
    face_normals: wp.array[wp.vec3] | None = None,
    face_areas: wp.array[wp.float32] | None = None,
) -> twt.Array2dVec3:
    """
    Per-corner normals: one normal for each ``(face, corner)``, averaged over its smooth group.

    The third weighting triwarp lacked. A *face* normal is flat, a *vertex* normal is smooth
    everywhere, and neither can render a crease: a cube needs three different normals at each of its
    corners, one per incident face group. This gives each corner the angle-weighted average of the
    faces reachable from its own by rotating about its vertex **without crossing a crease edge**,
    which is what an OBJ export or a hard-edge renderer needs.

    Two exact identities fall out of that definition and are worth knowing, because they are also
    how it is tested:

    * with **no** creases, every corner of a vertex gets that vertex's normal under the matching
      weighting -- [`vertex_normals`][triwarp.vertices.vertex_normals] at ``weighting="angle"``
      or [`vertex_normals`][triwarp.vertices.vertex_normals] at ``weighting="area"``;
    * with **every** edge a crease, every corner gets its own face's normal, whatever the weighting.

    Parameters
    ----------
    vertices
        ``(n_vertices,)`` mesh vertex positions.
    faces
        Length-``3 * n_faces`` ``wp.int32`` triangle index buffer.
    crease_edges
        ``(k, 2)`` ``wp.int32`` vertex pairs to treat as hard edges, in either order per row --
        typically [`crease_edges`][triwarp.seams.crease_edges]. ``None`` means no creases, which is
        the fully smooth case above and costs one comparison per rotation step rather than a branch.
    weighting
        Per-corner weight for the average, named as in
        [`triwarp.vertices`][triwarp.vertices]' four vertex-normal functions: ``"angle"`` is the
        corner angle (Thuerrner & Wuethrich) and ``"area"`` is the face's own area, constant across
        its three corners. Both are offered because the two reference implementations disagree --
        the one that computes per-corner normals directly is the **area** one.
    twins
        Optional precomputed [`halfedge_twins`][triwarp.halfedge.halfedge_twins].
    n_vertices
        Total vertex count. When ``None`` it is inferred with
        [`array.index_bound`][triwarp.array.index_bound], which costs a host readback.
    face_normals
        Optional length-``n_faces`` unit face normals and matching areas from
        [`face_normals_and_areas`][triwarp.triangles.face_normals_and_areas]; recomputed together
        when either is ``None``. [`Trimesh.face_normals`][triwarp.mesh.Trimesh.face_normals] and
        [`Trimesh.face_areas`][triwarp.mesh.Trimesh.face_areas] cache the pair.
    face_areas
        See ``face_normals``.

    Returns
    -------
    twt.Array2dVec3
        ``(n_faces, 3)`` unit normals on ``faces.device``; entry ``(f, k)`` belongs to corner
        ``k`` of face ``f``. A corner whose weighted sum cancels to zero comes back as the zero
        vector, matching [`weighted_vertex_normals`][triwarp.vertices.weighted_vertex_normals].

    Raises
    ------
    TypeError
        If ``crease_edges`` is given and is not a rank-2 ``wp.int32`` array.
    ValueError
        If ``crease_edges`` is given and does not have two columns, or ``weighting`` is not one of
        the two names above. Also propagated from
        [`halfedge_twins`][triwarp.halfedge.halfedge_twins] when the mesh is **not edge-manifold**:
        rotating about a vertex is what this computes, and an edge with three faces has no rotation.
        Pass ``twins`` yourself if you have already resolved that.
    RuntimeError
        If ``vertices``, ``faces``, ``crease_edges``, ``twins``, ``face_normals`` and
        ``face_areas`` are not all on one device.

    Examples
    --------
    ```python
    creases = tw.seams.crease_edges(v, f, angle=0.5)
    normals = tw.triangles.corner_normals(v, f, creases)
    assert normals.shape == (f.shape[0] // 3, 3)
    ```

    See Also
    --------
    [`face_normals_and_areas`][triwarp.triangles.face_normals_and_areas]
        The flat weighting: one normal per face.
    [`vertex_normals`][triwarp.vertices.vertex_normals] at ``weighting="angle"``
        The smooth weighting this reduces to when there are no creases.
    [`crease_edges`][triwarp.seams.crease_edges]
        Produces the hard-edge set.
    """
    require_same_device(
        vertices=vertices,
        faces=faces,
        crease_edges=crease_edges,
        twins=twins,
        face_normals=face_normals,
        face_areas=face_areas,
    )
    device = faces.device
    n_faces = int(faces.shape[0]) // 3
    if n_faces == 0:
        return twt.as_array2d(twt.empty_2d((0, 3), wp.vec3, device=device), wp.vec3)
    if n_vertices is None:
        n_vertices = tw.array.index_bound(faces)
    if twins is None:
        twins = tw.halfedge.halfedge_twins(faces, n_vertices=n_vertices)

    crease_keys = wp.empty(0, dtype=wp.uint64, device=device)
    if crease_edges is not None:
        twt.ensure_edge_pairs(crease_edges, "crease_edges")
        if int(crease_edges.shape[0]) > 0:
            crease_keys = tw.grouping.sorted_undirected_edge_keys(crease_edges, n_vertices)

    if face_normals is None or face_areas is None:
        face_normals, face_areas = face_normals_and_areas(vertices, faces)
    if weighting == "angle":
        # ``(n_faces, 3)`` reshaped flat is already corner-indexed: row ``f`` column ``k`` is
        # corner ``3f + k``, the same index the kernel's thread carries.
        corner_weights = face_angles(vertices, faces).reshape(3 * n_faces)
    elif weighting == "area":
        # One area per face broadcast to its three corners -- a gather rather than a kernel, since
        # ``arange_repeat`` already builds ``i // 3``.
        corner_weights = tw.array.gather(
            face_areas, tw.array.arange_repeat(3 * n_faces, 3, device=device)
        )
    else:
        raise ValueError(f"weighting must be 'angle' or 'area', got {weighting!r}")

    normals = twt.empty_2d((n_faces, 3), wp.vec3, device=device)
    wp.launch(
        kernel_triangles.corner_normals,
        dim=3 * n_faces,
        inputs=[
            vertices,
            faces,
            twins,
            face_normals,
            corner_weights,
            crease_keys,
            wp.uint64(n_vertices),
            normals,
        ],
        device=device,
    )
    return twt.as_array2d(normals, wp.vec3)

face_quality

face_quality(
    vertices: array[vec3],
    faces: array[int32],
    metric: FaceQualityMetric = "aspect_ratio",
) -> wp.array[wp.float32]

Per-face shape-quality measure.

This is the quantity the library already uses internally to decide whether a triangle is worth keeping — isotropic_remesh and flip_to_delaunay gate on aspect_ratio, and the hole-filling cost functions weight candidate triangles by it — exposed so a caller can inspect or threshold it directly.

Parameters:

Name Type Description Default
vertices array[vec3]

(n_vertices,) mesh vertex positions.

required
faces array[int32]

Length-3 * n_faces wp.int32 triangle index buffer.

required
metric FaceQualityMetric

Which measure to compute. All but "area" are invariant to a uniform scaling of the mesh, and all but "aspect_ratio" are larger is better:

  • "aspect_ratio" (default) — circumradius over twice the inradius. 1 for an equilateral triangle and unbounded above, so smaller is better; a degenerate triangle reads +inf. This is the measure the remeshing gates use.
  • "radius_ratio" — inradius over circumradius, rescaled so an equilateral triangle reads 1; 0 when degenerate. MeshLab's inradius/circumradius.
  • "area_max_side" — twice the area over the longest side squared, sqrt(3)/2 at best. MeshLab's area/max side (scale-invariant despite the name).
  • "mean_ratio" — 4 sqrt(3) A / (a^2 + b^2 + c^2), 1 at best. MeshLab's Mean ratio.
  • "area" — the plain triangle area, for parity with MeshLab's Area; identical to the second return of face_normals_and_areas.
'aspect_ratio'

Returns:

Type Description
array[float32]

Length-n_faces quality values on vertices.device.

Raises:

Type Description
ValueError

If metric is not one of the listed names.

RuntimeError

If vertices and faces are not all on one device.

See Also

face_normals_and_areas face_nondegenerate_mask triwarp.remesh.isotropic_remesh

Source code in triwarp/triangles.py
275
276
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
def face_quality(
    vertices: wp.array[wp.vec3],
    faces: wp.array[wp.int32],
    metric: FaceQualityMetric = "aspect_ratio",
) -> wp.array[wp.float32]:
    """
    Per-face shape-quality measure.

    This is the quantity the library already uses internally to decide whether a triangle is worth
    keeping — [`isotropic_remesh`][triwarp.remesh.isotropic_remesh] and
    [`flip_to_delaunay`][triwarp.remesh.flip_to_delaunay] gate on ``aspect_ratio``, and the
    hole-filling cost functions weight candidate triangles by it — exposed so a caller can inspect
    or threshold it directly.

    Parameters
    ----------
    vertices
        ``(n_vertices,)`` mesh vertex positions.
    faces
        Length-``3 * n_faces`` ``wp.int32`` triangle index buffer.
    metric
        Which measure to compute. All but ``"area"`` are invariant to a uniform scaling of the
        mesh, and all but ``"aspect_ratio"`` are *larger is better*:

        - ``"aspect_ratio"`` (default) — circumradius over twice the inradius. ``1`` for an
          equilateral triangle and unbounded above, so **smaller is better**; a degenerate
          triangle reads ``+inf``. This is the measure the remeshing gates use.
        - ``"radius_ratio"`` — inradius over circumradius, rescaled so an equilateral triangle
          reads ``1``; ``0`` when degenerate. MeshLab's ``inradius/circumradius``.
        - ``"area_max_side"`` — twice the area over the longest side squared, ``sqrt(3)/2`` at
          best. MeshLab's ``area/max side`` (scale-invariant despite the name).
        - ``"mean_ratio"`` — ``4 sqrt(3) A / (a^2 + b^2 + c^2)``, ``1`` at best. MeshLab's
          ``Mean ratio``.
        - ``"area"`` — the plain triangle area, for parity with MeshLab's ``Area``; identical to
          the second return of
          [`face_normals_and_areas`][triwarp.triangles.face_normals_and_areas].

    Returns
    -------
    wp.array[wp.float32]
        Length-``n_faces`` quality values on ``vertices.device``.

    Raises
    ------
    ValueError
        If ``metric`` is not one of the listed names.
    RuntimeError
        If ``vertices`` and ``faces`` are not all on one device.

    See Also
    --------
    [`face_normals_and_areas`][triwarp.triangles.face_normals_and_areas]
    [`face_nondegenerate_mask`][triwarp.triangles.face_nondegenerate_mask]
    [`triwarp.remesh.isotropic_remesh`][triwarp.remesh.isotropic_remesh]
    """
    require_same_device(vertices=vertices, faces=faces)
    if metric not in _QUALITY_METRICS:
        raise ValueError(f"unknown metric {metric!r}, expected one of {sorted(_QUALITY_METRICS)}")
    f = faces.shape[0] // 3
    out_quality = wp.empty(f, dtype=wp.float32, device=vertices.device)
    wp.launch(
        kernel_triangles.face_quality,
        dim=f,
        inputs=[vertices, faces, _QUALITY_METRICS[metric], out_quality],
        device=vertices.device,
    )
    return out_quality

face_centroids

face_centroids(
    vertices: array[vec3], faces: array[int32]
) -> wp.array[wp.vec3]

Barycentre of every face: the mean of its three corners.

Not to be confused with surface_centroid, which is the mesh's single area-weighted centre. This is one point per triangle and no weighting is involved.

Parameters:

Name Type Description Default
vertices array[vec3]

(n_vertices,) mesh vertex positions.

required
faces array[int32]

Length-3 * n_faces wp.int32 triangle index buffer.

required

Returns:

Type Description
array[vec3]

(n_faces,) face barycentres on vertices.device.

Raises:

Type Description
RuntimeError

If vertices and faces are not all on one device.

See Also

centroid moments igl.barycenter

Source code in triwarp/triangles.py
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
def face_centroids(vertices: wp.array[wp.vec3], faces: wp.array[wp.int32]) -> wp.array[wp.vec3]:
    """
    Barycentre of every face: the mean of its three corners.

    Not to be confused with [`surface_centroid`][triwarp.measures.surface_centroid], which is the
    *mesh's* single area-weighted centre. This is one point per triangle and no weighting is
    involved.

    Parameters
    ----------
    vertices
        ``(n_vertices,)`` mesh vertex positions.
    faces
        Length-``3 * n_faces`` ``wp.int32`` triangle index buffer.

    Returns
    -------
    wp.array[wp.vec3]
        ``(n_faces,)`` face barycentres on ``vertices.device``.

    Raises
    ------
    RuntimeError
        If ``vertices`` and ``faces`` are not all on one device.

    See Also
    --------
    [`centroid`][triwarp.measures.surface_centroid]
    [`moments`][triwarp.measures.moments]
    ``igl.barycenter``
    """
    require_same_device(vertices=vertices, faces=faces)
    n_faces = int(faces.shape[0]) // 3
    out_centroids = wp.empty(n_faces, dtype=wp.vec3, device=vertices.device)
    if n_faces == 0:
        return out_centroids
    wp.launch(
        kernel_triangles.face_centroids,
        dim=n_faces,
        inputs=[vertices, faces, out_centroids],
        device=vertices.device,
    )
    return out_centroids

face_signed_volumes

face_signed_volumes(
    vertices: array[vec3] | array[vec3d],
    faces: array[int32],
    apex: vec3 | vec3d | None = None,
) -> wp.array[wp.float32] | wp.array[wp.float64]

Signed volume of the tetrahedron (apex, v0, v1, v2) for each face.

dot(v0 - apex, cross(v1 - apex, v2 - apex)) / 6, so the sign follows the face's winding: positive where the face turns its front to the apex. Summed over a closed, consistently wound surface this is the enclosed volume (volume) and is independent of apex; per face it is not, which is why the argument exists — sample_volume fans from the surface centroid so that every tetrahedron is positive on a star-shaped mesh, and uses the array as a cumulative distribution.

Parameters:

Name Type Description Default
vertices array[vec3] | array[vec3d]

(n_vertices,) positions, wp.vec3 or wp.vec3d. The output dtype follows: pass vec3d where the sum's low digits matter, as filter_laplacian's volume constraint does.

required
faces array[int32]

Length-3 * n_faces wp.int32 triangle index buffer.

required
apex vec3 | vec3d | None

Common apex of every tetrahedron, in vertices' dtype. None (the default) means the world origin, which is what every whole-mesh volume wants.

None

Returns:

Type Description
array[float32] | array[float64]

(n_faces,) signed volumes on vertices.device, in vertices' scalar type. Empty when faces is empty.

Raises:

Type Description
RuntimeError

If vertices and faces are not all on one device.

See Also

volume The sum of these over the whole mesh. is_volume Whether that sum is a meaningful volume at all. face_centroids

Source code in triwarp/triangles.py
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
def face_signed_volumes(
    vertices: wp.array[wp.vec3] | wp.array[wp.vec3d],
    faces: wp.array[wp.int32],
    apex: wp.vec3 | wp.vec3d | None = None,
) -> wp.array[wp.float32] | wp.array[wp.float64]:
    """
    Signed volume of the tetrahedron ``(apex, v0, v1, v2)`` for each face.

    ``dot(v0 - apex, cross(v1 - apex, v2 - apex)) / 6``, so the sign follows the face's winding:
    positive where the face turns its front to the apex. Summed over a closed, consistently wound
    surface this is the enclosed volume ([`volume`][triwarp.measures.volume]) and is independent of
    ``apex``; per face it is not, which is why the argument exists —
    [`sample_volume`][triwarp.sample.sample_volume] fans from the surface centroid so that every
    tetrahedron is positive on a star-shaped mesh, and uses the array as a cumulative distribution.

    Parameters
    ----------
    vertices
        ``(n_vertices,)`` positions, ``wp.vec3`` or ``wp.vec3d``. The output dtype follows: pass
        ``vec3d`` where the sum's low digits matter, as
        [`filter_laplacian`][triwarp.smoothing.filter_laplacian]'s volume constraint does.
    faces
        Length-``3 * n_faces`` ``wp.int32`` triangle index buffer.
    apex
        Common apex of every tetrahedron, in ``vertices``' dtype. ``None`` (the default) means the
        world origin, which is what every whole-mesh volume wants.

    Returns
    -------
    wp.array[wp.float32] | wp.array[wp.float64]
        ``(n_faces,)`` signed volumes on ``vertices.device``, in ``vertices``' scalar type. Empty
        when ``faces`` is empty.

    Raises
    ------
    RuntimeError
        If ``vertices`` and ``faces`` are not all on one device.

    See Also
    --------
    [`volume`][triwarp.measures.volume]
        The sum of these over the whole mesh.
    [`is_volume`][triwarp.validation.is_volume]
        Whether that sum is a meaningful volume at all.
    [`face_centroids`][triwarp.triangles.face_centroids]
    """
    require_same_device(vertices=vertices, faces=faces)
    n_faces = int(faces.shape[0]) // 3
    device = vertices.device
    scalar = wp.float64 if vertices.dtype is wp.vec3d else wp.float32
    volumes = wp.empty(n_faces, dtype=scalar, device=device)
    if n_faces == 0:
        return volumes
    if apex is None:
        apex = wp.vec3d(0.0, 0.0, 0.0) if vertices.dtype is wp.vec3d else wp.vec3(0.0, 0.0, 0.0)
    wp.launch(
        kernel_triangles.FACE_SIGNED_VOLUMES[vertices.dtype],
        dim=n_faces,
        inputs=[vertices, faces, apex, volumes],
        device=device,
    )
    return volumes

face_nondegenerate_mask

face_nondegenerate_mask(
    vertices: array[vec3], faces: array[int32]
) -> wp.array[wp.bool]

Flag triangles with non-zero area.

Parameters:

Name Type Description Default
vertices array[vec3]

(n_vertices,) mesh vertex positions.

required
faces array[int32]

Length-3 * n_faces wp.int32 triangle index buffer.

required

Returns:

Type Description
array[bool]

Length-n_faces mask on vertices.device; True where the triangle area is non-zero.

Raises:

Type Description
RuntimeError

If vertices and faces are not all on one device.

See Also

trimesh.triangles.nondegenerate

Source code in triwarp/triangles.py
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
def face_nondegenerate_mask(
    vertices: wp.array[wp.vec3], faces: wp.array[wp.int32]
) -> wp.array[wp.bool]:
    """
    Flag triangles with non-zero area.

    Parameters
    ----------
    vertices
        ``(n_vertices,)`` mesh vertex positions.
    faces
        Length-``3 * n_faces`` ``wp.int32`` triangle index buffer.

    Returns
    -------
    wp.array[wp.bool]
        Length-``n_faces`` mask on ``vertices.device``; ``True`` where the triangle area is
        non-zero.

    Raises
    ------
    RuntimeError
        If ``vertices`` and ``faces`` are not all on one device.

    See Also
    --------
    [`trimesh.triangles.nondegenerate`][]
    """
    require_same_device(vertices=vertices, faces=faces)
    f = faces.shape[0] // 3
    out_nondegenerate = wp.empty(f, dtype=wp.bool, device=vertices.device)
    wp.launch(
        kernel_triangles.face_nondegenerate_mask,
        dim=f,
        inputs=[vertices, faces, out_nondegenerate],
        device=vertices.device,
    )
    return out_nondegenerate

barycentric_to_points

barycentric_to_points(
    vertices: array[vec3],
    faces: array[int32],
    barycentric: array[vec3],
) -> wp.array[wp.vec3]

Convert barycentric coordinates to Cartesian points, one triangle per row.

Operates on the "triangle soup" convention shared with trimesh.triangles: row i of barycentric is evaluated against triangle i of faces (not every triangle against every coordinate).

Parameters:

Name Type Description Default
vertices array[vec3]

(n_vertices,) mesh vertex positions.

required
faces array[int32]

Length-3 * n_faces wp.int32 triangle index buffer.

required
barycentric array[vec3]

Length-n_faces barycentric coordinates (u, v, w) as wp.vec3, aligned one-to-one with the triangles in faces.

required

Returns:

Type Description
array[vec3]

Length-n_faces Cartesian points on vertices.device.

Raises:

Type Description
RuntimeError

If vertices, faces and barycentric are not all on one device.

See Also

points_to_barycentric The inverse: recovers barycentric coordinates from Cartesian points. trimesh.triangles.barycentric_to_points

Source code in triwarp/triangles.py
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
def barycentric_to_points(
    vertices: wp.array[wp.vec3], faces: wp.array[wp.int32], barycentric: wp.array[wp.vec3]
) -> wp.array[wp.vec3]:
    """
    Convert barycentric coordinates to Cartesian points, one triangle per row.

    Operates on the "triangle soup" convention shared with `trimesh.triangles`: row ``i``
    of ``barycentric`` is evaluated against triangle ``i`` of ``faces`` (not every triangle
    against every coordinate).

    Parameters
    ----------
    vertices
        ``(n_vertices,)`` mesh vertex positions.
    faces
        Length-``3 * n_faces`` ``wp.int32`` triangle index buffer.
    barycentric
        Length-``n_faces`` barycentric coordinates ``(u, v, w)`` as ``wp.vec3``, aligned
        one-to-one with the triangles in ``faces``.

    Returns
    -------
    wp.array[wp.vec3]
        Length-``n_faces`` Cartesian points on ``vertices.device``.

    Raises
    ------
    RuntimeError
        If ``vertices``, ``faces`` and ``barycentric`` are not all on one device.

    See Also
    --------
    [`points_to_barycentric`][triwarp.triangles.points_to_barycentric]
        The inverse: recovers barycentric coordinates from Cartesian points.
    [`trimesh.triangles.barycentric_to_points`][]
    """
    require_same_device(vertices=vertices, faces=faces, barycentric=barycentric)
    f = faces.shape[0] // 3
    out_points = wp.empty(f, dtype=wp.vec3, device=vertices.device)
    wp.launch(
        kernel_triangles.barycentric_to_points,
        dim=f,
        inputs=[vertices, faces, barycentric, out_points],
        device=vertices.device,
    )
    return out_points

points_to_barycentric

points_to_barycentric(
    vertices: array[vec3],
    faces: array[int32],
    points: array[vec3],
) -> wp.array[wp.vec3]

Convert Cartesian points to barycentric coordinates, one triangle per row.

Operates on the "triangle soup" convention shared with trimesh.triangles: row i of points is projected onto triangle i of faces (not every triangle against every point).

Parameters:

Name Type Description Default
vertices array[vec3]

(n_vertices,) mesh vertex positions.

required
faces array[int32]

Length-3 * n_faces wp.int32 triangle index buffer.

required
points array[vec3]

Length-n_faces query positions as wp.vec3, aligned one-to-one with the triangles in faces.

required

Returns:

Type Description
array[vec3]

Length-n_faces barycentric coordinates (u, v, w) on vertices.device. A triangle with exactly zero area has no barycentric frame; it is answered along the triangle's longest edge, which is what such a "triangle" actually is.

Raises:

Type Description
RuntimeError

If vertices, faces and points are not all on one device.

See Also

barycentric_to_points The inverse: recovers Cartesian points from barycentric coordinates. trimesh.triangles.points_to_barycentric

Notes

trimesh.triangles.points_to_barycentric offers a method choice between a cross-product ratio and Cramer's rule on the Gram system, and this function deliberately does not: the two are algebraically equal, and the Cramer form is less accurate at every triangle shape and scale measured and more accurate at none, returning wholly wrong finite values on the slivers an ordinary decimated mesh carries. The two also cost the same, both being launch-bound, so there was nothing to trade. The measured table is in triwarp.kernels.triangles.point_barycentric.

Source code in triwarp/triangles.py
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
def points_to_barycentric(
    vertices: wp.array[wp.vec3], faces: wp.array[wp.int32], points: wp.array[wp.vec3]
) -> wp.array[wp.vec3]:
    """
    Convert Cartesian points to barycentric coordinates, one triangle per row.

    Operates on the "triangle soup" convention shared with `trimesh.triangles`: row ``i`` of
    ``points`` is projected onto triangle ``i`` of ``faces`` (not every triangle against every
    point).

    Parameters
    ----------
    vertices
        ``(n_vertices,)`` mesh vertex positions.
    faces
        Length-``3 * n_faces`` ``wp.int32`` triangle index buffer.
    points
        Length-``n_faces`` query positions as ``wp.vec3``, aligned one-to-one with the
        triangles in ``faces``.

    Returns
    -------
    wp.array[wp.vec3]
        Length-``n_faces`` barycentric coordinates ``(u, v, w)`` on ``vertices.device``. A triangle
        with exactly zero area has no barycentric frame; it is answered along the triangle's longest
        edge, which is what such a "triangle" actually is.

    Raises
    ------
    RuntimeError
        If ``vertices``, ``faces`` and ``points`` are not all on one device.

    See Also
    --------
    [`barycentric_to_points`][triwarp.triangles.barycentric_to_points]
        The inverse: recovers Cartesian points from barycentric coordinates.
    [`trimesh.triangles.points_to_barycentric`][]

    Notes
    -----
    [`trimesh.triangles.points_to_barycentric`][] offers a ``method`` choice between a
    cross-product ratio and Cramer's rule on the Gram system, and this function deliberately does
    not: the two are algebraically equal, and the Cramer form is less accurate at every triangle
    shape and scale measured and more accurate at none, returning wholly wrong *finite* values on
    the slivers an ordinary decimated mesh carries. The two also cost the same, both being
    launch-bound, so there was nothing to trade. The measured table is in
    ``triwarp.kernels.triangles.point_barycentric``.
    """
    require_same_device(vertices=vertices, faces=faces, points=points)
    f = faces.shape[0] // 3
    out_barycentric = wp.empty(f, dtype=wp.vec3, device=vertices.device)
    wp.launch(
        kernel_triangles.points_to_barycentric,
        dim=f,
        inputs=[vertices, faces, points, out_barycentric],
        device=vertices.device,
    )
    return out_barycentric

closest_point

closest_point(
    vertices: array[vec3],
    faces: array[int32],
    points: array[vec3],
) -> wp.array[wp.vec3]

Closest point on each triangle to a query point, one triangle per row.

Operates on the "triangle soup" convention shared with trimesh.triangles: row i of points is projected onto triangle i of faces (not every triangle against every point). For nearest-triangle queries over an entire mesh, see closest_point_on_mesh instead.

Parameters:

Name Type Description Default
vertices array[vec3]

(n_vertices,) mesh vertex positions.

required
faces array[int32]

Length-3 * n_faces wp.int32 triangle index buffer.

required
points array[vec3]

Length-n_faces query positions as wp.vec3, aligned one-to-one with the triangles in faces.

required

Returns:

Type Description
array[vec3]

Length-n_faces closest points on vertices.device.

Raises:

Type Description
RuntimeError

If vertices, faces and points are not all on one device.

See Also

trimesh.triangles.closest_point

Source code in triwarp/triangles.py
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
def closest_point(
    vertices: wp.array[wp.vec3], faces: wp.array[wp.int32], points: wp.array[wp.vec3]
) -> wp.array[wp.vec3]:
    """
    Closest point on each triangle to a query point, one triangle per row.

    Operates on the "triangle soup" convention shared with `trimesh.triangles`: row ``i`` of
    ``points`` is projected onto triangle ``i`` of ``faces`` (not every triangle against every
    point). For nearest-triangle queries over an entire mesh, see
    [`closest_point_on_mesh`][triwarp.proximity.closest_point_on_mesh] instead.

    Parameters
    ----------
    vertices
        ``(n_vertices,)`` mesh vertex positions.
    faces
        Length-``3 * n_faces`` ``wp.int32`` triangle index buffer.
    points
        Length-``n_faces`` query positions as ``wp.vec3``, aligned one-to-one with the
        triangles in ``faces``.

    Returns
    -------
    wp.array[wp.vec3]
        Length-``n_faces`` closest points on ``vertices.device``.

    Raises
    ------
    RuntimeError
        If ``vertices``, ``faces`` and ``points`` are not all on one device.

    See Also
    --------
    [`trimesh.triangles.closest_point`][]
    """
    require_same_device(vertices=vertices, faces=faces, points=points)
    f = faces.shape[0] // 3
    out_closest = wp.empty(f, dtype=wp.vec3, device=vertices.device)
    wp.launch(
        kernel_triangles.closest_point,
        dim=f,
        inputs=[vertices, faces, points, out_closest],
        device=vertices.device,
    )
    return out_closest