vertices
triwarp.vertices
Per-vertex quantities: normals under three weighting rules, and the angle defect.
A vertex has no normal of its own -- only the faces around it do -- so a vertex normal is always a
weighted average of incident face normals, and the weight is a modelling choice rather than a
detail. vertex_normals is the entry point, and its
weighting= keyword is that choice: by face area, by incident angle (the one invariant to how a
neighbouring polygon happened to be triangulated), or by Nelson Max's sine-and-edge-length
reciprocals.
Two accumulators sit underneath it and are public in their own right:
weighted_vertex_normals takes a caller-supplied
per-corner weight table and mean_vertex_normals is the
same thing with unit weights. They know no geometry -- they take no vertices at all, which is
why they keep an explicit n_vertices argument where vertex_normals derives it. That
asymmetry is the visible marker of which of the two layers you are in.
vertex_defects is the odd one out and not a normal at all:
2 * pi minus the incident angle sum, which is the intrinsic curvature concentrated at that
vertex and the quantity
discrete_gaussian_curvature reports.
mean_vertex_normals
mean_vertex_normals(
n_vertices: int,
faces: array[int32],
face_normals: array[vec3],
) -> wp.array[wp.vec3]
Vertex normals as the (unnormalized) sum of incident face normals, then unit-length.
For each vertex, face normals sharing that corner are accumulated in float32 on
faces.device, cast to wp.vec3, and L2-normalized. Vertices not referenced by
any face remain zero.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n_vertices
|
int
|
Number of vertices indexed by |
required |
faces
|
array[int32]
|
Triangle indices as |
required |
face_normals
|
array[vec3]
|
One unit (or unnormalized) normal per triangle, length |
required |
Returns:
| Type | Description |
|---|---|
array[vec3]
|
Length- |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
RuntimeError
|
If |
See Also
weighted_vertex_normals
The same accumulator with a per-corner weight table; this is that with unit weights.
vertex_normals
The geometry-aware entry point, which derives a weight table and calls one of these two.
Source code in triwarp/vertices.py
46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 | |
weighted_vertex_normals
weighted_vertex_normals(
n_vertices: int,
faces: array[int32],
face_normals: array[vec3],
face_weights: Array2dFloat32,
) -> wp.array[wp.vec3]
Vertex normals from a weighted sum of incident face normals, then unit-length.
Each face contributes its normal scaled by the per-corner weight in face_weights;
contributions are summed per vertex in float32 on faces.device, cast to
wp.vec3, and L2-normalized.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n_vertices
|
int
|
Number of vertices indexed by |
required |
faces
|
array[int32]
|
Triangle indices as |
required |
face_normals
|
array[vec3]
|
One normal per triangle, length |
required |
face_weights
|
Array2dFloat32
|
Per-corner weights, shape |
required |
Returns:
| Type | Description |
|---|---|
array[vec3]
|
Length- |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
RuntimeError
|
If |
See Also
mean_vertex_normals
This with unit weights.
vertex_normals
The geometry-aware entry point, which derives a weight table and calls this.
Source code in triwarp/vertices.py
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 | |
vertex_normals
vertex_normals(
vertices: array[vec3],
faces: array[int32],
*,
weighting: Literal["area", "angle", "mwselr"] = "area",
face_normals: array[vec3] | None = None,
face_weights: array[float32]
| Array2dFloat32
| None = None,
) -> wp.array[wp.vec3]
Per-vertex unit normals, as a weighted average of the incident face normals.
A vertex has no normal of its own -- only the faces around it do -- so the weight is the whole
content of the choice, and weighting selects it. Contributions are summed per vertex and
L2-normalized, so any constant factor in the weights cancels.
Which weighting to want is a measurable question, not a preference: one reference library
ships both conventions as separate entry points, and its plain per-vertex normals reproduce
"area" to 1.19e-07 while its pseudo-normals reproduce "angle" to 1.19e-07 --
each sitting 6.8e-03 from the other's partner, so the two are distinguishable well above
float32 noise. Reach for "angle" when the answer must not depend on
how a neighbouring polygon happened to be triangulated -- it is the only one of the three that
is invariant to that -- and for "area" otherwise, since it is what libigl and most of the
field default to.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
vertices
|
array[vec3]
|
|
required |
faces
|
array[int32]
|
Length- |
required |
weighting
|
Literal['area', 'angle', 'mwselr']
|
Which average to take.
|
'area'
|
face_normals
|
array[vec3] | None
|
Precomputed |
None
|
face_weights
|
array[float32] | Array2dFloat32 | None
|
Precomputed weights, to skip deriving them. The expected shape follows |
None
|
Returns:
| Type | Description |
|---|---|
array[vec3]
|
Length- |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
RuntimeError
|
If |
See Also
weighted_vertex_normals
The accumulator underneath: a caller-supplied per-corner weight table, no geometry.
mean_vertex_normals
The same accumulator with unit weights.
triwarp.triangles.face_normals_and_areas
Produces the face_normals / face_weights the "area" path derives.
triwarp.triangles.face_angles
Produces the "angle" path's weight table.
Source code in triwarp/vertices.py
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 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 342 343 344 345 346 347 348 349 350 351 352 353 | |
vertex_defects
vertex_defects(
n_vertices: int,
faces: array[int32],
face_angles: Array2dFloat32,
) -> wp.array[wp.float32]
Discrete angle defect per vertex: 2π minus the sum of incident corner angles.
For each vertex, interior angles from every triangle corner that references that vertex
are accumulated in float32 on faces.device, then subtracted from a full turn.
This is the standard piecewise-linear angle defect (related to discrete Gaussian
curvature via the Gauss—Bonnet viewpoint on triangle meshes).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n_vertices
|
int
|
Number of vertices indexed by |
required |
faces
|
array[int32]
|
Triangle indices as |
required |
face_angles
|
Array2dFloat32
|
Interior angles at the three corners of each triangle, shape |
required |
Returns:
| Type | Description |
|---|---|
array[float32]
|
Length- |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
RuntimeError
|
If |
See Also
discrete_gaussian_curvature
The same quantity at a scale: the Cohen-Steiner/Morvan ball measure sums these defects
over a ball of given radius, where this is the pointwise value at one vertex. The ball
measure is what converges under refinement; the pointwise defect does not.
face_angles
The angles this sums.
Source code in triwarp/vertices.py
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 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 | |