Files

417 lines
19 KiB
Python

# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
ComfyUI Mesh Simplifier Node
This node uses PyMeshLab for mesh simplification with texture preservation.
PyMeshLab is released under the GPL License.
PyMeshLab Copyright:
PyMeshLab
All rights reserved.
VCGLib http://www.vcglib.net o o
Visual and Computer Graphics Library o o
_ O _
Paolo Cignoni \/)\/
Visual Computing Lab http://vcg.isti.cnr.it /\/|
ISTI - Italian National Research Council |
Copyright(C) 2020 \
"""
import os
import sys
import tempfile
import time
import numpy as np
import trimesh
import pymeshlab
import torch
# Node information
MANIFEST = {
"name": "ComfyUI-Mesh-Simplifier",
"version": (0, 1, 0),
"author": "roundyyy",
"project": "https://github.com/roundyyy/comfyui-mesh-simplifier",
"description": "A node for ComfyUI that simplifies 3D meshes with texture preservation using PyMeshLab",
}
class MeshSimplifierNode:
"""
ComfyUI node that simplifies 3D meshes using PyMeshLab's Quadric Edge Collapse Decimation algorithm.
Takes a mesh input from ComfyUI-3D-Pack and returns a simplified mesh with texture preservation.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"mesh": ("MESH",), # Input mesh from Comfy3D
"simplify_method": (["target_faces", "percentage_reduction"], {"default": "target_faces"}),
"target_faces": ("INT", {"default": 1000, "min": 10, "max": 1000000, "step": 100}),
"percentage_reduction": ("FLOAT", {"default": 0.75, "min": 0.01, "max": 0.99, "step": 0.01}),
"quality_threshold": ("FLOAT", {"default": 0.5, "min": 0.1, "max": 1.0, "step": 0.1}),
"texture_weight": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 2.0, "step": 0.1}),
"preserve_boundary": (["True", "False"], {"default": "True"}),
"boundary_weight": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 2.0, "step": 0.1}),
"optimal_position": (["True", "False"], {"default": "True"}),
"preserve_normal": (["True", "False"], {"default": "True"}),
"planar_simplification": (["True", "False"], {"default": "True"}),
"pre_clean": (["True", "False"], {"default": "True"}),
}
}
RETURN_TYPES = ("MESH",)
RETURN_NAMES = ("simplified_mesh",)
FUNCTION = "simplify_mesh"
CATEGORY = "3D/Mesh"
DESCRIPTION = """Simplifies 3D meshes with texture preservation using PyMeshLab's Quadric Edge Collapse Decimation algorithm.
This node allows you to reduce the complexity of 3D meshes while preserving visual quality and textures.
Works with any mesh from ComfyUI-3D-Pack nodes (StableFast3D, Hunyuan3D, etc.).
- Simplify by target face count or percentage reduction
- Preserves textures and UV maps when possible
- Pre-cleans meshes to fix common issues
- Compatible with textured and non-textured meshes
When using this tool in academic projects, please cite PyMeshLab:
@software{pymeshlab, author={Alessandro Muntoni and Paolo Cignoni}, title={{PyMeshLab}},
month=jan, year=2021, publisher={Zenodo}, doi={10.5281/zenodo.4438750}}
"""
def _bool_str_to_bool(self, bool_str):
"""Convert string bool representation to actual boolean"""
return bool_str == "True"
def simplify_mesh(self, mesh, simplify_method, target_faces, percentage_reduction,
quality_threshold, texture_weight, preserve_boundary, boundary_weight,
optimal_position, preserve_normal, planar_simplification, pre_clean):
"""
Simplify the input mesh using PyMeshLab through temporary OBJ files.
Args:
mesh: ComfyUI/Comfy3D mesh object
Other parameters: Correspond to simplification parameters
Returns:
simplified_mesh: The simplified mesh in a format compatible with ComfyUI-3D-Pack
"""
# Convert string representations of booleans to actual booleans
preserve_boundary = self._bool_str_to_bool(preserve_boundary)
optimal_position = self._bool_str_to_bool(optimal_position)
preserve_normal = self._bool_str_to_bool(preserve_normal)
planar_simplification = self._bool_str_to_bool(planar_simplification)
pre_clean = self._bool_str_to_bool(pre_clean)
# Set target faces or percentage reduction based on simplify_method
if simplify_method == "target_faces":
target_faces_val = int(target_faces)
percentage_reduction_val = None
else: # percentage_reduction
target_faces_val = None
percentage_reduction_val = float(percentage_reduction)
# Create temporary OBJ files
with tempfile.NamedTemporaryFile(suffix='.obj', delete=False) as tmp:
input_obj = tmp.name
with tempfile.NamedTemporaryFile(suffix='.obj', delete=False) as tmp:
output_obj = tmp.name
try:
# Print mesh stats before simplification
print(f"Original mesh: {len(mesh.v):,} vertices, {len(mesh.f):,} faces")
has_texture = hasattr(mesh, 'vt') and mesh.vt is not None and mesh.ft is not None
if has_texture:
print(f"Mesh has texture coordinates: {len(mesh.vt):,} texture vertices")
else:
print("Mesh does not have texture coordinates")
# Save the mesh to OBJ format
print(f"Saving mesh to temporary OBJ file: {input_obj}")
mesh.write_obj(input_obj)
# Simplify the mesh
print(f"Simplifying mesh...")
if has_texture:
# Use texture-preserving simplification for meshes with textures
print("Using texture-preserving simplification")
self._simplify_with_texture(
input_obj,
output_obj,
target_faces_val,
percentage_reduction_val,
quality_threshold,
texture_weight,
preserve_boundary,
boundary_weight,
optimal_position,
preserve_normal,
planar_simplification,
pre_clean
)
else:
# Use standard simplification for meshes without textures
print("Using standard simplification without texture preservation")
self._simplify_without_texture(
input_obj,
output_obj,
target_faces_val,
percentage_reduction_val,
quality_threshold,
preserve_boundary,
boundary_weight,
optimal_position,
preserve_normal,
planar_simplification,
pre_clean
)
# Load the simplified mesh back into ComfyUI-3D-Pack format
print(f"Loading simplified mesh from temporary OBJ file: {output_obj}")
simplified_mesh = type(mesh).load(output_obj, resize=False, renormal=True, retex=False)
# Copy any attributes from the original mesh that might not have been saved to OBJ
for attr in ['device', 'ori_center', 'ori_scale']:
if hasattr(mesh, attr):
setattr(simplified_mesh, attr, getattr(mesh, attr))
# Transfer texture from original mesh if available
if hasattr(mesh, 'albedo') and mesh.albedo is not None:
simplified_mesh.albedo = mesh.albedo
print("Transferred texture from original mesh")
# Transfer metallic-roughness map if available
if hasattr(mesh, 'metallicRoughness') and mesh.metallicRoughness is not None:
simplified_mesh.metallicRoughness = mesh.metallicRoughness
print("Transferred metallic-roughness map from original mesh")
# Make sure we're using the same device as the original mesh
if hasattr(mesh, 'device'):
simplified_mesh = simplified_mesh.to(mesh.device)
# Print mesh stats after simplification
print(f"Simplified mesh: {len(simplified_mesh.v):,} vertices, {len(simplified_mesh.f):,} faces")
if hasattr(simplified_mesh, 'vt') and simplified_mesh.vt is not None:
print(f"Simplified mesh has texture coordinates: {len(simplified_mesh.vt):,} texture vertices")
return (simplified_mesh,)
finally:
# Clean up the temporary files
if os.path.exists(input_obj):
os.remove(input_obj)
if os.path.exists(output_obj):
os.remove(output_obj)
def _simplify_with_texture(self, input_path, output_path, target_faces, percentage_reduction,
quality_threshold, texture_weight, preserve_boundary, boundary_weight,
optimal_position, preserve_normal, planar_simplification, pre_clean):
"""
Simplify a mesh with texture preservation.
"""
# Create a MeshSet
ms = pymeshlab.MeshSet()
# Load the mesh
ms.load_new_mesh(input_path)
# Get current face count for calculation
current_faces = ms.current_mesh().face_number()
current_vertices = ms.current_mesh().vertex_number()
print(f"Loaded mesh: {current_vertices:,} vertices, {current_faces:,} faces")
# Pre-processing step to clean the mesh
if pre_clean:
print(f"Performing pre-processing cleaning operations...")
# Merge close vertices (helps with many common mesh issues)
ms.apply_filter('meshing_merge_close_vertices')
# Remove unreferenced vertices
ms.apply_filter('meshing_remove_unreferenced_vertices')
# Remove duplicate faces if they exist
ms.apply_filter('meshing_remove_duplicate_faces')
# Update counts after pre-processing
cleaned_vertices = ms.current_mesh().vertex_number()
cleaned_faces = ms.current_mesh().face_number()
# Report the effect of pre-processing
vertices_removed = current_vertices - cleaned_vertices
faces_removed = current_faces - cleaned_faces
if vertices_removed > 0 or faces_removed > 0:
print(f"Pre-processing removed {vertices_removed:,} vertices and {faces_removed:,} faces")
print(f"Mesh after cleaning: {cleaned_vertices:,} vertices, {cleaned_faces:,} faces")
else:
print("Pre-processing complete. No issues found in the mesh.")
# Update current faces count for target calculation
current_faces = cleaned_faces
# Calculate target face count
if target_faces is not None and target_faces > 0:
targetfacenum = int(target_faces)
elif percentage_reduction is not None and 0.0 <= percentage_reduction <= 1.0:
targetfacenum = int(current_faces * (1.0 - percentage_reduction))
else:
# Default case
targetfacenum = int(target_faces) if target_faces is not None else 1000
# Ensure we don't go below a minimum number of faces
targetfacenum = max(4, targetfacenum)
print(f"Starting mesh simplification (current: {current_faces:,} faces, target: {targetfacenum:,} faces)...")
start_time = time.time()
try:
# Use the texture-preserving filter
ms.apply_filter('meshing_decimation_quadric_edge_collapse_with_texture',
targetfacenum=targetfacenum,
qualitythr=float(quality_threshold),
extratcoordw=float(texture_weight),
preserveboundary=preserve_boundary,
boundaryweight=float(boundary_weight),
optimalplacement=optimal_position,
preservenormal=preserve_normal,
planarquadric=planar_simplification)
# Optional: Quality improvement as post-processing
ms.apply_filter('meshing_edge_flip_by_planar_optimization',
planartype='area/max side',
pthreshold=1.0,
iterations=2)
except Exception as e:
print(f"Warning: Texture simplification failed with error: {e}")
print("Falling back to standard simplification...")
# Fall back to standard simplification if texture simplification fails
self._simplify_without_texture(
input_path,
output_path,
target_faces,
percentage_reduction,
quality_threshold,
preserve_boundary,
boundary_weight,
optimal_position,
preserve_normal,
planar_simplification,
False # Don't do pre-clean again
)
return
elapsed = time.time() - start_time
new_faces = ms.current_mesh().face_number()
reduction_percent = ((current_faces - new_faces) / current_faces) * 100
print(f"Mesh simplification completed in {elapsed:.2f} seconds.")
print(f"Reduced from {current_faces:,} to {new_faces:,} faces ({reduction_percent:.1f}% reduction)")
# Save the mesh
ms.save_current_mesh(output_path)
def _simplify_without_texture(self, input_path, output_path, target_faces, percentage_reduction,
quality_threshold, preserve_boundary, boundary_weight,
optimal_position, preserve_normal, planar_simplification, pre_clean):
"""
Simplify a mesh without texture preservation.
"""
# Create a MeshSet
ms = pymeshlab.MeshSet()
# Load the mesh
ms.load_new_mesh(input_path)
# Get current face count for calculation
current_faces = ms.current_mesh().face_number()
current_vertices = ms.current_mesh().vertex_number()
print(f"Loaded mesh: {current_vertices:,} vertices, {current_faces:,} faces")
# Pre-processing step to clean the mesh
if pre_clean:
print(f"Performing pre-processing cleaning operations...")
# Merge close vertices (helps with many common mesh issues)
ms.apply_filter('meshing_merge_close_vertices')
# Remove unreferenced vertices
ms.apply_filter('meshing_remove_unreferenced_vertices')
# Remove duplicate faces if they exist
ms.apply_filter('meshing_remove_duplicate_faces')
# Update counts after pre-processing
cleaned_vertices = ms.current_mesh().vertex_number()
cleaned_faces = ms.current_mesh().face_number()
# Report the effect of pre-processing
vertices_removed = current_vertices - cleaned_vertices
faces_removed = current_faces - cleaned_faces
if vertices_removed > 0 or faces_removed > 0:
print(f"Pre-processing removed {vertices_removed:,} vertices and {faces_removed:,} faces")
print(f"Mesh after cleaning: {cleaned_vertices:,} vertices, {cleaned_faces:,} faces")
else:
print("Pre-processing complete. No issues found in the mesh.")
# Update current faces count for target calculation
current_faces = cleaned_faces
# Calculate target face count
if target_faces is not None and target_faces > 0:
targetfacenum = int(target_faces)
elif percentage_reduction is not None and 0.0 <= percentage_reduction <= 1.0:
targetfacenum = int(current_faces * (1.0 - percentage_reduction))
else:
# Default case
targetfacenum = int(target_faces) if target_faces is not None else 1000
# Ensure we don't go below a minimum number of faces
targetfacenum = max(4, targetfacenum)
print(f"Starting standard mesh simplification (current: {current_faces:,} faces, target: {targetfacenum:,} faces)...")
start_time = time.time()
# Use the standard quadric edge collapse filter
ms.apply_filter('meshing_decimation_quadric_edge_collapse',
targetfacenum=targetfacenum,
qualitythr=float(quality_threshold),
preserveboundary=preserve_boundary,
boundaryweight=float(boundary_weight),
optimalplacement=optimal_position,
preservenormal=preserve_normal,
planarquadric=planar_simplification)
# Optional: Quality improvement as post-processing
ms.apply_filter('meshing_edge_flip_by_planar_optimization',
planartype='area/max side',
pthreshold=1.0,
iterations=2)
elapsed = time.time() - start_time
new_faces = ms.current_mesh().face_number()
reduction_percent = ((current_faces - new_faces) / current_faces) * 100
print(f"Mesh simplification completed in {elapsed:.2f} seconds.")
print(f"Reduced from {current_faces:,} to {new_faces:,} faces ({reduction_percent:.1f}% reduction)")
# Save the mesh
ms.save_current_mesh(output_path)