378 lines
16 KiB
Python
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)",
|
|
} |