smoothing
triwarp.smoothing
Laplacian smoothing filters, for vertex positions and for per-vertex scalar fields.
Most of the module moves geometry: filter_laplacian,
filter_taubin,
filter_humphrey,
filter_neighborhood_average,
filter_mut_dif_laplacian and
filter_implicit_fairing all diffuse vertex positions
through the same row-stochastic 1-ring operator, differing in the time integration and in what they
do to counteract shrinkage. smooth_region
and its sharp-boundary variant instead solve a Dirichlet problem over a region, holding the rest
of the mesh fixed.
Three functions break that pattern by working on the normal field instead of positions, which is
what lets them keep a crease sharp: filter_normals diffuses
face normals with a crease gate, filter_two_step then refits
the vertices to them, and filter_sharpen runs the
whole idea backwards to sharpen.
A second group relaxes toward something other than a Laplacian residual, which is what lets each
member fix a failure the filters above cannot see:
equalize_triangle_areas evens out triangle areas,
relax_keep_volume removes the shrinkage locally instead of
rescaling it away at the end, relax_approx fits a plane or a
quadric to a whole geodesic neighbourhood rather than averaging a 1-ring, and
filter_spikes moves only the vertices that fail an angle test.
The first three take a vertex region and a max_displacement bound, so a relaxation can be
confined to where it is wanted and kept within a tolerance of the surface it started from;
filter_spikes needs neither, because the set it touches is the answer to its own test.
smooth_region_boundary completes the region trio by
smoothing the region's rim curve, where the two above it smooth across the rim or inside it.
The verb tracks the mechanism, not the group, which is why that second group spans three of
them. filter_* runs a fixed operator to a schedule -- an assembled Laplacian, a normal-field
pass, a windowed Taubin pair -- so the answer is a function of the operator and the iteration count.
relax_* iterates against a geometric objective and re-derives its target every pass, which is
why those take a max_displacement bound: there is nothing in the mechanism that keeps the result
near the input. equalize_triangle_areas and smooth_region* name their objective outright
because there is only one of each. By that axis
filter_spikes is correctly a filter_*: it runs the same
fixed 1-ring operator the filters above it do, and only its selection is geometric. It sits in the
paragraph above because its selection is what makes it useful, not because it is a relaxation.
filter_scalar_laplacian runs the same operator over a
per-vertex scalar field rather than positions. Capping how fast such a field may vary along an
edge — the other half of turning a raw scalar into a usable sizing field — is not a smoothing filter
at all but a one-sided Lipschitz projection, and lives in
shortest_path_envelope.
filter_laplacian
filter_laplacian(
vertices: array[vec3],
faces: array[int32],
lamb: float = 0.5,
iterations: int = 10,
implicit_time_integration: bool = False,
volume_constraint: bool = True,
laplacian_operator: BsrMatrix[float32] | None = None,
) -> wp.array[wp.vec3]
Laplacian mesh smoothing (Vollmer et al.; Desbrun et al. implicit fairing).
Diffuses each vertex toward the mean of its 1-ring neighbors. With implicit_time_integration
the diffusion is solved implicitly (backward Euler, unconditionally stable); otherwise an
explicit forward-Euler step is used (stable for lamb <= 1). Mirrors
trimesh.smoothing.filter_laplacian but returns a new vertex array instead of mutating
the mesh in place.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
vertices
|
array[vec3]
|
|
required |
faces
|
array[int32]
|
Length- |
required |
lamb
|
float
|
Diffusion speed. |
0.5
|
iterations
|
int
|
Number of smoothing passes. |
10
|
implicit_time_integration
|
bool
|
If |
False
|
volume_constraint
|
bool
|
If |
True
|
laplacian_operator
|
BsrMatrix[float32] | None
|
Optional precomputed row-stochastic operator (see
|
None
|
Returns:
| Type | Description |
|---|---|
array[vec3]
|
Smoothed |
Raises:
| Type | Description |
|---|---|
RuntimeError
|
If |
Source code in triwarp/smoothing.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 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 | |
inflate
inflate(
vertices: array[vec3],
faces: array[int32],
pressure: float,
*,
iterations: int = 3,
pre_smooth: bool = True,
gradual: bool = True,
lamb: float = 0.5,
) -> wp.array[wp.vec3]
Inflate a surface under uniform pressure: normal displacement alternating with smoothing.
The "balloon" flow. Each pass moves every vertex along its own
vertex_normals at weighting="area" and then relaxes
with one Laplacian pass, which is what keeps the triangles from shearing as the surface grows.
Uses of it: puffing a thin shell out to a printable thickness, opening a collapsed scan, and
supplying a starting surface a fitting loop can shrink back onto data.
pressure is an absolute distance per pass, in the mesh's own units, so scale it off
something intrinsic -- a fraction of the mean edge length is the usual choice. With gradual
pass k of n uses pressure * (k + 1) / n rather than the full amount, which is
gentler on a mesh with fine triangles: the early passes let the relaxation redistribute
before the later ones push hard.
Not the implicit formulation
The reference inflations displace and then solve an implicit Laplacian system, where
this runs one explicit pass -- so the two produce different surfaces from the same
pressure, and the tests compare the properties an inflation must have (volume grows,
the displacement is normal-aligned) rather than positions. Pass the result through
filter_implicit_fairing if you want that
formulation; it costs a conjugate-gradient solve per pass, which is why it is not the
default here.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
vertices
|
array[vec3]
|
|
required |
faces
|
array[int32]
|
Length- |
required |
pressure
|
float
|
Distance to move along the normal per pass. Negative deflates. |
required |
iterations
|
int
|
Number of displace-and-relax passes. |
3
|
pre_smooth
|
bool
|
Run one relaxation pass before the first displacement. Worth it on a noisy input, where the normals are what the noise corrupts and displacing along them amplifies it. |
True
|
gradual
|
bool
|
Ramp the pressure linearly across the passes instead of applying it in full each time. |
True
|
lamb
|
float
|
Relaxation strength of each Laplacian pass, as
|
0.5
|
Returns:
| Type | Description |
|---|---|
array[vec3]
|
Inflated positions on |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
RuntimeError
|
If |
See Also
offset_mesh
The exact outward offset, through a signed distance field -- it changes the connectivity
and cannot self-intersect, where this keeps the mesh and can.
filter_laplacian
The relaxation half of each pass.
thicken_mesh
Turns a surface into a solid shell, which is the other way to give it thickness.
Source code in triwarp/smoothing.py
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 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 | |
filter_humphrey
filter_humphrey(
vertices: array[vec3],
faces: array[int32],
alpha: float = 0.1,
beta: float = 0.5,
iterations: int = 10,
laplacian_operator: BsrMatrix[float32] | None = None,
) -> wp.array[wp.vec3]
Laplacian smoothing with Humphrey (HC) filtering (Vollmer et al.).
Each pass performs a Laplacian smoothing step and then pushes the vertices partway back
toward their original and pre-smoothing positions, strongly reducing the shrinkage of plain
Laplacian smoothing. Mirrors trimesh.smoothing.filter_humphrey.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
vertices
|
array[vec3]
|
|
required |
faces
|
array[int32]
|
Length- |
required |
alpha
|
float
|
Shrinkage control in |
0.1
|
beta
|
float
|
Correction aggressiveness in |
0.5
|
iterations
|
int
|
Number of smoothing passes. |
10
|
laplacian_operator
|
BsrMatrix[float32] | None
|
Optional precomputed row-stochastic operator (see
|
None
|
Returns:
| Type | Description |
|---|---|
array[vec3]
|
Smoothed |
Raises:
| Type | Description |
|---|---|
RuntimeError
|
If |
Source code in triwarp/smoothing.py
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 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 451 452 453 454 455 456 457 458 459 | |
filter_spikes
filter_spikes(
vertices: array[vec3],
faces: array[int32],
min_angle_sum: float,
*,
max_iter: int = 10,
return_count: bool = False,
) -> wp.array[wp.vec3] | tuple[wp.array[wp.vec3], int]
Pull needle-like vertices back onto the surface, leaving every other vertex untouched.
A spike is a vertex whose incident corner angles sum to less than min_angle_sum. On a
flat surface that sum is 2 * pi; the sharper the cone, the smaller it gets, and a vertex on
a thin needle -- the classic scan and reconstruction artifact -- has a sum near zero. So the
threshold is read in radians against a full turn, and a value like 0.5 * pi selects
genuinely degenerate cones while leaving ordinary sharp features alone.
Each pass replaces the flagged vertices by the average of their closed 1-rings and nothing else moves: this is not a smoothing filter restricted to a region, it is a repair that touches only the vertices that fail the test. Flattening a spike changes its neighbours' angle sums, so the pass repeats -- a needle whose base is itself spiky needs more than one -- and stops as soon as a pass finds none.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
vertices
|
array[vec3]
|
|
required |
faces
|
array[int32]
|
Length- |
required |
min_angle_sum
|
float
|
Angle-sum threshold in radians. A vertex is a spike when its incident angles sum below it.
Compare against |
required |
max_iter
|
int
|
Cap on the number of passes. |
10
|
return_count
|
bool
|
If |
False
|
Returns:
| Name | Type | Description |
|---|---|---|
vertices |
array[vec3]
|
Repaired positions on |
flattened |
(int, optional)
|
Present when |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
RuntimeError
|
If |
See Also
filter_neighborhood_average
The unrestricted version of the move this makes, over every vertex.
vertex_defects
2 * pi minus the same angle sum, which is what the threshold is read against.
validation.face_defective_mask
Flags the faces a spike produces, where this flags the vertex itself.
Source code in triwarp/smoothing.py
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 491 492 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 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 | |
equalize_triangle_areas
equalize_triangle_areas(
vertices: array[vec3],
faces: array[int32],
iterations: int = 1,
force: float = 0.5,
no_shrinkage: bool = False,
region: array[bool] | None = None,
max_displacement: float | None = None,
*,
vertex_faces: tuple[array[int32], array[int32]]
| None = None,
) -> wp.array[wp.vec3]
Even out triangle areas by moving vertices, without touching the connectivity.
Every other filter in this module drives the surface toward a Laplacian residual of zero, which equalizes edge directions and says nothing about area: a patch of long thin triangles beside a patch of fat ones is already Laplacian-smooth. This one minimizes the summed squared areas of each vertex's incident triangles instead, which is the objective that actually redistributes them -- a vertex sitting close to one of its opposite edges is pushed away from it.
Twice a triangle's area is |(x - p) x (q - p)| for the free vertex x over its opposite
edge (p, q), so the per-vertex objective is a sum of quadratic forms and its minimum is one
3x3 solve, taken in float64. Each pass steps a fraction force of the way there, from the
previous pass's positions, so the whole mesh moves at once and the result does not depend on
a vertex ordering.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
vertices
|
array[vec3]
|
|
required |
faces
|
array[int32]
|
Length- |
required |
iterations
|
int
|
Number of passes. |
1
|
force
|
float
|
Fraction of the way to the per-vertex minimum each pass moves. |
0.5
|
no_shrinkage
|
bool
|
Constrain each vertex to the tangent plane through its current position, so it slides across the surface instead of sinking into it. The unconstrained minimum of squared area pulls the whole 1-ring inward, so leave this on when the shape matters and off when only the triangle quality does. |
False
|
region
|
array[bool] | None
|
|
None
|
max_displacement
|
float | None
|
Clamp every vertex into a ball of this radius around its input position, applied after
each pass. |
None
|
vertex_faces
|
tuple[array[int32], array[int32]] | None
|
Optional precomputed |
None
|
Returns:
| Type | Description |
|---|---|
array[vec3]
|
Relaxed |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
RuntimeError
|
If |
See Also
relax_keep_volume
The other member of this group: a uniform relax that does not shrink the shape.
filter_laplacian
The plain diffusion this is the area-driven alternative to.
isotropic_remesh
Equalizes edge lengths, by changing the connectivity as well.
Notes
It equalizes areas, not shapes
Nothing here bounds a triangle's aspect ratio, and a long thin triangle can have exactly the
right area. For shape, flip the triangulation
(flip_by_objective) or remesh
(isotropic_remesh); this moves vertices only.
Source code in triwarp/smoothing.py
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 599 600 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 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 | |
relax_keep_volume
relax_keep_volume(
vertices: array[vec3],
faces: array[int32],
iterations: int = 1,
force: float = 0.5,
region: array[bool] | None = None,
max_displacement: float | None = None,
) -> wp.array[wp.vec3]
Uniform relaxation with the shrinkage taken out locally, rather than rescaled away.
A plain 1-ring average moves every vertex toward the inside of the surface it curves around,
so a closed shape loses volume with every pass.
filter_laplacian's volume_constraint answers that
by rescaling the whole mesh at the end, which restores the total and is wrong everywhere the
shrinkage was not uniform. This answers it per vertex instead: the relax displacement is
computed as a field, and each vertex then has its own neighbourhood's average
displacement subtracted from its own.
That difference is the whole method. A translation shared by a neighbourhood -- which is what shrinkage locally is -- cancels exactly, while the high-frequency part of the displacement, which is the noise the relax was for, survives untouched. No global rescale, and a mesh with a flat region and a curved one is treated correctly in both.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
vertices
|
array[vec3]
|
|
required |
faces
|
array[int32]
|
Length- |
required |
iterations
|
int
|
Number of passes. |
1
|
force
|
float
|
Fraction of the way to the 1-ring average each pass moves before the correction. |
0.5
|
region
|
array[bool] | None
|
|
None
|
max_displacement
|
float | None
|
Clamp every vertex into a ball of this radius around its input position, applied after
each pass. |
None
|
Returns:
| Type | Description |
|---|---|
array[vec3]
|
Relaxed |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
RuntimeError
|
If |
See Also
filter_laplacian
The plain diffusion, whose volume_constraint is the global rescale this replaces.
filter_taubin
The other anti-shrinkage answer: alternate a shrinking pass with an inflating one.
equalize_triangle_areas
Source code in triwarp/smoothing.py
695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 | |
relax_approx
relax_approx(
vertices: array[vec3],
faces: array[int32],
dilate_radius: float,
iterations: int = 1,
force: float = 0.5,
fit: Literal["planar", "quadric"] = "planar",
region: array[bool] | None = None,
max_displacement: float | None = None,
) -> wp.array[wp.vec3]
Move each vertex onto a surface fitted to its neighbourhood, rather than toward its 1-ring.
Every diffusion filter here is a 1-ring operator, so the largest feature it can see is one edge
across and its idea of "where the surface is" is the average of six neighbours. This fits a
local surface -- a least-squares plane, or a quadric graph over that plane -- to every vertex
inside a geodesic ball of dilate_radius and moves the vertex onto it. The ball is what makes
it different in kind: noise smaller than the radius is averaged out in one pass instead of being
diffused away over many, and the fitted surface is not pulled off the shape by the vertex being
fitted.
fit="planar" flattens: it is the strongest of the two and will take the curvature out of a
genuinely curved surface if the radius is large. fit="quadric" keeps curvature, because a
curved surface is in its model space, and is the one to use on anything but a plate.
The neighbourhood is a geodesic ball (geodesic_ball),
not a Euclidean one, so the opposite wall of a thin tube is excluded even when it is close --
which is exactly the case that corrupts a fit.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
vertices
|
array[vec3]
|
|
required |
faces
|
array[int32]
|
Length- |
required |
dilate_radius
|
float
|
Radius of the neighbourhood ball, in world units. It has no scale-free default: below one
edge length the ball is the vertex alone and the call is a no-op, and the useful range
starts at a small multiple of the mean edge length
( |
required |
iterations
|
int
|
Number of passes. |
1
|
force
|
float
|
Fraction of the way to the fitted surface each pass moves. |
0.5
|
fit
|
Literal['planar', 'quadric']
|
|
'planar'
|
region
|
array[bool] | None
|
|
None
|
max_displacement
|
float | None
|
Clamp every vertex into a ball of this radius around its input position, applied after
each pass. |
None
|
Returns:
| Type | Description |
|---|---|
array[vec3]
|
Relaxed |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
RuntimeError
|
If |
See Also
filter_laplacian
The 1-ring diffusion this replaces with a neighbourhood fit.
principal_curvature
Fits a quadric over the same geodesic balls, to measure rather than to move.
geodesic_ball
Notes
A vertex with too small a ball does not move
A plane fit needs three independent points and the quadric six, so a vertex whose ball holds
fewer than six vertices is left exactly where it is rather than fitted to whatever it has.
On a mesh whose edges are longer than dilate_radius that is every vertex and the call
returns the input -- check the result moved before concluding the parameters were right.
Source code in triwarp/smoothing.py
789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 | |
filter_taubin
filter_taubin(
vertices: array[vec3],
faces: array[int32],
lamb: float = 0.5,
nu: float = 0.5,
iterations: int = 10,
laplacian_operator: BsrMatrix[float32] | None = None,
*,
recompute: bool = False,
) -> wp.array[wp.vec3]
Taubin lambda/nu mesh smoothing (Vollmer et al.).
Alternates a shrinking diffusion step (+lamb) on even passes with an inflating step
(-nu) on odd passes, so the low-frequency shape is preserved while high-frequency noise is
removed with little volume loss. Mirrors trimesh.smoothing.filter_taubin.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
vertices
|
array[vec3]
|
|
required |
faces
|
array[int32]
|
Length- |
required |
lamb
|
float
|
Shrinking step size in |
0.5
|
nu
|
float
|
Inflating step size in |
0.5
|
iterations
|
int
|
Number of smoothing passes (shrink and inflate alternate by pass index). |
10
|
laplacian_operator
|
BsrMatrix[float32] | None
|
Optional precomputed row-stochastic operator (see
|
None
|
recompute
|
bool
|
Reassemble the inverse-distance operator from the current positions before every pass,
instead of applying one operator throughout. Off by default; see the |
False
|
Returns:
| Type | Description |
|---|---|
array[vec3]
|
Smoothed |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
RuntimeError
|
If |
See Also
filter_laplacian
trimesh.smoothing.filter_taubin
laplacian
The operator this applies, and the equal_weight switch recompute pins to False.
Notes
recompute is a different filter, not a tuning of this one:
pytorch3d.ops.taubin_smoothing rebuilds its inverse-distance operator from the current
geometry before each half-pass, and matching that convention is what makes recompute=True
agree with it, where the fixed operator does not. The default is unchanged and stays the fixed
operator, which is trimesh's filter and this group's oracle.
It is pinned to the inverse-distance weighting rather than taking a weighting of its own,
and that is not a simplification: the uniform operator is 1 / degree off the diagonal, a
function of the connectivity alone, which a position filter never changes -- so recomputing it
would rebuild the identical matrix every pass and cost without doing anything. Only the
geometry-dependent branch has anything to recompute.
recompute reassembles a sparse operator every pass instead of once, which is markedly more
expensive; the connectivity never changes, though, so laplacian's edges keyword lets
every pass share one edges_unique call rather than re-deriving it each time.
Source code in triwarp/smoothing.py
952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 | |
filter_neighborhood_average
filter_neighborhood_average(
vertices: array[vec3],
faces: array[int32],
iterations: int = 10,
laplacian_operator: BsrMatrix[float32] | None = None,
) -> wp.array[wp.vec3]
Uniform neighborhood-average smoothing over the closed 1-ring.
Each pass replaces every vertex by the equal-weight average of itself and its direct
neighbors, v' = (v + sum(v_neighbors)) / (1 + degree). Unlike
filter_laplacian (which averages the open 1-ring,
excluding the vertex, and scales the result by lamb) this is the simplest possible
smoother, with no diffusion coefficient. It is the CPU/GPU-portable analogue of Open3D's
TriangleMesh.filter_smooth_simple.
Isolated vertices (degree 0) are left unchanged.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
vertices
|
array[vec3]
|
|
required |
faces
|
array[int32]
|
Length- |
required |
iterations
|
int
|
Number of smoothing passes. |
10
|
laplacian_operator
|
BsrMatrix[float32] | None
|
Optional precomputed row-stochastic operator (see
|
None
|
Returns:
| Type | Description |
|---|---|
array[vec3]
|
Smoothed |
Raises:
| Type | Description |
|---|---|
RuntimeError
|
If |
See Also
Source code in triwarp/smoothing.py
1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 | |
filter_mut_dif_laplacian
filter_mut_dif_laplacian(
vertices: array[vec3],
faces: array[int32],
lamb: float = 0.5,
iterations: int = 10,
volume_constraint: bool = True,
laplacian_operator: BsrMatrix[float32] | None = None,
*,
face_normals: array[vec3] | None = None,
face_areas: array[float32] | None = None,
) -> wp.array[wp.vec3]
Laplacian smoothing with a mutable diffusion coefficient (Barroqueiro et al.).
Each pass runs a Laplacian diffusion step whose speed is adapted per vertex: the
coefficient lamber is scaled by how much of the vertex's Laplacian residual lies along
its normal, so flat regions diffuse quickly while feature regions diffuse slowly. When
volume_constraint is set, the mesh is inflated along its vertex normals (rather than
isotropically rescaled) to counteract shrinkage. Mirrors
trimesh.smoothing.filter_mut_dif_laplacian but returns a new vertex array instead of
mutating the mesh in place.
Following the reference, the vertex normals (and the finite-difference step eps) are
computed once from the input mesh and held constant across all passes.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
vertices
|
array[vec3]
|
|
required |
faces
|
array[int32]
|
Length- |
required |
lamb
|
float
|
Diffusion speed. |
0.5
|
iterations
|
int
|
Number of smoothing passes. |
10
|
volume_constraint
|
bool
|
If |
True
|
laplacian_operator
|
BsrMatrix[float32] | None
|
Optional precomputed row-stochastic operator (see
|
None
|
face_normals
|
array[vec3] | None
|
Optional length- |
None
|
face_areas
|
array[float32] | None
|
See |
None
|
Returns:
| Type | Description |
|---|---|
array[vec3]
|
Smoothed |
Raises:
| Type | Description |
|---|---|
RuntimeError
|
If |
Source code in triwarp/smoothing.py
1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 | |
filter_implicit_fairing
filter_implicit_fairing(
vertices: array[vec3],
faces: array[int32],
lamb: float = 0.1,
iterations: int = 10,
*,
pin_boundary: bool = True,
) -> wp.array[wp.vec3]
Implicit fairing with the cotangent Laplace-Beltrami operator (Desbrun et al.).
Solves the backward-Euler diffusion (M - lamb L) V' = M V each pass, where L is the
cotangent stiffness matrix (cotmatrix) and M the
barycentric lumped mass matrix (mass_matrix), both recomputed
from the current geometry. This is the most accurate of the smoothing filters (curvature flow;
libigl tutorial 205). The solve uses float64 throughout.
On a mesh with an open boundary the flow needs a boundary condition, which is what
pin_boundary supplies: without one the unconstrained boundary is pulled inward, collapsing
the triangles there, and since the system is solved by conjugate gradient (the only Warp solver
available) further passes diverge rather than degrade gracefully. Pinning turns the pass into a
Dirichlet problem over the interior, which is the formulation Desbrun et al. give for a surface
with boundary and is well posed for any number of passes.
A closed mesh has no boundary, so pin_boundary costs nothing and changes nothing there --
the solve is over every vertex either way, and curvature flow on a closed surface still shrinks
it (a sphere contracts toward a point, which is the method working, not failing).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
vertices
|
array[vec3]
|
|
required |
faces
|
array[int32]
|
Length- |
required |
lamb
|
float
|
Diffusion time step. Larger values smooth faster; the implicit solve is unconditionally stable. |
0.1
|
iterations
|
int
|
Number of smoothing passes. |
10
|
pin_boundary
|
bool
|
Hold boundary vertices at their input positions and solve only for the interior. A no-op on
a closed mesh. Set |
True
|
Returns:
| Type | Description |
|---|---|
array[vec3]
|
Smoothed |
Raises:
| Type | Description |
|---|---|
RuntimeError
|
If |
See Also
Source code in triwarp/smoothing.py
1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 1340 1341 1342 1343 1344 1345 1346 1347 1348 1349 1350 1351 1352 1353 1354 1355 1356 1357 1358 1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 1373 1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 1397 1398 1399 1400 1401 1402 1403 1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425 1426 1427 1428 1429 1430 1431 1432 1433 1434 1435 1436 1437 1438 1439 1440 1441 1442 1443 1444 1445 1446 1447 1448 1449 1450 1451 1452 1453 1454 | |
smooth_region_fixed_rim
smooth_region_fixed_rim(
vertices: array[vec3],
faces: array[int32],
free_mask: array[bool],
stabilizer: float = 0.0,
) -> wp.array[wp.vec3]
Reposition a free vertex region as the umbrella-Laplacian solution with a sharp fixed boundary.
The free vertices (free_mask) are moved to the solution of the graph-Laplacian Dirichlet
system (D - W) x = b (unit edge weights), where the fixed one-ring neighbours are folded
into b, so the region rim stays sharp
(a hard C⁰ constraint). Fixed vertices keep their positions.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
vertices
|
array[vec3]
|
|
required |
faces
|
array[int32]
|
Length- |
required |
free_mask
|
array[bool]
|
Length- |
required |
stabilizer
|
float
|
Optional attraction to the original position (adds to the diagonal), which also makes the
system solvable for a fully-free component. Defaults to |
0.0
|
Returns:
| Type | Description |
|---|---|
array[vec3]
|
New vertex positions on |
Raises:
| Type | Description |
|---|---|
RuntimeError
|
If |
See Also
smooth_region
smooth_region_boundary
The third case: smooths the rim's own path rather than the surface on either side of it.
fill_smooth
Notes
The system is SPD only when no free connected component is entirely free (or stabilizer >
0); the hole-filling pipeline guarantees this because the patch rim is always fixed.
A free vertex that no face refers to contributes no row, so the solve does not constrain it
and it is returned where it was. That is what the solution seed decides rather than the
system: it is seeded with the current positions, not with zeros.
Source code in triwarp/smoothing.py
1518 1519 1520 1521 1522 1523 1524 1525 1526 1527 1528 1529 1530 1531 1532 1533 1534 1535 1536 1537 1538 1539 1540 1541 1542 1543 1544 1545 1546 1547 1548 1549 1550 1551 1552 1553 1554 1555 1556 1557 1558 1559 1560 1561 1562 1563 1564 1565 1566 1567 1568 1569 1570 1571 1572 1573 1574 1575 1576 1577 1578 1579 1580 1581 1582 1583 1584 1585 1586 1587 1588 1589 1590 1591 1592 1593 1594 1595 1596 1597 1598 1599 1600 1601 1602 1603 1604 1605 1606 1607 1608 1609 1610 1611 1612 1613 1614 1615 1616 1617 1618 1619 1620 1621 1622 1623 1624 1625 1626 1627 1628 1629 1630 1631 | |
smooth_region
smooth_region(
vertices: array[vec3],
faces: array[int32],
free_mask: array[bool],
edge_weights: str = "cotan",
) -> wp.array[wp.vec3]
Reposition a free vertex region so the surface is smooth across the region boundary too.
Every vertex in the region and its first fixed ring contributes the umbrella equation
p_v = Σ_d (w_vd / ΣW) p_d; free vertices are unknowns and fixed neighbours move to the
right-hand side. The normal equations (MᵀM) x = Mᵀ b are solved per
coordinate, giving a patch that is smooth (C¹) across the region rim, unlike the sharp-rim
smooth_region_fixed_rim.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
vertices
|
array[vec3]
|
|
required |
faces
|
array[int32]
|
Length- |
required |
free_mask
|
array[bool]
|
Length- |
required |
edge_weights
|
str
|
|
'cotan'
|
Returns:
| Type | Description |
|---|---|
array[vec3]
|
New vertex positions on |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
RuntimeError
|
If |
See Also
smooth_region_fixed_rim
smooth_region_boundary
The third case: smooths the rim's own path rather than the surface on either side of it.
fill_smooth
Notes
A free vertex that no face refers to contributes no row, so the least-squares system does not
constrain it and it is returned where it was. That is what the solution seed decides rather
than the system: it is seeded with the current positions, not with zeros.
Source code in triwarp/smoothing.py
1634 1635 1636 1637 1638 1639 1640 1641 1642 1643 1644 1645 1646 1647 1648 1649 1650 1651 1652 1653 1654 1655 1656 1657 1658 1659 1660 1661 1662 1663 1664 1665 1666 1667 1668 1669 1670 1671 1672 1673 1674 1675 1676 1677 1678 1679 1680 1681 1682 1683 1684 1685 1686 1687 1688 1689 1690 1691 1692 1693 1694 1695 1696 1697 1698 1699 1700 1701 1702 1703 1704 1705 1706 1707 1708 1709 1710 1711 1712 1713 1714 1715 1716 1717 1718 1719 1720 1721 1722 1723 1724 1725 1726 1727 1728 1729 1730 1731 1732 1733 1734 1735 1736 1737 1738 1739 1740 1741 1742 1743 1744 1745 1746 1747 1748 1749 1750 1751 1752 1753 1754 1755 1756 1757 1758 1759 1760 1761 1762 1763 1764 1765 1766 1767 1768 1769 1770 1771 1772 1773 1774 1775 1776 1777 1778 1779 | |
refine_and_smooth_region
refine_and_smooth_region(
vertices: array[vec3],
faces: array[int32],
n_vertices_before: int,
patch_face_mask: array[bool],
max_edge: float,
max_edge_splits: int,
max_angle_change_after_flip: float,
smooth_curvature: bool,
smooth_boundary: bool,
natural_smooth: bool,
edge_weights: str,
*,
refine: Literal["max_edge", "density"] = "max_edge",
) -> tuple[
wp.array[wp.vec3], wp.array[wp.int32], wp.array[wp.bool]
]
Subdivide the patch and smooth its new vertices.
Shared finisher of fill_smooth and
stitch_smooth.
refine selects the subdivision criterion, and the two are genuinely different questions
rather than two tunings of one: "max_edge" bisects region edges longer than max_edge
(remesh.subdivide_region_to_size), and
"density" splits region triangles at their centroid while their sampling is coarser than
the surrounding mesh's -- Liepa's criterion, in
remesh.refine_region_to_density. max_edge and
max_edge_splits are ignored under "density", which takes its target from the mesh
instead of from an argument.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
vertices
|
array[vec3]
|
|
required |
faces
|
array[int32]
|
Length- |
required |
n_vertices_before
|
int
|
Vertex count before the patch was appended. Everything at or above this index is a patch vertex, which is how the smooth step finds the vertices it may move. |
required |
patch_face_mask
|
array[bool]
|
|
required |
max_edge
|
float
|
Target edge length for |
required |
max_edge_splits
|
int
|
Soft cap on the number of edge splits during subdivision. Ignored under |
required |
max_angle_change_after_flip
|
float
|
Dihedral-angle-change gate for the Delaunay flip pass, in radians. |
required |
smooth_curvature
|
bool
|
When |
required |
smooth_boundary
|
bool
|
When |
required |
natural_smooth
|
bool
|
When |
required |
edge_weights
|
str
|
Laplacian edge weights for the smooth solve: |
required |
refine
|
Literal['max_edge', 'density']
|
Subdivision criterion, as above. |
'max_edge'
|
Returns:
| Name | Type | Description |
|---|---|---|
vertices |
array[vec3]
|
|
faces |
array[int32]
|
Flat |
patch_face_mask |
array[bool]
|
The patch mask carried through the refinement, one flag per face of the returned buffer -- longer than the input mask, since subdivision adds faces. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
RuntimeError
|
If |
Source code in triwarp/smoothing.py
1848 1849 1850 1851 1852 1853 1854 1855 1856 1857 1858 1859 1860 1861 1862 1863 1864 1865 1866 1867 1868 1869 1870 1871 1872 1873 1874 1875 1876 1877 1878 1879 1880 1881 1882 1883 1884 1885 1886 1887 1888 1889 1890 1891 1892 1893 1894 1895 1896 1897 1898 1899 1900 1901 1902 1903 1904 1905 1906 1907 1908 1909 1910 1911 1912 1913 1914 1915 1916 1917 1918 1919 1920 1921 1922 1923 1924 1925 1926 1927 1928 1929 1930 1931 1932 1933 1934 1935 1936 1937 1938 1939 1940 1941 1942 1943 1944 1945 1946 1947 1948 1949 1950 1951 1952 1953 1954 1955 1956 1957 1958 1959 1960 1961 1962 1963 1964 1965 1966 1967 1968 1969 1970 1971 1972 1973 1974 1975 1976 | |
smooth_region_boundary
smooth_region_boundary(
vertices: array[vec3],
faces: array[int32],
region: array[bool],
iterations: int = 4,
*,
vertex_faces: tuple[array[int32], array[int32]]
| None = None,
) -> wp.array[wp.vec3]
Straighten the rim curve of a face region, by sliding the vertices that lie on it.
The third member of the region family, and the one whose subject is the boundary itself.
smooth_region smooths the surface across the rim and
smooth_region_fixed_rim holds the rim still while
smoothing inside it; both leave the rim's own path through the mesh exactly where the face
selection put it. That path is usually ragged -- a selection by angle, by height or by a paint
stroke follows triangle edges and zigzags -- and this is what makes it a smooth curve, without
moving the surface off itself in any visible way.
The rim is smoothed as a level set rather than as a polyline. A field is pinned to -1 on
the region's vertices and +1 outside, its harmonic interpolation is solved over the band of
vertices that touch both sides, and each of those vertices is moved onto the zero level set of
the result. Harmonic interpolation is smooth, so its zero set is a smooth curve; the vertices
slide along the surface to sit on it. Repeating recomputes the field from the moved positions,
and the move is damped so the sequence settles rather than oscillates.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
vertices
|
array[vec3]
|
|
required |
faces
|
array[int32]
|
Length- |
required |
region
|
array[bool]
|
|
required |
iterations
|
int
|
Number of solve-and-project passes. |
4
|
vertex_faces
|
tuple[array[int32], array[int32]] | None
|
Optional precomputed |
None
|
Returns:
| Type | Description |
|---|---|
array[vec3]
|
Positions on |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
RuntimeError
|
If |
See Also
smooth_region
Smooths the surface across the rim, leaving the rim's path alone.
smooth_region_fixed_rim
Smooths inside the rim, holding it fixed.
region_boundary_edges
The rim as an edge list, which is what this leaves in a better place.
Notes
It slides vertices; it does not re-cut the rim
The rim can only become as smooth as the existing triangulation lets it: a vertex slides
to the level set but the rim still passes through the same vertices, so a curve that wants
to cross a triangle diagonally cannot. Retriangulating the band first
(flip_by_objective, or
split_faces_along_field to cut along the
level set outright) is the way to get past that, and is deliberately not folded in here --
it changes the face buffer, which this promises not to.
Source code in triwarp/smoothing.py
1989 1990 1991 1992 1993 1994 1995 1996 1997 1998 1999 2000 2001 2002 2003 2004 2005 2006 2007 2008 2009 2010 2011 2012 2013 2014 2015 2016 2017 2018 2019 2020 2021 2022 2023 2024 2025 2026 2027 2028 2029 2030 2031 2032 2033 2034 2035 2036 2037 2038 2039 2040 2041 2042 2043 2044 2045 2046 2047 2048 2049 2050 2051 2052 2053 2054 2055 2056 2057 2058 2059 2060 2061 2062 2063 2064 2065 2066 2067 2068 2069 2070 2071 2072 2073 2074 2075 2076 2077 2078 2079 2080 2081 2082 2083 2084 2085 2086 2087 2088 2089 2090 2091 2092 2093 2094 2095 2096 2097 2098 2099 2100 2101 2102 2103 2104 2105 2106 2107 2108 2109 2110 2111 2112 2113 2114 2115 2116 2117 2118 | |
filter_scalar_laplacian
filter_scalar_laplacian(
values: array[float32],
vertices: array[vec3],
faces: array[int32],
lamb: float = 0.5,
iterations: int = 10,
laplacian_operator: BsrMatrix[float32] | None = None,
) -> wp.array[wp.float32]
Diffuse a per-vertex scalar field through the 1-ring averaging operator.
The scalar counterpart of filter_laplacian, running the
identical explicit step q' = q + lamb (L q - q) on a field rather than on positions. Reach
for it whenever a computed per-vertex quantity is noisier than the thing it will drive —
curvature before it selects features, a heat or occlusion field before it becomes a weight, a
per-vertex sizing function before it drives remeshing.
On a closed mesh, lamb=1.0, iterations=1 is exactly MeshLab's
apply_scalar_smoothing_per_vertex: one unweighted 1-ring average with the vertex's own value
excluded. The defaults instead match the rest of this module (a partial step, repeated), which
is the gentler behaviour a caller usually wants.
It differs from MeshLab on a boundary
VCG's VertexQualityLaplacian smooths a boundary vertex along the boundary curve
only — it averages just that vertex's two boundary neighbours and ignores the rest of its
ring, so the boundary values evolve as an independent 1D field. That is a different
operator, not this one with other constants, and it is why the pymeshlab oracle in
tests/test_smoothing.py runs on closed fixtures. Here a boundary vertex averages its
whole ring like any other; to hold the boundary fixed instead, restore its values after the
call.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
values
|
array[float32]
|
Length- |
required |
vertices
|
array[vec3]
|
|
required |
faces
|
array[int32]
|
Length- |
required |
lamb
|
float
|
Step size. |
0.5
|
iterations
|
int
|
Number of passes. |
10
|
laplacian_operator
|
BsrMatrix[float32] | None
|
Optional precomputed row-stochastic operator (see
When |
None
|
Returns:
| Type | Description |
|---|---|
array[float32]
|
Length- |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
RuntimeError
|
If |
Source code in triwarp/smoothing.py
2180 2181 2182 2183 2184 2185 2186 2187 2188 2189 2190 2191 2192 2193 2194 2195 2196 2197 2198 2199 2200 2201 2202 2203 2204 2205 2206 2207 2208 2209 2210 2211 2212 2213 2214 2215 2216 2217 2218 2219 2220 2221 2222 2223 2224 2225 2226 2227 2228 2229 2230 2231 2232 2233 2234 2235 2236 2237 2238 2239 2240 2241 2242 2243 2244 2245 2246 2247 2248 2249 2250 2251 2252 2253 2254 2255 2256 2257 2258 2259 2260 2261 2262 2263 2264 2265 2266 2267 2268 2269 2270 2271 2272 2273 2274 2275 2276 2277 2278 2279 | |
filter_normals
filter_normals(
vertices: array[vec3],
faces: array[int32],
*,
iterations: int = 20,
threshold: float = 60.0,
face_adjacency: Array2dInt32 | None = None,
face_normals: array[vec3] | None = None,
face_areas: array[float32] | None = None,
) -> wp.array[wp.vec3]
Smooth the face normals without moving a vertex, preserving creases.
Each pass replaces a face's normal with the area-weighted average of its own and those of its
edge-neighbours whose normal is within threshold of it. That gate is what makes this
feature-preserving rather than isotropic: across a crease the two normals disagree by more than
the threshold and never average, so a sharp edge survives any number of passes while noise on a
flat region diffuses away in a few. MeshLab's apply_normal_smoothing_per_face.
The result is a normal field that is no longer the geometric normal of any triangle — it is the
target for filter_two_step's second half, and useful on
its own for shading.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
vertices
|
array[vec3]
|
|
required |
faces
|
array[int32]
|
Length- |
required |
iterations
|
int
|
Number of diffusion passes over the normal field. MeshLab's |
20
|
threshold
|
float
|
Angle in degrees beyond which two neighbouring faces refuse to average. |
60.0
|
face_adjacency
|
Array2dInt32 | None
|
Optional |
None
|
face_normals
|
array[vec3] | None
|
Optional length- |
None
|
face_areas
|
array[float32] | None
|
See |
None
|
Returns:
| Type | Description |
|---|---|
array[vec3]
|
Length- |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
RuntimeError
|
If |
Source code in triwarp/smoothing.py
2282 2283 2284 2285 2286 2287 2288 2289 2290 2291 2292 2293 2294 2295 2296 2297 2298 2299 2300 2301 2302 2303 2304 2305 2306 2307 2308 2309 2310 2311 2312 2313 2314 2315 2316 2317 2318 2319 2320 2321 2322 2323 2324 2325 2326 2327 2328 2329 2330 2331 2332 2333 2334 2335 2336 2337 2338 2339 2340 2341 2342 2343 2344 2345 2346 2347 2348 2349 2350 2351 2352 2353 2354 2355 2356 2357 2358 2359 2360 2361 2362 2363 2364 2365 2366 2367 2368 2369 2370 2371 2372 2373 2374 2375 2376 2377 2378 2379 2380 2381 2382 2383 2384 2385 2386 2387 2388 2389 2390 2391 2392 2393 2394 2395 2396 2397 2398 2399 2400 2401 2402 2403 2404 2405 2406 | |
filter_two_step
filter_two_step(
vertices: array[vec3],
faces: array[int32],
*,
iterations: int = 3,
threshold: float = 60.0,
normal_iterations: int = 20,
fit_iterations: int = 20,
) -> wp.array[wp.vec3]
Feature-preserving smoothing: filter the face normals, then fit the vertices to them.
The one filter in this module that does not blur a crease. Every other scheme here diffuses
positions directly, which cannot distinguish noise from a sharp edge — both are high-frequency.
Two-step smoothing (Ohtake et al., and MeshLab's apply_coord_two_steps_smoothing) separates
the two questions:
- Where should the surface face? Diffuse the normal field with
filter_normals, whose threshold refuses to average across a crease. Noise is removed; the crease is not. - Where should the vertices go? Move each vertex so the planes through its incident faces'
centroids with those filtered normals agree as well as possible — a gradient step repeated
fit_iterationstimes, with no free step size (the average over incident faces is the step).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
vertices
|
array[vec3]
|
|
required |
faces
|
array[int32]
|
Length- |
required |
iterations
|
int
|
Number of outer passes; each one re-derives the normals from the current positions and
refits. MeshLab's |
3
|
threshold
|
float
|
Crease threshold in degrees for the normal filter. See
|
60.0
|
normal_iterations
|
int
|
Normal-diffusion passes per outer pass. MeshLab's |
20
|
fit_iterations
|
int
|
Vertex-fitting gradient steps per outer pass. MeshLab's |
20
|
Returns:
| Type | Description |
|---|---|
array[vec3]
|
Smoothed |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
RuntimeError
|
If |
See Also
Notes
There is no volume constraint and none is needed: the fitting step moves each vertex along the
filtered normals rather than toward its neighbours' mean, so the systematic inward drift that
filter_laplacian has to correct for does not arise. A
flat region is already a fixed point of both halves.
Source code in triwarp/smoothing.py
2409 2410 2411 2412 2413 2414 2415 2416 2417 2418 2419 2420 2421 2422 2423 2424 2425 2426 2427 2428 2429 2430 2431 2432 2433 2434 2435 2436 2437 2438 2439 2440 2441 2442 2443 2444 2445 2446 2447 2448 2449 2450 2451 2452 2453 2454 2455 2456 2457 2458 2459 2460 2461 2462 2463 2464 2465 2466 2467 2468 2469 2470 2471 2472 2473 2474 2475 2476 2477 2478 2479 2480 2481 2482 2483 2484 2485 2486 2487 2488 2489 2490 2491 2492 2493 2494 2495 2496 2497 2498 2499 2500 2501 2502 2503 2504 2505 2506 2507 2508 | |
filter_sharpen
filter_sharpen(
vertices: array[vec3],
faces: array[int32],
*,
weight: float = 0.3,
weight_original: float = 1.0,
iterations: int = 5,
laplacian_operator: BsrMatrix[float32] | None = None,
) -> wp.array[wp.vec3]
Sharpen a surface by adding back the detail a smoothing pass removes.
The inverse of a smoothing filter, built from one: the difference between the mesh and its
Laplacian-smoothed self is its high-frequency content, so adding a multiple of that difference
exaggerates every feature. This is unsharp masking, the photographic technique, applied to
vertex positions instead of pixels — MeshLab spells it apply_coord_unsharp_mask — and it is
the standard way to recover crispness lost to an earlier smoothing or a decimation.
The name says what it returns, which is positions rather than a mask: the *_mask family
in this package is boolean throughout.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
vertices
|
array[vec3]
|
|
required |
faces
|
array[int32]
|
Length- |
required |
weight
|
float
|
How much of the detail to add back. |
0.3
|
weight_original
|
float
|
Multiplier on the original position, MeshLab's |
1.0
|
iterations
|
int
|
Laplacian passes used to define "smoothed", so this sets the scale of detail that gets
amplified: more passes remove lower frequencies and therefore sharpen more coarsely.
MeshLab's |
5
|
laplacian_operator
|
BsrMatrix[float32] | None
|
Optional precomputed operator, forwarded to
|
None
|
Returns:
| Type | Description |
|---|---|
array[vec3]
|
Sharpened |
Raises:
| Type | Description |
|---|---|
RuntimeError
|
If |
See Also
Source code in triwarp/smoothing.py
2511 2512 2513 2514 2515 2516 2517 2518 2519 2520 2521 2522 2523 2524 2525 2526 2527 2528 2529 2530 2531 2532 2533 2534 2535 2536 2537 2538 2539 2540 2541 2542 2543 2544 2545 2546 2547 2548 2549 2550 2551 2552 2553 2554 2555 2556 2557 2558 2559 2560 2561 2562 2563 2564 2565 2566 2567 2568 2569 2570 2571 2572 2573 2574 2575 2576 2577 2578 2579 2580 2581 2582 2583 2584 2585 2586 2587 2588 2589 2590 2591 | |