Files

378 lines
16 KiB
Python

"""
Advanced Mesh Processing Nodes for RDAWG 3D Pack
Utilizes Open3D's advanced mesh processing capabilities
"""
import torch
import numpy as np
import open3d as o3d
from typing import Optional, Tuple, Any
import copy
class RDAWG3DSimplifyMesh:
"""Simplify mesh using Open3D's decimation algorithms"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"mesh": ("MESH",),
"target_triangles": ("INT", {"default": 1000, "min": 100, "max": 100000}),
"preserve_boundaries": ("BOOLEAN", {"default": True}),
"method": (["quadric", "shortest_edge"], {"default": "quadric"}),
}
}
RETURN_TYPES = ("MESH", "STRING")
RETURN_NAMES = ("simplified_mesh", "info")
FUNCTION = "simplify_mesh"
CATEGORY = "RDAWG 3D/Processing"
def simplify_mesh(self, mesh: dict, target_triangles: int, preserve_boundaries: bool, method: str):
if 'open3d_mesh' not in mesh:
raise ValueError("Mesh must have Open3D mesh data")
o3d_mesh = copy.deepcopy(mesh['open3d_mesh'])
if len(o3d_mesh.triangles) <= target_triangles:
return (mesh, f"Mesh already has {len(o3d_mesh.triangles)} triangles (≤ target)")
try:
if method == "quadric":
# Quadric decimation
simplified_mesh = o3d_mesh.simplify_quadric_decimation(
target_number_of_triangles=target_triangles,
preserve_boundary=preserve_boundaries
)
else:
# Shortest edge decimation
simplified_mesh = o3d_mesh.simplify_vertex_clustering(
voxel_size=o3d_mesh.get_max_bound() / (target_triangles ** 0.33) * 2
)
# Create new mesh data
new_mesh = mesh.copy()
new_mesh['open3d_mesh'] = simplified_mesh
new_mesh['vertices'] = torch.from_numpy(np.asarray(simplified_mesh.vertices)).float().to(mesh['vertices'].device)
new_mesh['faces'] = torch.from_numpy(np.asarray(simplified_mesh.triangles)).long().to(mesh['faces'].device) if mesh['faces'] is not None else None
simplified_mesh.compute_vertex_normals()
new_mesh['vertex_normals'] = torch.from_numpy(np.asarray(simplified_mesh.vertex_normals)).float().to(mesh['vertices'].device) if simplified_mesh.has_vertex_normals() else None
info = f"Simplified mesh using {method}\n"
info += f"Original triangles: {len(o3d_mesh.triangles)}\n"
info += f"Simplified triangles: {len(simplified_mesh.triangles)}\n"
info += f"Reduction: {(1 - len(simplified_mesh.triangles)/len(o3d_mesh.triangles))*100:.1f}%"
return (new_mesh, info)
except Exception as e:
raise RuntimeError(f"Mesh simplification failed: {e}")
class RDAWG3DSubdivideMesh:
"""Subdivide mesh using different subdivision schemes"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"mesh": ("MESH",),
"subdivisions": ("INT", {"default": 1, "min": 1, "max": 4}),
"method": (["linear", "loop"], {"default": "loop"}),
}
}
RETURN_TYPES = ("MESH", "STRING")
RETURN_NAMES = ("subdivided_mesh", "info")
FUNCTION = "subdivide_mesh"
CATEGORY = "RDAWG 3D/Processing"
def subdivide_mesh(self, mesh: dict, subdivisions: int, method: str):
if 'open3d_mesh' not in mesh:
raise ValueError("Mesh must have Open3D mesh data")
o3d_mesh = copy.deepcopy(mesh['open3d_mesh'])
try:
for _ in range(subdivisions):
if method == "linear":
o3d_mesh = o3d_mesh.subdivide_midpoint()
else: # loop
o3d_mesh = o3d_mesh.subdivide_loop()
# Create new mesh data
new_mesh = mesh.copy()
new_mesh['open3d_mesh'] = o3d_mesh
new_mesh['vertices'] = torch.from_numpy(np.asarray(o3d_mesh.vertices)).float().to(mesh['vertices'].device)
new_mesh['faces'] = torch.from_numpy(np.asarray(o3d_mesh.triangles)).long().to(mesh['faces'].device) if mesh['faces'] is not None else None
o3d_mesh.compute_vertex_normals()
new_mesh['vertex_normals'] = torch.from_numpy(np.asarray(o3d_mesh.vertex_normals)).float().to(mesh['vertices'].device) if o3d_mesh.has_vertex_normals() else None
info = f"Subdivided mesh using {method}\n"
info += f"Subdivisions: {subdivisions}\n"
info += f"Original vertices: {len(mesh['vertices'])}\n"
info += f"Subdivided vertices: {len(o3d_mesh.vertices)}\n"
info += f"Original faces: {len(mesh['faces']) if mesh['faces'] is not None else 0}\n"
info += f"Subdivided faces: {len(o3d_mesh.triangles)}"
return (new_mesh, info)
except Exception as e:
raise RuntimeError(f"Mesh subdivision failed: {e}")
class RDAWG3DSmoothMesh:
"""Smooth mesh using various algorithms"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"mesh": ("MESH",),
"iterations": ("INT", {"default": 1, "min": 1, "max": 50}),
"lambda_factor": ("FLOAT", {"default": 0.5, "min": 0.01, "max": 2.0, "step": 0.01}),
"method": (["taubin", "laplacian"], {"default": "taubin"}),
}
}
RETURN_TYPES = ("MESH", "STRING")
RETURN_NAMES = ("smoothed_mesh", "info")
FUNCTION = "smooth_mesh"
CATEGORY = "RDAWG 3D/Processing"
def smooth_mesh(self, mesh: dict, iterations: int, lambda_factor: float, method: str):
if 'open3d_mesh' not in mesh:
raise ValueError("Mesh must have Open3D mesh data")
o3d_mesh = copy.deepcopy(mesh['open3d_mesh'])
try:
if method == "taubin":
# Taubin smoothing
o3d_mesh.filter_smooth_taubin(number_of_iterations=iterations, lambda_filter=lambda_factor)
else:
# Laplacian smoothing
o3d_mesh.filter_smooth_laplacian(number_of_iterations=iterations, lambda_filter=lambda_factor)
# Create new mesh data
new_mesh = mesh.copy()
new_mesh['open3d_mesh'] = o3d_mesh
new_mesh['vertices'] = torch.from_numpy(np.asarray(o3d_mesh.vertices)).float().to(mesh['vertices'].device)
if o3d_mesh.has_vertex_normals():
new_mesh['vertex_normals'] = torch.from_numpy(np.asarray(o3d_mesh.vertex_normals)).float().to(mesh['vertices'].device)
info = f"Smoothed mesh using {method}\n"
info += f"Iterations: {iterations}\n"
info += f"Lambda factor: {lambda_factor}\n"
info += f"Vertices: {len(o3d_mesh.vertices)}"
return (new_mesh, info)
except Exception as e:
raise RuntimeError(f"Mesh smoothing failed: {e}")
class RDAWG3DRemeshUniform:
"""Remesh mesh to uniform edge lengths"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"mesh": ("MESH",),
"target_edge_length": ("FLOAT", {"default": 0.01, "min": 0.001, "max": 1.0, "step": 0.001}),
"iterations": ("INT", {"default": 10, "min": 1, "max": 100}),
}
}
RETURN_TYPES = ("MESH", "STRING")
RETURN_NAMES = ("remeshed_mesh", "info")
FUNCTION = "remesh_uniform"
CATEGORY = "RDAWG 3D/Processing"
def remesh_uniform(self, mesh: dict, target_edge_length: float, iterations: int):
if 'open3d_mesh' not in mesh:
raise ValueError("Mesh must have Open3D mesh data")
o3d_mesh = copy.deepcopy(mesh['open3d_mesh'])
try:
# Create a simple uniform remeshing using voxel decimation and subdivision
# This is a simplified approach - for production use, consider more advanced remeshing
# First, estimate current edge length
vertices = np.asarray(o3d_mesh.vertices)
triangles = np.asarray(o3d_mesh.triangles)
edge_lengths = []
for triangle in triangles:
for i in range(3):
v1_idx, v2_idx = triangle[i], triangle[(i+1)%3]
edge_length = np.linalg.norm(vertices[v1_idx] - vertices[v2_idx])
edge_lengths.append(edge_length)
current_avg_edge = np.mean(edge_lengths) if edge_lengths else 0.01
scale_factor = current_avg_edge / target_edge_length
# Apply appropriate operation based on scale factor
if scale_factor > 1.5: # Need to subdivide
subdivisions = min(3, int(np.log2(scale_factor)))
for _ in range(subdivisions):
o3d_mesh = o3d_mesh.subdivide_loop()
elif scale_factor < 0.7: # Need to decimate
target_triangles = int(len(o3d_mesh.triangles) * scale_factor ** 2)
target_triangles = max(target_triangles, 100) # Minimum triangles
o3d_mesh = o3d_mesh.simplify_quadric_decimation(target_number_of_triangles=target_triangles)
# Create new mesh data
new_mesh = mesh.copy()
new_mesh['open3d_mesh'] = o3d_mesh
new_mesh['vertices'] = torch.from_numpy(np.asarray(o3d_mesh.vertices)).float().to(mesh['vertices'].device)
new_mesh['faces'] = torch.from_numpy(np.asarray(o3d_mesh.triangles)).long().to(mesh['faces'].device) if mesh['faces'] is not None else None
o3d_mesh.compute_vertex_normals()
new_mesh['vertex_normals'] = torch.from_numpy(np.asarray(o3d_mesh.vertex_normals)).float().to(mesh['vertices'].device) if o3d_mesh.has_vertex_normals() else None
# Calculate new average edge length
new_vertices = np.asarray(o3d_mesh.vertices)
new_triangles = np.asarray(o3d_mesh.triangles)
new_edge_lengths = []
for triangle in new_triangles:
for i in range(3):
v1_idx, v2_idx = triangle[i], triangle[(i+1)%3]
edge_length = np.linalg.norm(new_vertices[v1_idx] - new_vertices[v2_idx])
new_edge_lengths.append(edge_length)
new_avg_edge = np.mean(new_edge_lengths) if new_edge_lengths else target_edge_length
info = f"Uniform remeshing completed\n"
info += f"Target edge length: {target_edge_length:.4f}\n"
info += f"Original avg edge: {current_avg_edge:.4f}\n"
info += f"New avg edge: {new_avg_edge:.4f}\n"
info += f"Original triangles: {len(triangles)}\n"
info += f"New triangles: {len(new_triangles)}"
return (new_mesh, info)
except Exception as e:
raise RuntimeError(f"Mesh remeshing failed: {e}")
class RDAWG3DComputeCurvature:
"""Compute curvature information for mesh vertices"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"mesh": ("MESH",),
"curvature_type": (["mean", "gaussian", "principal"], {"default": "mean"}),
"radius": ("FLOAT", {"default": 0.1, "min": 0.01, "max": 1.0, "step": 0.01}),
}
}
RETURN_TYPES = ("MESH", "TENSOR", "STRING")
RETURN_NAMES = ("mesh", "curvature", "info")
FUNCTION = "compute_curvature"
CATEGORY = "RDAWG 3D/Processing"
def compute_curvature(self, mesh: dict, curvature_type: str, radius: float):
if 'open3d_mesh' not in mesh:
raise ValueError("Mesh must have Open3D mesh data")
o3d_mesh = mesh['open3d_mesh']
try:
# Compute curvature using Open3D's estimate_normals and then curvature calculation
# Open3D doesn't have built-in curvature, so we'll estimate it using normal differences
if not o3d_mesh.has_vertex_normals():
o3d_mesh.compute_vertex_normals()
vertices = np.asarray(o3d_mesh.vertices)
normals = np.asarray(o3d_mesh.vertex_normals)
triangles = np.asarray(o3d_mesh.triangles)
# Simple curvature estimation based on normal variations
curvatures = np.zeros(len(vertices))
if curvature_type == "mean":
# Mean curvature estimation based on normal variation
kdtree = o3d.geometry.KDTreeFlann(o3d_mesh)
for i in range(len(vertices)):
_, idx, _ = kdtree.search_radius_vector_3d(o3d_mesh.vertices[i], radius)
if len(idx) > 1:
neighbor_normals = normals[idx[1:]] # Exclude self
normal_diff = np.linalg.norm(neighbor_normals - normals[i], axis=1)
curvatures[i] = np.mean(normal_diff)
elif curvature_type == "gaussian":
# Gaussian curvature approximation
for i, vertex in enumerate(vertices):
# Find triangles containing this vertex
vertex_triangles = [t for t in triangles if i in t]
if len(vertex_triangles) >= 3:
# Simple Gaussian curvature approximation
angle_sum = 0
for triangle in vertex_triangles:
# Get the other two vertices
other_vertices = [v for v in triangle if v != i]
if len(other_vertices) == 2:
v1, v2 = vertices[other_vertices[0]], vertices[other_vertices[1]]
# Compute angle at this vertex
edge1 = v1 - vertex
edge2 = v2 - vertex
cos_angle = np.dot(edge1, edge2) / (np.linalg.norm(edge1) * np.linalg.norm(edge2))
angle_sum += np.arccos(np.clip(cos_angle, -1, 1))
# Gaussian curvature approximation
curvatures[i] = (2 * np.pi - angle_sum) / len(vertex_triangles)
else: # principal
# Principal curvature approximation (using shape operator)
for i in range(len(vertices)):
kdtree = o3d.geometry.KDTreeFlann(o3d_mesh)
_, idx, _ = kdtree.search_radius_vector_3d(o3d_mesh.vertices[i], radius)
if len(idx) > 3:
local_vertices = vertices[idx] - vertices[i]
local_normals = normals[idx]
# Build covariance matrix of normals
cov_matrix = np.cov(local_normals.T)
eigenvalues = np.linalg.eigvals(cov_matrix)
curvatures[i] = np.max(np.abs(eigenvalues))
# Normalize curvatures
if np.max(curvatures) > np.min(curvatures):
curvatures = (curvatures - np.min(curvatures)) / (np.max(curvatures) - np.min(curvatures))
# Convert to tensor
curvature_tensor = torch.from_numpy(curvatures).float().to(mesh['vertices'].device)
info = f"Computed {curvature_type} curvature\n"
info += f"Search radius: {radius}\n"
info += f"Min curvature: {np.min(curvatures):.4f}\n"
info += f"Max curvature: {np.max(curvatures):.4f}\n"
info += f"Mean curvature: {np.mean(curvatures):.4f}"
return (mesh, curvature_tensor, info)
except Exception as e:
raise RuntimeError(f"Curvature computation failed: {e}")
# Add to node mappings
NODE_CLASS_MAPPINGS = {
"RDAWG3DSimplifyMesh": RDAWG3DSimplifyMesh,
"RDAWG3DSubdivideMesh": RDAWG3DSubdivideMesh,
"RDAWG3DSmoothMesh": RDAWG3DSmoothMesh,
"RDAWG3DRemeshUniform": RDAWG3DRemeshUniform,
"RDAWG3DComputeCurvature": RDAWG3DComputeCurvature,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"RDAWG3DSimplifyMesh": "🔷 Simplify Mesh (RDAWG)",
"RDAWG3DSubdivideMesh": "🔷 Subdivide Mesh (RDAWG)",
"RDAWG3DSmoothMesh": "🔷 Smooth Mesh (RDAWG)",
"RDAWG3DRemeshUniform": "🔷 Remesh Uniform (RDAWG)",
"RDAWG3DComputeCurvature": "🔷 Compute Curvature (RDAWG)",
}