Sharpen Guided Normal: add neighborhood_rings knob

Exposes the face-neighborhood (k-ring) size used for both guidance and
filtering, previously hardcoded to the vertex 1-ring. Higher rings widen the
filter footprint so each pass hits harder / reaches farther -- a genuinely
stronger lever than just adding iterations. _build_vertex_based_face_neighbors
grows the patch by ring expansion (rings=1 reproduces the old 1-ring exactly, so
existing callers are unchanged). Wired through CPU + GPU paths and the dispatcher
(default 1, max 4; cost/memory grow ~rings^2).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
PozzettiAndrea
2026-06-16 20:57:40 +02:00
co-authored by Claude Opus 4.8
parent 57e09d6c43
commit 7fac4f40ba
4 changed files with 51 additions and 17 deletions
+7
View File
@@ -167,6 +167,13 @@ class SharpenMeshNode(io.ComfyNode):
"Iterations for updating vertex positions to match filtered "
"normals. More iterations give better convergence."
)),
io.Int.Input("neighborhood_rings", default=1, min=1, max=4, step=1, tooltip=(
"Face neighborhood size (k-ring) for guidance + filtering. "
"1 = faces sharing a vertex (standard). Higher = wider footprint, "
"so each pass hits HARDER / reaches farther (stronger than just "
"more iterations) -- but cost & memory grow ~rings^2. Try 2 for a "
"noticeably stronger effect."
)),
io.Float.Input("sigma_s", default=1.0, min=0.1, max=10.0, step=0.1, tooltip=(
"SPATIAL scale = neighborhood size as a multiple of the average "
"edge length. How far (in surface distance) a neighbor face still "
+26 -10
View File
@@ -93,20 +93,36 @@ def _build_vertex_to_faces(n_verts, F):
return vtf
def _build_vertex_based_face_neighbors(F, vert_to_faces, include_central=True):
"""Build vertex-based face neighbor lists.
def _build_vertex_based_face_neighbors(F, vert_to_faces, include_central=True, rings=1):
"""Build vertex-based face neighbor lists (k-ring patches).
Two faces are neighbors if they share at least one vertex.
Returns list of sorted index lists, one per face.
Two faces are 1-ring neighbors if they share at least one vertex. With
rings>1 the patch is grown outward `rings` times: each step adds every face
touching a vertex of the faces added in the previous step. Larger rings =
wider filtering footprint (stronger per-pass effect, more cost).
Returns a list of sorted index lists, one per face.
"""
m = len(F)
rings = max(1, int(rings))
neighbors = []
for fi in range(m):
nbrs = set()
for vi in F[fi]:
for fj in vert_to_faces[vi]:
nbrs.add(fj)
face_set = {fi}
frontier_verts = set(int(v) for v in F[fi])
for _ in range(rings):
new_faces = set()
for vi in frontier_verts:
for fj in vert_to_faces[vi]:
if fj not in face_set:
new_faces.add(fj)
if not new_faces:
break
face_set |= new_faces
# Next frontier = vertices of the newly added faces.
frontier_verts = set()
for fj in new_faces:
for vi in F[fj]:
frontier_verts.add(int(vi))
if not include_central:
nbrs.discard(fi)
neighbors.append(sorted(nbrs))
face_set.discard(fi)
neighbors.append(sorted(face_set))
return neighbors
@@ -18,7 +18,7 @@ log = logging.getLogger("geometrypack")
def _guided_normal_sharpen(mesh, normal_iterations, vertex_iterations,
sigma_s, sigma_r):
sigma_s, sigma_r, neighborhood_rings=1):
"""Guided mesh normal filtering with interleaved vertex update.
Matches GuidedMeshNormalFiltering::updateFilteredNormalsLocalScheme from
@@ -47,7 +47,7 @@ def _guided_normal_sharpen(mesh, normal_iterations, vertex_iterations,
# Build vertex-based face neighbor structures
vert_to_faces = _build_vertex_to_faces(len(V), F)
# Guided neighborhood: vertex-based including central face
guided_neighbors = _build_vertex_based_face_neighbors(F, vert_to_faces, include_central=True)
guided_neighbors = _build_vertex_based_face_neighbors(F, vert_to_faces, include_central=True, rings=neighborhood_rings)
# Filtering neighborhood: same vertex-based including central
filter_neighbors = guided_neighbors
@@ -196,6 +196,13 @@ class SharpenGuidedNormalNode(io.ComfyNode):
"Iterations for updating vertex positions to match filtered "
"normals. More iterations give better convergence."
)),
io.Int.Input("neighborhood_rings", default=1, min=1, max=4, step=1, tooltip=(
"Size of the face neighborhood (k-ring) used for both guidance and "
"filtering. 1 = faces sharing a vertex (standard). Higher = wider "
"footprint, so each pass hits HARDER and reaches farther (stronger "
"than just adding iterations) -- but cost and memory grow quickly "
"(patch size ~ rings^2). Try 2 for a noticeably stronger effect."
)),
io.Float.Input("sigma_s", default=1.0, min=0.1, max=10.0, step=0.1, tooltip=(
"SPATIAL scale = neighborhood size as a multiple of the average edge "
"length. Controls how far (in surface distance) a neighbor face still "
@@ -227,7 +234,7 @@ class SharpenGuidedNormalNode(io.ComfyNode):
@classmethod
def execute(cls, trimesh, normal_iterations=5, vertex_iterations=10,
sigma_s=1.0, sigma_r_degrees=20.0, use_gpu="false"):
neighborhood_rings=1, sigma_s=1.0, sigma_r_degrees=20.0, use_gpu="false"):
import math
gpu = (use_gpu == "true")
algorithm = "guided_normal_gpu" if gpu else "guided_normal"
@@ -237,8 +244,8 @@ class SharpenGuidedNormalNode(io.ComfyNode):
sigma_r = 2.0 * math.sin(math.radians(sigma_r_degrees) / 2.0)
log.info("Backend: %s", algorithm)
log.info("Input: %d vertices, %d faces", len(trimesh.vertices), len(trimesh.faces))
log.info("Parameters: normal_iter=%d, vertex_iter=%d, sigma_s=%.2f, sigma_r=%.3f (%.1f deg), use_gpu=%s",
normal_iterations, vertex_iterations, sigma_s, sigma_r, sigma_r_degrees, use_gpu)
log.info("Parameters: normal_iter=%d, vertex_iter=%d, rings=%d, sigma_s=%.2f, sigma_r=%.3f (%.1f deg), use_gpu=%s",
normal_iterations, vertex_iterations, neighborhood_rings, sigma_s, sigma_r, sigma_r_degrees, use_gpu)
initial_vertices = len(trimesh.vertices)
initial_faces = len(trimesh.faces)
@@ -248,10 +255,12 @@ class SharpenGuidedNormalNode(io.ComfyNode):
from .guided_normal_gpu import _guided_normal_gpu
sharpened, error, device = _guided_normal_gpu(
trimesh, normal_iterations, vertex_iterations, sigma_s, sigma_r,
neighborhood_rings=neighborhood_rings,
)
else:
sharpened, error = _guided_normal_sharpen(
trimesh, normal_iterations, vertex_iterations, sigma_s, sigma_r,
neighborhood_rings=neighborhood_rings,
)
if sharpened is None:
@@ -281,6 +290,7 @@ class SharpenGuidedNormalNode(io.ComfyNode):
param_text = (
f"Normal Iterations: {normal_iterations}\n"
f"Vertex Iterations: {vertex_iterations}\n"
f"Neighborhood Rings: {neighborhood_rings}\n"
f"Sigma S: {sigma_s}\n"
f"Sigma R: {sigma_r_degrees} deg (= {sigma_r:.3f} normal-dist)\n"
f"Use GPU: {use_gpu}"
@@ -85,7 +85,8 @@ def _pad_inner_edges(neighbors, adj_pairs, face_to_adj, m):
return IE, IEM
def _guided_normal_gpu(mesh, normal_iterations, vertex_iterations, sigma_s, sigma_r):
def _guided_normal_gpu(mesh, normal_iterations, vertex_iterations, sigma_s, sigma_r,
neighborhood_rings=1):
import torch
V0 = np.asarray(mesh.vertices, dtype=np.float64)
@@ -101,7 +102,7 @@ def _guided_normal_gpu(mesh, normal_iterations, vertex_iterations, sigma_s, sigm
# --- one-time topology (CPU), constant across iterations ---
vtf = _build_vertex_to_faces(n, Ff)
neighbors = _build_vertex_based_face_neighbors(Ff, vtf, include_central=True)
neighbors = _build_vertex_based_face_neighbors(Ff, vtf, include_central=True, rings=neighborhood_rings)
face_to_adj = [[] for _ in range(m)]
for ai in range(len(adj)):
fa, fb = adj[ai]