Compute Curvature: fix smoothing blow-up (1e12) -- use stable averaging

smoothing_iterations used explicit cotangent diffusion `f += 0.5 * Minv @ L @ f`,
which is unstable: the Laplace-Beltrami spectral radius scales like 1/edge^2, so a
fixed step diverges on fine meshes (and obtuse triangles add negative cotangent
weights) -> fields exploded to ~1e12. Replaced with row-normalized umbrella
neighbour averaging (a convex combination), unconditionally stable at any
resolution. Verified: on a fine noisy sphere, smoothing now monotonically reduces
the field variance with finite values instead of blowing up.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
PozzettiAndrea
2026-06-17 02:36:30 +02:00
co-authored by Claude Opus 4.8
parent fcacd4e807
commit 092f564feb
+17 -4
View File
@@ -78,8 +78,8 @@ class ComputeCurvatureNode(io.ComfyNode):
"Larger = smoother, more noise-robust, but blurs sharp features. ~3 for "
"crisp CAD, ~5 default, 7-8 for noisy scans. Ignored by the ddg method.")),
io.Int.Input("smoothing_iterations", default=0, min=0, max=50, step=1, tooltip=(
"Explicit cotangent-Laplacian diffusion of the curvature fields. 0 = off. "
"A few passes denoise the field without re-fitting.")),
"Laplacian (neighbour-averaging) smoothing of the curvature fields. 0 = off. "
"A few passes denoise the field without re-fitting. Stable at any resolution.")),
io.Combo.Input("clamp_to_resolution", options=["true", "false"], default="true", tooltip=(
"AUTOMATICALLY cap curvature the mesh can't actually represent. A fillet "
"finer than the local triangles is just the fit blowing up at a sharp edge "
@@ -229,13 +229,26 @@ class ComputeCurvatureNode(io.ComfyNode):
"curvature_shape_index": shape_index,
}
# Optional explicit Laplacian diffusion of each scalar field.
# Optional Laplacian smoothing of each scalar field. Row-normalized (umbrella)
# neighbour averaging -- a CONVEX combination, so it's unconditionally stable at
# any mesh resolution. (The previous explicit cotangent diffusion
# `f += 0.5 * Minv @ L @ f` blew up to ~1e12: the Laplace-Beltrami spectral
# radius scales like 1/edge^2, so a fixed step diverges on fine meshes, and
# obtuse triangles add negative cotangent weights.)
if smoothing_iterations > 0:
ev = np.asarray(mesh.edges_unique)
deg = np.zeros(n, dtype=np.float64)
np.add.at(deg, ev[:, 0], 1.0)
np.add.at(deg, ev[:, 1], 1.0)
deg = np.maximum(deg, 1.0)
lam = 0.5
for name in list(fields.keys()):
f = np.asarray(fields[name], dtype=np.float64).copy()
for _ in range(int(smoothing_iterations)):
f = f + lam * np.asarray(Minv @ (L @ f))
nb = np.zeros(n, dtype=np.float64)
np.add.at(nb, ev[:, 0], f[ev[:, 1]])
np.add.at(nb, ev[:, 1], f[ev[:, 0]])
f = (1.0 - lam) * f + lam * (nb / deg)
fields[name] = f
# Optional percentile clamp so slivers don't dominate the range.