From 86c900ee58edb4e42d44d4539f9fec452b40f91c Mon Sep 17 00:00:00 2001 From: PozzettiAndrea Date: Fri, 13 Feb 2026 01:24:40 +0100 Subject: [PATCH] blender more, reqs --- nodes/blender/remeshing/remesh.py | 283 +++++++++++++++++++++++++----- requirements.txt | 2 +- 2 files changed, 239 insertions(+), 46 deletions(-) diff --git a/nodes/blender/remeshing/remesh.py b/nodes/blender/remeshing/remesh.py index 9edde9c0..c4aebf05 100644 --- a/nodes/blender/remeshing/remesh.py +++ b/nodes/blender/remeshing/remesh.py @@ -2,7 +2,8 @@ # Copyright (C) 2025 ComfyUI-GeometryPack Contributors """ -Remesh Blender Node - Blender voxel and quadriflow remeshing +Remesh Blender Node - Blender remeshing backends using bpy. +Supports: voxel, quadriflow, smooth, sharp, blocks. Requires bpy (Blender Python module). """ @@ -10,63 +11,154 @@ import numpy as np import trimesh as trimesh_module +def _bpy_setup_object(vertices, faces): + """Create a Blender mesh object from vertices and faces. Returns (obj, mesh).""" + import bpy + + mesh = bpy.data.meshes.new("RemeshMesh") + obj = bpy.data.objects.new("RemeshObject", mesh) + bpy.context.collection.objects.link(obj) + + # Deselect everything first (default scene has a Cube selected) + bpy.ops.object.select_all(action='DESELECT') + + bpy.context.view_layer.objects.active = obj + obj.select_set(True) + + mesh.from_pydata(vertices.tolist(), [], faces.tolist()) + mesh.update() + + return obj, mesh + + +def _bpy_extract_and_cleanup(obj): + """Extract vertices/faces from object, then delete it. Triangulates quads/n-gons.""" + import bpy + + mesh = obj.data + result_vertices = [list(v.co) for v in mesh.vertices] + + # Triangulate polygons with >3 verts (Quadriflow produces quads) + result_faces = [] + for p in mesh.polygons: + verts = list(p.vertices) + if len(verts) == 3: + result_faces.append(verts) + elif len(verts) == 4: + # Split quad into 2 triangles + result_faces.append([verts[0], verts[1], verts[2]]) + result_faces.append([verts[0], verts[2], verts[3]]) + else: + # Fan triangulation for n-gons + for i in range(1, len(verts) - 1): + result_faces.append([verts[0], verts[i], verts[i + 1]]) + + bpy.data.objects.remove(obj, do_unlink=True) + bpy.data.meshes.remove(mesh) + + return {'vertices': result_vertices, 'faces': result_faces} + + def _bpy_voxel_remesh(vertices, faces, voxel_size): """Blender voxel remesh using bpy.""" import bpy - mesh = bpy.data.meshes.new("RemeshMesh") - obj = bpy.data.objects.new("RemeshObject", mesh) - bpy.context.collection.objects.link(obj) - bpy.context.view_layer.objects.active = obj - obj.select_set(True) - - mesh.from_pydata(vertices.tolist(), [], faces.tolist()) - mesh.update() + obj, mesh = _bpy_setup_object(vertices, faces) obj.data.remesh_voxel_size = voxel_size bpy.ops.object.voxel_remesh() - mesh = obj.data - result_vertices = [list(v.co) for v in mesh.vertices] - result_faces = [list(p.vertices) for p in mesh.polygons] - - bpy.data.objects.remove(obj, do_unlink=True) - bpy.data.meshes.remove(mesh) - - return {'vertices': result_vertices, 'faces': result_faces} + return _bpy_extract_and_cleanup(obj) def _bpy_quadriflow_remesh(vertices, faces, target_face_count): - """Blender Quadriflow remesh using bpy.""" + """Blender Quadriflow remesh using bpy with context override for headless.""" + import bpy + import traceback + + obj, mesh = _bpy_setup_object(vertices, faces) + + # Quadriflow operator needs proper context — use temp_override for headless + window = bpy.context.window + screen = window.screen + area = next((a for a in screen.areas if a.type == 'VIEW_3D'), None) + + print(f"[Remesh Blender] Quadriflow debug: areas={[a.type for a in screen.areas]}") + + if area is None: + # Headless: no VIEW_3D area exists, override the first area + area = screen.areas[0] + original_type = area.type + area.type = 'VIEW_3D' + print(f"[Remesh Blender] Quadriflow debug: changed area from {original_type} to VIEW_3D") + else: + original_type = None + + region = next((r for r in area.regions if r.type == 'WINDOW'), area.regions[0]) + print(f"[Remesh Blender] Quadriflow debug: area.type={area.type}, region.type={region.type}") + + # Check poll before calling + try: + with bpy.context.temp_override(window=window, area=area, region=region): + poll_ok = bpy.ops.object.quadriflow_remesh.poll() + print(f"[Remesh Blender] Quadriflow debug: poll={poll_ok}") + print(f"[Remesh Blender] Quadriflow debug: active_object={bpy.context.active_object}") + print(f"[Remesh Blender] Quadriflow debug: selected_objects={bpy.context.selected_objects}") + except Exception as e: + print(f"[Remesh Blender] Quadriflow debug: poll check failed: {e}") + + # Clean mesh before quadriflow (fixes CANCELLED on non-manifold/degenerate meshes) + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.mesh.select_all(action='SELECT') + bpy.ops.mesh.delete_loose() + bpy.ops.mesh.dissolve_degenerate() + bpy.ops.mesh.normals_make_consistent(inside=False) + bpy.ops.object.mode_set(mode='OBJECT') + print(f"[Remesh Blender] Quadriflow debug: mesh cleaned") + + result = {'CANCELLED'} + try: + with bpy.context.temp_override(window=window, area=area, region=region): + result = bpy.ops.object.quadriflow_remesh( + 'EXEC_DEFAULT', + use_mesh_symmetry=False, + use_preserve_sharp=False, + use_preserve_boundary=True, + smooth_normals=False, + mode='FACES', + target_faces=target_face_count, + seed=0 + ) + print(f"[Remesh Blender] Quadriflow result: {result}") + except Exception as e: + print(f"[Remesh Blender] Quadriflow EXCEPTION: {e}") + traceback.print_exc() + finally: + if original_type is not None: + area.type = original_type + + if result != {'FINISHED'}: + print(f"[Remesh Blender] Quadriflow FAILED with result={result}") + + return _bpy_extract_and_cleanup(obj) + + +def _bpy_remesh_modifier(vertices, faces, mode, octree_depth=6, scale=0.9, sharpness=1.0): + """Blender Remesh Modifier (Smooth/Sharp/Blocks) using bpy.""" import bpy - mesh = bpy.data.meshes.new("RemeshMesh") - obj = bpy.data.objects.new("RemeshObject", mesh) - bpy.context.collection.objects.link(obj) - bpy.context.view_layer.objects.active = obj - obj.select_set(True) + obj, mesh = _bpy_setup_object(vertices, faces) - mesh.from_pydata(vertices.tolist(), [], faces.tolist()) - mesh.update() + mod = obj.modifiers.new(name="Remesh", type='REMESH') + mod.mode = mode # 'SMOOTH', 'SHARP', or 'BLOCKS' + mod.octree_depth = octree_depth + mod.scale = scale + if mode == 'SHARP': + mod.sharpness = sharpness - bpy.ops.object.quadriflow_remesh( - use_mesh_symmetry=False, - use_preserve_sharp=False, - use_preserve_boundary=False, - smooth_normals=False, - mode='FACES', - target_faces=target_face_count, - seed=0 - ) + bpy.ops.object.modifier_apply(modifier="Remesh") - mesh = obj.data - result_vertices = [list(v.co) for v in mesh.vertices] - result_faces = [list(p.vertices) for p in mesh.polygons] - - bpy.data.objects.remove(obj, do_unlink=True) - bpy.data.meshes.remove(mesh) - - return {'vertices': result_vertices, 'faces': result_faces} + return _bpy_extract_and_cleanup(obj) class RemeshBlenderNode: @@ -76,6 +168,9 @@ class RemeshBlenderNode: Available backends: - blender_voxel: Voxel-based remeshing (watertight output) - blender_quadriflow: Quadriflow quad remeshing + - blender_smooth: Smooth remesh modifier + - blender_sharp: Sharp remesh modifier (preserves edges) + - blender_blocks: Blocky remesh modifier Requires bpy (Blender Python module) to be installed. """ @@ -88,9 +183,12 @@ class RemeshBlenderNode: "backend": ([ "blender_voxel", "blender_quadriflow", + "blender_smooth", + "blender_sharp", + "blender_blocks", ], { "default": "blender_voxel", - "tooltip": "Remeshing algorithm. blender_voxel=watertight output, blender_quadriflow=quad remesh" + "tooltip": "Remeshing algorithm. voxel=watertight, quadriflow=quad remesh, smooth/sharp/blocks=modifier-based" }), }, "optional": { @@ -113,6 +211,33 @@ class RemeshBlenderNode: "tooltip": "Target number of output faces for quadriflow backend.", "visible_when": {"backend": ["blender_quadriflow"]}, }), + # Modifier-based (Smooth/Sharp/Blocks) + "octree_depth": ("INT", { + "default": 6, + "min": 1, + "max": 10, + "step": 1, + "tooltip": "Resolution of the remesh. Higher = more detail, more faces.", + "visible_when": {"backend": ["blender_smooth", "blender_sharp", "blender_blocks"]}, + }), + "scale": ("FLOAT", { + "default": 0.9, + "min": 0.0, + "max": 1.0, + "step": 0.05, + "display": "number", + "tooltip": "Ratio of output size to input bounding box.", + "visible_when": {"backend": ["blender_smooth", "blender_sharp", "blender_blocks"]}, + }), + "sharpness": ("FLOAT", { + "default": 1.0, + "min": 0.0, + "max": 5.0, + "step": 0.1, + "display": "number", + "tooltip": "Edge sharpness for Sharp mode.", + "visible_when": {"backend": ["blender_sharp"]}, + }), } } @@ -122,8 +247,16 @@ class RemeshBlenderNode: CATEGORY = "geompack/remeshing" OUTPUT_NODE = True - def remesh(self, trimesh, backend, voxel_size=1.0, target_face_count=500000): + def remesh(self, trimesh, backend, voxel_size=1.0, target_face_count=500000, + octree_depth=6, scale=0.9, sharpness=1.0): """Apply Blender-based remeshing.""" + # Sanitize hidden widget values (ComfyUI sends '' for hidden visible_when widgets) + voxel_size = float(voxel_size) if voxel_size not in (None, '') else 1.0 + target_face_count = int(target_face_count) if target_face_count not in (None, '') else 500000 + octree_depth = int(octree_depth) if octree_depth not in (None, '') else 6 + scale = float(scale) if scale not in (None, '') else 0.9 + sharpness = float(sharpness) if sharpness not in (None, '') else 1.0 + initial_vertices = len(trimesh.vertices) initial_faces = len(trimesh.faces) @@ -137,6 +270,11 @@ class RemeshBlenderNode: elif backend == "blender_quadriflow": print(f"[Remesh Blender] Parameters: target_face_count={target_face_count:,}") remeshed_mesh, info = self._blender_quadriflow(trimesh, target_face_count) + elif backend in ("blender_smooth", "blender_sharp", "blender_blocks"): + mode = backend.replace("blender_", "").upper() + print(f"[Remesh Blender] Parameters: mode={mode}, octree_depth={octree_depth}, scale={scale}" + + (f", sharpness={sharpness}" if backend == "blender_sharp" else "")) + remeshed_mesh, info = self._blender_modifier(trimesh, mode, octree_depth, scale, sharpness) else: raise ValueError(f"Unknown backend: {backend}") @@ -211,6 +349,14 @@ After: 'original_faces': len(trimesh.faces) } + changed = (len(remeshed_mesh.vertices) != len(trimesh.vertices) or + len(remeshed_mesh.faces) != len(trimesh.faces)) + if changed: + status = "" + else: + status = ("\nWARNING: Mesh unchanged — Quadriflow operator likely failed (CANCELLED).\n" + "This is a known issue with bpy headless. Check server logs for debug info.\n") + info = f"""Remesh Results (Blender Quadriflow): Target Face Count: {target_face_count:,} @@ -223,11 +369,58 @@ Before: After: Vertices: {len(remeshed_mesh.vertices):,} Faces: {len(remeshed_mesh.faces):,} - +{status} Quadriflow creates quad-dominant meshes with good topology. """ return remeshed_mesh, info + def _blender_modifier(self, trimesh, mode, octree_depth, scale, sharpness): + """Blender Remesh Modifier (Smooth/Sharp/Blocks).""" + print(f"[Remesh Blender] Running Blender Remesh Modifier (mode={mode}, depth={octree_depth})...") + result = _bpy_remesh_modifier( + vertices=np.asarray(trimesh.vertices, dtype=np.float32), + faces=np.asarray(trimesh.faces, dtype=np.int32), + mode=mode, + octree_depth=octree_depth, + scale=scale, + sharpness=sharpness + ) + + remeshed_mesh = trimesh_module.Trimesh( + vertices=np.array(result['vertices'], dtype=np.float32), + faces=np.array(result['faces'], dtype=np.int32), + process=False + ) + + remeshed_mesh.metadata = trimesh.metadata.copy() + remeshed_mesh.metadata['remeshing'] = { + 'algorithm': f'blender_{mode.lower()}', + 'octree_depth': octree_depth, + 'scale': scale, + 'original_vertices': len(trimesh.vertices), + 'original_faces': len(trimesh.faces) + } + + mode_label = mode.capitalize() + params = f"Octree Depth: {octree_depth}\nScale: {scale}" + if mode == 'SHARP': + params += f"\nSharpness: {sharpness}" + + info = f"""Remesh Results (Blender {mode_label}): + +{params} +Method: bpy (Remesh Modifier) + +Before: + Vertices: {len(trimesh.vertices):,} + Faces: {len(trimesh.faces):,} + +After: + Vertices: {len(remeshed_mesh.vertices):,} + Faces: {len(remeshed_mesh.faces):,} +""" + return remeshed_mesh, info + NODE_CLASS_MAPPINGS = { "GeomPackRemeshBlender": RemeshBlenderNode, diff --git a/requirements.txt b/requirements.txt index 5569f481..2db280b8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ # Main process deps (prestartup + lightweight shared libs) -comfy-env==0.1.79 +comfy-env==0.1.80 comfy-dynamic-widgets==0.1.4 comfy-3d-viewers==0.2.12 numpy