diff --git a/nodes/smoothing/sharpen.py b/nodes/smoothing/sharpen.py index d6ad2171..ed3ded82 100644 --- a/nodes/smoothing/sharpen.py +++ b/nodes/smoothing/sharpen.py @@ -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 " diff --git a/nodes/smoothing/sharpen_backends/_helpers.py b/nodes/smoothing/sharpen_backends/_helpers.py index bb2dadab..858c010e 100644 --- a/nodes/smoothing/sharpen_backends/_helpers.py +++ b/nodes/smoothing/sharpen_backends/_helpers.py @@ -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 diff --git a/nodes/smoothing/sharpen_backends/guided_normal.py b/nodes/smoothing/sharpen_backends/guided_normal.py index ab49654a..ef53804f 100644 --- a/nodes/smoothing/sharpen_backends/guided_normal.py +++ b/nodes/smoothing/sharpen_backends/guided_normal.py @@ -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}" diff --git a/nodes/smoothing/sharpen_backends/guided_normal_gpu.py b/nodes/smoothing/sharpen_backends/guided_normal_gpu.py index 4dc69841..5fecff36 100644 --- a/nodes/smoothing/sharpen_backends/guided_normal_gpu.py +++ b/nodes/smoothing/sharpen_backends/guided_normal_gpu.py @@ -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]