diff --git a/install.py b/install.py index e569dcb7..64423659 100644 --- a/install.py +++ b/install.py @@ -5,229 +5,17 @@ """ GeometryPack Installer -Installs system dependencies and Python packages for ComfyUI-GeometryPack. +Orchestrates modular install scripts for ComfyUI-GeometryPack. Blender is optional - run 'python blender_install.py' separately if needed. """ -import os import sys -import platform -import subprocess -from pathlib import Path - -def get_platform_info(): - """Detect current platform and architecture.""" - system = platform.system().lower() - machine = platform.machine().lower() - - if system == "darwin": - plat = "macos" - arch = "arm64" if machine == "arm64" else "x64" - elif system == "linux": - plat = "linux" - arch = "x64" - elif system == "windows": - plat = "windows" - arch = "x64" - else: - plat = None - arch = None - - return plat, arch - - -def install_system_dependencies(): - """Install required system dependencies (Linux only).""" - plat, _ = get_platform_info() - - if plat != "linux": - return True - - print("\n" + "="*60) - print("ComfyUI-GeometryPack: System Dependencies") - print("="*60 + "\n") - - print("[Install] Checking for required OpenGL libraries...") - print("[Install] These are needed for PyMeshLab remeshing to work properly.") - - try: - critical_packages = ["libgl1", "libopengl0", "libglu1-mesa", "libglx-mesa0"] - optional_packages = ["libosmesa6"] - - all_packages = critical_packages + optional_packages - print(f"[Install] Installing OpenGL libraries: {', '.join(all_packages)}") - print("[Install] You may be prompted for your sudo password...") - - print("[Install] Updating apt cache...") - update_result = subprocess.run( - ['sudo', 'apt-get', 'update'], - capture_output=True, - text=True, - timeout=120 - ) - - if update_result.returncode != 0: - print("[Install] Warning: Failed to update apt cache") - print(f"[Install] You may need to run manually: sudo apt-get update") - - installed_packages = [] - failed_packages = [] - critical_failed = [] - - print("[Install] Installing critical OpenGL libraries...") - for package in critical_packages: - result = subprocess.run( - ['sudo', 'apt-get', 'install', '-y', package], - capture_output=True, - text=True, - timeout=60 - ) - if result.returncode == 0: - installed_packages.append(package) - print(f"[Install] + {package}") - else: - failed_packages.append(package) - critical_failed.append(package) - print(f"[Install] x {package} (failed)") - - print("[Install] Installing optional OpenGL libraries...") - for package in optional_packages: - result = subprocess.run( - ['sudo', 'apt-get', 'install', '-y', package], - capture_output=True, - text=True, - timeout=60 - ) - if result.returncode == 0: - installed_packages.append(package) - print(f"[Install] + {package}") - else: - failed_packages.append(package) - print(f"[Install] ~ {package} (optional, skipped)") - - print("[Install] Verifying OpenGL libraries...") - opengl_works = False - try: - import ctypes - ctypes.CDLL("libOpenGL.so.0") - opengl_works = True - print("[Install] + libOpenGL.so.0 loaded successfully") - except OSError as e: - print(f"[Install] x libOpenGL.so.0 failed to load: {e}") - - if installed_packages: - print(f"[Install] Installed: {', '.join(installed_packages)}") - - if failed_packages: - print(f"[Install] Failed to install: {', '.join(failed_packages)}") - - if critical_failed: - print(f"[Install] ERROR: Critical packages failed to install: {', '.join(critical_failed)}") - print(f"[Install] PyMeshLab remeshing will NOT work!") - print(f"[Install] You may need to run manually:") - print(f"[Install] sudo apt-get install {' '.join(critical_failed)}") - return False - elif not opengl_works: - print("[Install] ERROR: OpenGL libraries installed but cannot be loaded!") - print("[Install] PyMeshLab remeshing will NOT work!") - print("[Install] Try running: sudo ldconfig") - return False - else: - print("[Install] OpenGL libraries installed and verified successfully!") - return True - - except subprocess.TimeoutExpired: - print("[Install] Warning: Installation timed out") - print(f"[Install] You may need to run manually:") - print(f"[Install] sudo apt-get install libgl1 libopengl0 libglu1-mesa libglx-mesa0") - return False - except FileNotFoundError: - print("[Install] Warning: apt-get not found (not a Debian/Ubuntu system?)") - print("[Install] Please install OpenGL libraries manually for your distribution") - return True - except KeyboardInterrupt: - print("\n[Install] Installation cancelled by user") - print(f"[Install] You can install OpenGL libraries later with:") - print(f"[Install] sudo apt-get install libgl1 libopengl0 libglu1-mesa libglx-mesa0") - return False - except Exception as e: - print(f"[Install] Warning: Could not install system dependencies: {e}") - print(f"[Install] PyMeshLab remeshing may not work without OpenGL libraries.") - print(f"[Install] To fix, run: sudo apt-get update && sudo apt-get install libgl1 libopengl0 libglu1-mesa libglx-mesa0") - return False - - -def install_python_dependencies(): - """Install Python dependencies from requirements.txt.""" - print("\n" + "="*60) - print("ComfyUI-GeometryPack: Python Dependencies Installation") - print("="*60 + "\n") - - script_dir = Path(__file__).parent.absolute() - requirements_file = script_dir / "requirements.txt" - - if not requirements_file.exists(): - print(f"[Install] Warning: requirements.txt not found at {requirements_file}") - print("[Install] Skipping Python dependencies installation.") - return True - - print(f"[Install] Installing core Python dependencies...") - print(f"[Install] This may take a few minutes...\n") - - try: - result = subprocess.run( - [sys.executable, "-m", "pip", "install", "-r", str(requirements_file)], - capture_output=True, - text=True, - timeout=600 - ) - - if result.returncode == 0: - print("\n[Install] All Python dependencies installed successfully!") - return True - else: - print(f"\n[Install] Warning: Some packages failed to install") - print("[Install] Attempting to install core dependencies without optional packages...") - - result_without_optional = subprocess.run( - [sys.executable, "-m", "pip", "install", - "requests>=2.25.0", "tqdm>=4.60.0", - "numpy>=1.21.0", "scipy>=1.7.0", - "trimesh>=3.15.0", "pymeshlab>=2022.2", - "matplotlib>=3.5.0", "Pillow>=9.0.0", - "point-cloud-utils>=0.30.0", - "fast-simplification>=0.1.5", - "xatlas>=0.0.11", - "skeletor>=1.2.0", - "libigl>=2.6.1"], - capture_output=True, - text=True, - timeout=600 - ) - - if result_without_optional.returncode == 0: - print("\n[Install] Core Python dependencies installed successfully!") - print("[Install] Note: Some optional packages (like cgal) may not be available") - print("[Install] You can install them manually later if needed") - return True - else: - print(f"\n[Install] Error installing Python dependencies:") - print(result_without_optional.stderr) - print("\n[Install] You can try installing manually with:") - print(f"[Install] pip install -r {requirements_file}") - return False - - except subprocess.TimeoutExpired: - print("\n[Install] Error: Installation timed out after 10 minutes") - print("[Install] You can try installing manually with:") - print(f"[Install] pip install -r {requirements_file}") - return False - except Exception as e: - print(f"\n[Install] Error installing Python dependencies: {e}") - print("[Install] You can try installing manually with:") - print(f"[Install] pip install -r {requirements_file}") - return False +from install_scripts import ( + install_system_dependencies, + install_python_dependencies, + install_cumesh, +) def main(): @@ -238,6 +26,7 @@ def main(): print("This installer will set up:") print(" 1. System dependencies (OpenGL libraries on Linux)") print(" 2. Python dependencies (trimesh, pymeshlab, etc.)") + print(" 3. CuMesh (optional, GPU-accelerated mesh processing)") print("") print("Note: Blender is optional. If you need Blender nodes") print("(UV Unwrap, Remesh, etc.), run: python blender_install.py") @@ -246,16 +35,21 @@ def main(): results = { 'system_deps': False, 'python_deps': False, + 'cumesh': False, } + # Install in order results['system_deps'] = install_system_dependencies() results['python_deps'] = install_python_dependencies() + results['cumesh'] = install_cumesh() + # Print summary print("\n" + "="*60) print("Installation Summary") print("="*60) print(f" System Dependencies: {'+ Success' if results['system_deps'] else 'x Failed'}") print(f" Python Dependencies: {'+ Success' if results['python_deps'] else 'x Failed'}") + print(f" CuMesh (GPU): {'+ Success' if results['cumesh'] else '~ Skipped'}") print("="*60 + "\n") if results['python_deps']: diff --git a/install_scripts/__init__.py b/install_scripts/__init__.py new file mode 100644 index 00000000..e93dc8e6 --- /dev/null +++ b/install_scripts/__init__.py @@ -0,0 +1,17 @@ +# SPDX-License-Identifier: GPL-3.0-or-later +# Copyright (C) 2025 ComfyUI-GeometryPack Contributors + +""" +GeometryPack Install Scripts - Modular installation utilities. +""" + +from .system_deps import get_platform_info, install_system_dependencies +from .python_deps import install_python_dependencies +from .cumesh import install_cumesh + +__all__ = [ + 'get_platform_info', + 'install_system_dependencies', + 'install_python_dependencies', + 'install_cumesh', +] diff --git a/install_scripts/cumesh.py b/install_scripts/cumesh.py new file mode 100644 index 00000000..922d6448 --- /dev/null +++ b/install_scripts/cumesh.py @@ -0,0 +1,213 @@ +# SPDX-License-Identifier: GPL-3.0-or-later +# Copyright (C) 2025 ComfyUI-GeometryPack Contributors + +""" +CuMesh Installation - GPU-accelerated mesh processing library. + +Tries to install cumesh via: +1. Pre-built wheel from cumesh-wheels repository +2. Source compilation from JeffreyXiang/CuMesh + +Requires CUDA and PyTorch. Gracefully skips if not available. +""" + +import os +import sys +import shutil +import tempfile +import subprocess +from pathlib import Path + + +# Known wheel configurations +WHEEL_REPO_BASE = "https://github.com/PozzettiAndrea/cumesh-wheels/releases/download" +SOURCE_REPO = "https://github.com/JeffreyXiang/CuMesh.git" + + +def _check_cuda_available(): + """Check if CUDA is available via PyTorch.""" + try: + import torch + if torch.cuda.is_available(): + return True, torch.version.cuda, torch.__version__ + else: + return False, None, torch.__version__ + except ImportError: + return False, None, None + + +def _get_python_version(): + """Get Python version string for wheel filename.""" + return f"{sys.version_info.major}{sys.version_info.minor}" + + +def _verify_cumesh_import(): + """Try to import cumesh and verify it works.""" + try: + import cumesh + # Try to instantiate the main class + _ = cumesh.CuMesh + return True + except Exception: + return False + + +def _install_wheel(cuda_version, torch_version, python_version): + """Try to install cumesh from pre-built wheel.""" + # Format versions for wheel URL + # CUDA: "12.8" -> "128", Torch: "2.8.0" -> "280" + cuda_short = cuda_version.replace(".", "")[:3] if cuda_version else None + torch_short = torch_version.split("+")[0].replace(".", "")[:3] if torch_version else None + + if not cuda_short or not torch_short: + print("[CuMesh] Cannot determine CUDA/PyTorch versions for wheel selection") + return False + + # Construct wheel URL + wheel_tag = f"cu{cuda_short}-torch{torch_short}" + wheel_name = f"cumesh-0.0.1-cp{python_version}-cp{python_version}-linux_x86_64.whl" + wheel_url = f"{WHEEL_REPO_BASE}/{wheel_tag}/{wheel_name}" + + print(f"[CuMesh] Trying wheel: {wheel_tag}/{wheel_name}") + + try: + result = subprocess.run( + [sys.executable, "-m", "pip", "install", wheel_url], + capture_output=True, + text=True, + timeout=300 + ) + + if result.returncode == 0: + print(f"[CuMesh] Wheel installed successfully!") + return True + else: + print(f"[CuMesh] Wheel installation failed (may not exist for this configuration)") + if "404" in result.stderr or "not found" in result.stderr.lower(): + print(f"[CuMesh] Wheel not available for {wheel_tag}") + return False + + except subprocess.TimeoutExpired: + print("[CuMesh] Wheel download timed out") + return False + except Exception as e: + print(f"[CuMesh] Wheel installation error: {e}") + return False + + +def _install_from_source(): + """Try to install cumesh from source.""" + print("[CuMesh] Attempting source compilation...") + print("[CuMesh] This may take several minutes...") + + # Create temporary directory for clone + temp_dir = tempfile.mkdtemp(prefix="cumesh_build_") + clone_path = Path(temp_dir) / "CuMesh" + + try: + # Clone repository + print(f"[CuMesh] Cloning {SOURCE_REPO}...") + result = subprocess.run( + ["git", "clone", "--recursive", SOURCE_REPO, str(clone_path)], + capture_output=True, + text=True, + timeout=300 + ) + + if result.returncode != 0: + print(f"[CuMesh] Git clone failed: {result.stderr}") + return False + + # Install with pip + print("[CuMesh] Building and installing (this may take a while)...") + result = subprocess.run( + [sys.executable, "-m", "pip", "install", ".", "--no-build-isolation"], + cwd=str(clone_path), + capture_output=True, + text=True, + timeout=1200 # 20 minutes for compilation + ) + + if result.returncode == 0: + print("[CuMesh] Source build completed successfully!") + return True + else: + print(f"[CuMesh] Source build failed:") + print(result.stderr[-2000:] if len(result.stderr) > 2000 else result.stderr) + return False + + except subprocess.TimeoutExpired: + print("[CuMesh] Source build timed out (>20 minutes)") + return False + except FileNotFoundError: + print("[CuMesh] Git not found - cannot clone repository") + return False + except Exception as e: + print(f"[CuMesh] Source build error: {e}") + return False + finally: + # Clean up temp directory + try: + shutil.rmtree(temp_dir) + except Exception: + pass + + +def install_cumesh(): + """ + Install CuMesh library for GPU-accelerated mesh processing. + + Returns: + bool: True if installed successfully or already available, + False if installation failed (graceful failure). + """ + print("\n" + "="*60) + print("ComfyUI-GeometryPack: CuMesh Installation (Optional)") + print("="*60 + "\n") + + # Check if already installed + if _verify_cumesh_import(): + print("[CuMesh] Already installed and working!") + return True + + # Check CUDA availability + cuda_available, cuda_version, torch_version = _check_cuda_available() + + if not cuda_available: + print("[CuMesh] CUDA not available - skipping cumesh installation") + print("[CuMesh] This is fine! The 'cumesh' UV unwrap method will be disabled,") + print("[CuMesh] but other UV unwrap methods (xatlas, libigl, blender) will work.") + return True # Graceful skip, not a failure + + print(f"[CuMesh] CUDA {cuda_version} detected with PyTorch {torch_version}") + + python_version = _get_python_version() + print(f"[CuMesh] Python version: {sys.version_info.major}.{sys.version_info.minor}") + + # Try wheel first + print("\n[CuMesh] Step 1: Trying pre-built wheel...") + if _install_wheel(cuda_version, torch_version, python_version): + if _verify_cumesh_import(): + print("[CuMesh] Installation verified!") + return True + else: + print("[CuMesh] Wheel installed but import failed, trying source...") + + # Fallback to source + print("\n[CuMesh] Step 2: Trying source compilation...") + if _install_from_source(): + if _verify_cumesh_import(): + print("[CuMesh] Installation verified!") + return True + else: + print("[CuMesh] Source build completed but import failed") + + # Both failed + print("\n" + "-"*60) + print("[CuMesh] WARNING: CuMesh installation failed") + print("[CuMesh] The 'cumesh' UV unwrap method will not be available.") + print("[CuMesh] Other UV unwrap methods (xatlas, libigl, blender) will still work.") + print("-"*60) + + # Return True for graceful failure - don't break the whole install + return True diff --git a/install_scripts/python_deps.py b/install_scripts/python_deps.py new file mode 100644 index 00000000..4df76002 --- /dev/null +++ b/install_scripts/python_deps.py @@ -0,0 +1,83 @@ +# SPDX-License-Identifier: GPL-3.0-or-later +# Copyright (C) 2025 ComfyUI-GeometryPack Contributors + +""" +Python Dependencies Installation - pip requirements for GeometryPack. +""" + +import sys +import subprocess +from pathlib import Path + + +def install_python_dependencies(): + """Install Python dependencies from requirements.txt.""" + print("\n" + "="*60) + print("ComfyUI-GeometryPack: Python Dependencies Installation") + print("="*60 + "\n") + + # Look for requirements.txt relative to this script's parent directory + script_dir = Path(__file__).parent.parent.absolute() + requirements_file = script_dir / "requirements.txt" + + if not requirements_file.exists(): + print(f"[Install] Warning: requirements.txt not found at {requirements_file}") + print("[Install] Skipping Python dependencies installation.") + return True + + print(f"[Install] Installing core Python dependencies...") + print(f"[Install] This may take a few minutes...\n") + + try: + result = subprocess.run( + [sys.executable, "-m", "pip", "install", "-r", str(requirements_file)], + capture_output=True, + text=True, + timeout=600 + ) + + if result.returncode == 0: + print("\n[Install] All Python dependencies installed successfully!") + return True + else: + print(f"\n[Install] Warning: Some packages failed to install") + print("[Install] Attempting to install core dependencies without optional packages...") + + result_without_optional = subprocess.run( + [sys.executable, "-m", "pip", "install", + "requests>=2.25.0", "tqdm>=4.60.0", + "numpy>=1.21.0", "scipy>=1.7.0", + "trimesh>=3.15.0", "pymeshlab>=2022.2", + "matplotlib>=3.5.0", "Pillow>=9.0.0", + "point-cloud-utils>=0.30.0", + "fast-simplification>=0.1.5", + "xatlas>=0.0.11", + "skeletor>=1.2.0", + "libigl>=2.6.1"], + capture_output=True, + text=True, + timeout=600 + ) + + if result_without_optional.returncode == 0: + print("\n[Install] Core Python dependencies installed successfully!") + print("[Install] Note: Some optional packages (like cgal) may not be available") + print("[Install] You can install them manually later if needed") + return True + else: + print(f"\n[Install] Error installing Python dependencies:") + print(result_without_optional.stderr) + print("\n[Install] You can try installing manually with:") + print(f"[Install] pip install -r {requirements_file}") + return False + + except subprocess.TimeoutExpired: + print("\n[Install] Error: Installation timed out after 10 minutes") + print("[Install] You can try installing manually with:") + print(f"[Install] pip install -r {requirements_file}") + return False + except Exception as e: + print(f"\n[Install] Error installing Python dependencies: {e}") + print("[Install] You can try installing manually with:") + print(f"[Install] pip install -r {requirements_file}") + return False diff --git a/install_scripts/system_deps.py b/install_scripts/system_deps.py new file mode 100644 index 00000000..12c78009 --- /dev/null +++ b/install_scripts/system_deps.py @@ -0,0 +1,151 @@ +# SPDX-License-Identifier: GPL-3.0-or-later +# Copyright (C) 2025 ComfyUI-GeometryPack Contributors + +""" +System Dependencies Installation - OpenGL libraries for PyMeshLab. +""" + +import platform +import subprocess + + +def get_platform_info(): + """Detect current platform and architecture.""" + system = platform.system().lower() + machine = platform.machine().lower() + + if system == "darwin": + plat = "macos" + arch = "arm64" if machine == "arm64" else "x64" + elif system == "linux": + plat = "linux" + arch = "x64" + elif system == "windows": + plat = "windows" + arch = "x64" + else: + plat = None + arch = None + + return plat, arch + + +def install_system_dependencies(): + """Install required system dependencies (Linux only).""" + plat, _ = get_platform_info() + + if plat != "linux": + return True + + print("\n" + "="*60) + print("ComfyUI-GeometryPack: System Dependencies") + print("="*60 + "\n") + + print("[Install] Checking for required OpenGL libraries...") + print("[Install] These are needed for PyMeshLab remeshing to work properly.") + + try: + critical_packages = ["libgl1", "libopengl0", "libglu1-mesa", "libglx-mesa0"] + optional_packages = ["libosmesa6"] + + all_packages = critical_packages + optional_packages + print(f"[Install] Installing OpenGL libraries: {', '.join(all_packages)}") + print("[Install] You may be prompted for your sudo password...") + + print("[Install] Updating apt cache...") + update_result = subprocess.run( + ['sudo', 'apt-get', 'update'], + capture_output=True, + text=True, + timeout=120 + ) + + if update_result.returncode != 0: + print("[Install] Warning: Failed to update apt cache") + print(f"[Install] You may need to run manually: sudo apt-get update") + + installed_packages = [] + failed_packages = [] + critical_failed = [] + + print("[Install] Installing critical OpenGL libraries...") + for package in critical_packages: + result = subprocess.run( + ['sudo', 'apt-get', 'install', '-y', package], + capture_output=True, + text=True, + timeout=60 + ) + if result.returncode == 0: + installed_packages.append(package) + print(f"[Install] + {package}") + else: + failed_packages.append(package) + critical_failed.append(package) + print(f"[Install] x {package} (failed)") + + print("[Install] Installing optional OpenGL libraries...") + for package in optional_packages: + result = subprocess.run( + ['sudo', 'apt-get', 'install', '-y', package], + capture_output=True, + text=True, + timeout=60 + ) + if result.returncode == 0: + installed_packages.append(package) + print(f"[Install] + {package}") + else: + failed_packages.append(package) + print(f"[Install] ~ {package} (optional, skipped)") + + print("[Install] Verifying OpenGL libraries...") + opengl_works = False + try: + import ctypes + ctypes.CDLL("libOpenGL.so.0") + opengl_works = True + print("[Install] + libOpenGL.so.0 loaded successfully") + except OSError as e: + print(f"[Install] x libOpenGL.so.0 failed to load: {e}") + + if installed_packages: + print(f"[Install] Installed: {', '.join(installed_packages)}") + + if failed_packages: + print(f"[Install] Failed to install: {', '.join(failed_packages)}") + + if critical_failed: + print(f"[Install] ERROR: Critical packages failed to install: {', '.join(critical_failed)}") + print(f"[Install] PyMeshLab remeshing will NOT work!") + print(f"[Install] You may need to run manually:") + print(f"[Install] sudo apt-get install {' '.join(critical_failed)}") + return False + elif not opengl_works: + print("[Install] ERROR: OpenGL libraries installed but cannot be loaded!") + print("[Install] PyMeshLab remeshing will NOT work!") + print("[Install] Try running: sudo ldconfig") + return False + else: + print("[Install] OpenGL libraries installed and verified successfully!") + return True + + except subprocess.TimeoutExpired: + print("[Install] Warning: Installation timed out") + print(f"[Install] You may need to run manually:") + print(f"[Install] sudo apt-get install libgl1 libopengl0 libglu1-mesa libglx-mesa0") + return False + except FileNotFoundError: + print("[Install] Warning: apt-get not found (not a Debian/Ubuntu system?)") + print("[Install] Please install OpenGL libraries manually for your distribution") + return True + except KeyboardInterrupt: + print("\n[Install] Installation cancelled by user") + print(f"[Install] You can install OpenGL libraries later with:") + print(f"[Install] sudo apt-get install libgl1 libopengl0 libglu1-mesa libglx-mesa0") + return False + except Exception as e: + print(f"[Install] Warning: Could not install system dependencies: {e}") + print(f"[Install] PyMeshLab remeshing may not work without OpenGL libraries.") + print(f"[Install] To fix, run: sudo apt-get update && sudo apt-get install libgl1 libopengl0 libglu1-mesa libglx-mesa0") + return False diff --git a/nodes/uv/uv_unwrap.py b/nodes/uv/uv_unwrap.py index f4afc13a..84306939 100644 --- a/nodes/uv/uv_unwrap.py +++ b/nodes/uv/uv_unwrap.py @@ -36,6 +36,7 @@ class UVUnwrapNode: "trimesh": ("TRIMESH",), "method": ([ "xatlas", + "cumesh", "libigl_lscm", "libigl_harmonic", "libigl_arap", @@ -46,6 +47,32 @@ class UVUnwrapNode: ], {"default": "xatlas"}), }, "optional": { + # cumesh parameters (GPU-accelerated) + "chart_cone_angle": ("FLOAT", { + "default": 90.0, + "min": 0.0, + "max": 359.9, + "step": 1.0 + }), + "chart_refine_iterations": ("INT", { + "default": 0, + "min": 0, + "max": 10, + "step": 1 + }), + "chart_global_iterations": ("INT", { + "default": 1, + "min": 0, + "max": 10, + "step": 1 + }), + "chart_smooth_strength": ("INT", { + "default": 1, + "min": 0, + "max": 10, + "step": 1 + }), + # libigl_arap parameters "iterations": ("INT", { "default": 10, @@ -90,7 +117,10 @@ class UVUnwrapNode: FUNCTION = "unwrap" CATEGORY = "geompack/uv" - def unwrap(self, trimesh, method, iterations=10, angle_limit=66.0, island_margin=0.02, + def unwrap(self, trimesh, method, + chart_cone_angle=90.0, chart_refine_iterations=0, + chart_global_iterations=1, chart_smooth_strength=1, + iterations=10, angle_limit=66.0, island_margin=0.02, scale_to_bounds="true", cube_size=1.0, cylinder_radius=1.0): """ Apply UV unwrapping based on selected method. @@ -98,6 +128,10 @@ class UVUnwrapNode: Args: trimesh: Input trimesh.Trimesh object method: UV unwrapping method to use + chart_cone_angle: UV chart clustering threshold in degrees (cumesh, default: 90.0) + chart_refine_iterations: Refine UV charts iterations (cumesh, default: 0) + chart_global_iterations: Global UV optimization passes (cumesh, default: 1) + chart_smooth_strength: UV smoothing strength (cumesh, default: 1) iterations: Number of iterations for libigl_arap (default: 10) angle_limit: Angle limit for blender_smart in degrees (default: 66.0) island_margin: Island margin for blender_smart (default: 0.02) @@ -116,6 +150,11 @@ class UVUnwrapNode: if method == "xatlas": unwrapped_mesh, info = self._xatlas(trimesh) + elif method == "cumesh": + unwrapped_mesh, info = self._cumesh( + trimesh, chart_cone_angle, chart_refine_iterations, + chart_global_iterations, chart_smooth_strength + ) elif method == "libigl_lscm": unwrapped_mesh, info = self._libigl_lscm(trimesh) elif method == "libigl_harmonic": @@ -184,6 +223,112 @@ After: Vertex Duplication: {len(new_vertices)/len(trimesh.vertices):.2f}x Fast automatic UV unwrapping with vertex splitting at seams. +""" + return unwrapped, info + + def _cumesh(self, trimesh, chart_cone_angle, chart_refine_iterations, + chart_global_iterations, chart_smooth_strength): + """CuMesh GPU-accelerated UV unwrapping with fast clustering + xatlas.""" + try: + import torch + import cumesh as CuMesh + except ImportError as e: + raise ImportError( + f"cumesh not installed or CUDA not available: {e}\n" + "CuMesh requires CUDA and PyTorch. Install with:\n" + " pip install cumesh (or run install.py)\n" + "Alternatively, use 'xatlas' method which works on CPU." + ) + + if not torch.cuda.is_available(): + raise RuntimeError( + "CUDA not available. CuMesh requires a CUDA-capable GPU.\n" + "Use 'xatlas' method for CPU-based UV unwrapping." + ) + + print(f"[UVUnwrap] CuMesh: chart_cone_angle={chart_cone_angle}, " + f"refine_iter={chart_refine_iterations}, global_iter={chart_global_iterations}, " + f"smooth={chart_smooth_strength}") + + # Convert to torch tensors on GPU + vertices = torch.tensor(trimesh.vertices, dtype=torch.float32).cuda() + faces = torch.tensor(trimesh.faces, dtype=torch.int32).cuda() + + # Convert cone angle to radians + chart_cone_angle_rad = np.radians(chart_cone_angle) + + # Initialize CuMesh + cumesh = CuMesh.CuMesh() + cumesh.init(vertices, faces) + + # UV Unwrap with two-stage process (fast clustering + xatlas) + print("[UVUnwrap] Running CuMesh UV unwrap...") + out_vertices, out_faces, out_uvs, out_vmaps = cumesh.uv_unwrap( + compute_charts_kwargs={ + "threshold_cone_half_angle_rad": chart_cone_angle_rad, + "refine_iterations": chart_refine_iterations, + "global_iterations": chart_global_iterations, + "smooth_strength": chart_smooth_strength, + }, + return_vmaps=True, + verbose=True, + ) + + # Convert back to numpy + out_vertices_np = out_vertices.cpu().numpy() + out_faces_np = out_faces.cpu().numpy() + out_uvs_np = out_uvs.cpu().numpy() + + # Flip V coordinate (cumesh uses different UV convention) + out_uvs_np[:, 1] = 1 - out_uvs_np[:, 1] + + # Build result trimesh + unwrapped = trimesh_module.Trimesh( + vertices=out_vertices_np, + faces=out_faces_np, + process=False + ) + + from trimesh.visual import TextureVisuals + unwrapped.visual = TextureVisuals(uv=out_uvs_np) + + # Preserve metadata + unwrapped.metadata = trimesh.metadata.copy() + unwrapped.metadata['uv_unwrap'] = { + 'algorithm': 'cumesh', + 'chart_cone_angle': chart_cone_angle, + 'chart_refine_iterations': chart_refine_iterations, + 'chart_global_iterations': chart_global_iterations, + 'chart_smooth_strength': chart_smooth_strength, + 'original_vertices': len(trimesh.vertices), + 'unwrapped_vertices': len(out_vertices_np), + 'vertex_duplication_ratio': len(out_vertices_np) / len(trimesh.vertices) + } + + # Clean up GPU memory + torch.cuda.empty_cache() + + info = f"""UV Unwrap Results (CuMesh): + +Algorithm: CuMesh GPU-accelerated (fast clustering + xatlas) +Optimized for: Large meshes, GPU acceleration + +Parameters: + Chart Cone Angle: {chart_cone_angle}° + Refine Iterations: {chart_refine_iterations} + Global Iterations: {chart_global_iterations} + Smooth Strength: {chart_smooth_strength} + +Before: + Vertices: {len(trimesh.vertices):,} + Faces: {len(trimesh.faces):,} + +After: + Vertices: {len(out_vertices_np):,} + Faces: {len(unwrapped.faces):,} + Vertex Duplication: {len(out_vertices_np)/len(trimesh.vertices):.2f}x + +Two-stage GPU-accelerated UV unwrapping with vertex splitting at seams. """ return unwrapped, info diff --git a/requirements.txt b/requirements.txt index 9fc7ea30..e5a677b4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -29,6 +29,8 @@ fast-simplification>=0.1.5 # UV unwrapping xatlas>=0.0.11 +# GPU-accelerated UV unwrapping (optional, requires CUDA) +# cumesh is installed separately via install.py (wheel or source) # Skeleton extraction (optional) skeletor>=1.2.0