blender more, reqs
This commit is contained in:
@@ -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,
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user