2034 lines
101 KiB
Python
Executable File
2034 lines
101 KiB
Python
Executable File
import os
|
|
import json
|
|
import tempfile
|
|
import logging
|
|
import struct
|
|
import time
|
|
from typing import List, Any, Dict, Tuple
|
|
|
|
import numpy as np
|
|
import torch
|
|
|
|
# 导入ComfyUI的路径管理
|
|
try:
|
|
import folder_paths
|
|
FOLDER_PATHS_AVAILABLE = True
|
|
except ImportError:
|
|
folder_paths = None
|
|
FOLDER_PATHS_AVAILABLE = False
|
|
|
|
# 导入trimesh用于GLB文件处理
|
|
try:
|
|
import trimesh
|
|
TRIMESH_AVAILABLE = True
|
|
except ImportError:
|
|
TRIMESH_AVAILABLE = False
|
|
trimesh = None
|
|
|
|
# 配置日志
|
|
logger = logging.getLogger('glb_point_cloud_processor')
|
|
|
|
class GLBPointCloudProcessor:
|
|
"""GLB点云文件处理器 - 专门用于删除黑色点和暗色点优化"""
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"glb_file_path": ("STRING", {
|
|
"default": "",
|
|
"tooltip": "GLB点云文件路径:输入需要处理的GLB格式点云文件路径。支持绝对路径(如C:/path/to/file.glb)或相对于ComfyUI output目录的相对路径(如pointcloud.glb)。GLB文件应包含点云数据,支持带颜色信息的点云处理"
|
|
}),
|
|
},
|
|
"optional": {
|
|
"black_threshold": ("INT", {
|
|
"default": 30, "min": 0, "max": 255, "step": 1,
|
|
"tooltip": "黑色阈值控制:RGB三色总和小于此值的点将被删除。0=仅删除纯黑点(0,0,0);30=删除深黑色点;60=删除较暗点;100=删除中等暗度点;255=删除所有点。调大=删除更多暗色点,点云更亮净;调小=保留更多暗色细节,但可能有噪点"
|
|
}),
|
|
"output_filename": ("STRING", {
|
|
"default": "clean_pointcloud",
|
|
"tooltip": "输出GLB文件名:处理后点云的保存文件名,系统会自动添加.glb扩展名。文件将保存到ComfyUI的output目录中。建议使用描述性名称如'clean_pointcloud'、'filtered_scan'等便于识别"
|
|
}),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = (
|
|
"STRING", # 处理后的GLB文件路径
|
|
)
|
|
RETURN_NAMES = (
|
|
"processed_glb_path",
|
|
)
|
|
OUTPUT_TOOLTIPS = [
|
|
"处理后的GLB文件完整路径",
|
|
]
|
|
OUTPUT_NODE = True
|
|
FUNCTION = "process_point_cloud"
|
|
CATEGORY = "💃VVL/Point Cloud Cleaning"
|
|
|
|
def process_point_cloud(self,
|
|
glb_file_path: str,
|
|
black_threshold: int = 30,
|
|
output_filename: str = "clean_pointcloud"):
|
|
"""
|
|
处理GLB点云文件,删除黑色点和暗色点
|
|
"""
|
|
|
|
processing_log = []
|
|
processing_log.append("开始GLB点云黑色点清理...")
|
|
|
|
# 检查依赖
|
|
if not TRIMESH_AVAILABLE:
|
|
error_msg = "trimesh库不可用,无法处理GLB文件"
|
|
logger.error(error_msg)
|
|
processing_log.append(f"错误: {error_msg}")
|
|
return ("",)
|
|
|
|
# 验证输入文件路径
|
|
if not glb_file_path or not glb_file_path.strip():
|
|
error_msg = "GLB文件路径为空"
|
|
logger.error(error_msg)
|
|
processing_log.append(f"错误: {error_msg}")
|
|
return ("",)
|
|
|
|
# 处理文件路径
|
|
input_path = self._resolve_file_path(glb_file_path.strip())
|
|
processing_log.append(f"输入文件路径: {input_path}")
|
|
|
|
if not os.path.exists(input_path):
|
|
error_msg = f"GLB文件不存在: {input_path}"
|
|
logger.error(error_msg)
|
|
processing_log.append(f"错误: {error_msg}")
|
|
return ("",)
|
|
|
|
try:
|
|
# 加载GLB文件
|
|
processing_log.append("正在加载GLB文件...")
|
|
scene = trimesh.load(input_path)
|
|
|
|
# 提取点云数据
|
|
point_clouds = []
|
|
other_geometries = []
|
|
|
|
if isinstance(scene, trimesh.Scene):
|
|
for name, geometry in scene.geometry.items():
|
|
if isinstance(geometry, trimesh.PointCloud):
|
|
point_clouds.append((name, geometry))
|
|
processing_log.append(f"发现点云: {name}, 点数: {len(geometry.vertices)}")
|
|
else:
|
|
other_geometries.append((name, geometry))
|
|
processing_log.append(f"发现其他几何体: {name}, 类型: {type(geometry).__name__}")
|
|
elif isinstance(scene, trimesh.PointCloud):
|
|
point_clouds.append(("main_pointcloud", scene))
|
|
processing_log.append(f"发现点云: main_pointcloud, 点数: {len(scene.vertices)}")
|
|
else:
|
|
# 尝试转换为点云
|
|
if hasattr(scene, 'vertices'):
|
|
pc = trimesh.PointCloud(vertices=scene.vertices, colors=getattr(scene.visual, 'vertex_colors', None))
|
|
point_clouds.append(("converted_pointcloud", pc))
|
|
processing_log.append(f"转换为点云: converted_pointcloud, 点数: {len(pc.vertices)}")
|
|
else:
|
|
error_msg = "GLB文件中未找到点云数据"
|
|
processing_log.append(f"错误: {error_msg}")
|
|
return ("",)
|
|
|
|
if not point_clouds:
|
|
error_msg = "GLB文件中没有点云数据"
|
|
processing_log.append(f"错误: {error_msg}")
|
|
return ("",)
|
|
|
|
# 处理每个点云
|
|
processed_point_clouds = []
|
|
total_original_points = 0
|
|
total_removed_points = 0
|
|
|
|
for name, point_cloud in point_clouds:
|
|
processing_log.append(f"\n处理点云: {name}")
|
|
original_count = len(point_cloud.vertices)
|
|
total_original_points += original_count
|
|
|
|
# 获取原始点云的所有属性和状态
|
|
original_vertices = point_cloud.vertices.copy()
|
|
colors = None
|
|
|
|
# 获取颜色信息
|
|
if hasattr(point_cloud.visual, 'vertex_colors') and point_cloud.visual.vertex_colors is not None:
|
|
colors = point_cloud.visual.vertex_colors.copy()
|
|
elif hasattr(point_cloud, 'colors') and point_cloud.colors is not None:
|
|
colors = point_cloud.colors.copy()
|
|
|
|
# 初始化掩码(所有点都保留)
|
|
keep_mask = np.ones(len(original_vertices), dtype=bool)
|
|
|
|
# 删除暗色点(仅基于颜色,不改变顶点坐标)
|
|
if colors is not None:
|
|
removed_count = self._remove_dark_points(original_vertices, colors, keep_mask, black_threshold, processing_log)
|
|
processing_log.append(f"删除黑色点: {removed_count} 个")
|
|
else:
|
|
processing_log.append("跳过黑色点过滤: 点云无颜色信息")
|
|
|
|
# 应用掩码,保留原始的顶点坐标
|
|
filtered_vertices = original_vertices[keep_mask]
|
|
filtered_colors = colors[keep_mask] if colors is not None else None
|
|
|
|
remaining_count = len(filtered_vertices)
|
|
removed_count = original_count - remaining_count
|
|
total_removed_points += removed_count
|
|
|
|
processing_log.append(f"点云 {name}: 原始 {original_count} -> 剩余 {remaining_count} (删除 {removed_count})")
|
|
|
|
# 创建处理后的点云,保留所有原始属性
|
|
if remaining_count > 0:
|
|
# 使用原始点云作为模板,只更新顶点和颜色
|
|
processed_pc = point_cloud.copy()
|
|
processed_pc.vertices = filtered_vertices
|
|
if filtered_colors is not None:
|
|
processed_pc.visual.vertex_colors = filtered_colors
|
|
|
|
processed_point_clouds.append((name, processed_pc))
|
|
processing_log.append(f"保留点云 {name} 的所有原始属性和状态")
|
|
else:
|
|
processing_log.append(f"警告: 点云 {name} 处理后没有剩余点")
|
|
|
|
# 直接修改原始场景,保持所有几何信息不变
|
|
if processed_point_clouds:
|
|
# 使用原始场景作为基础,直接替换点云数据
|
|
new_scene = scene
|
|
|
|
# 直接修改原始场景中的点云几何体,保持变换不变
|
|
for name, processed_pc in processed_point_clouds:
|
|
if isinstance(scene, trimesh.Scene):
|
|
# 获取原始几何体
|
|
if name in scene.geometry:
|
|
original_geometry = scene.geometry[name]
|
|
|
|
# 如果是点云,直接修改顶点和颜色
|
|
if isinstance(original_geometry, trimesh.PointCloud):
|
|
original_geometry.vertices = processed_pc.vertices
|
|
if hasattr(processed_pc.visual, 'vertex_colors') and processed_pc.visual.vertex_colors is not None:
|
|
original_geometry.visual.vertex_colors = processed_pc.visual.vertex_colors
|
|
|
|
processing_log.append(f"直接修改原始点云 {name},保持所有变换和属性不变")
|
|
else:
|
|
# 如果不是点云类型,替换整个几何体但保持变换
|
|
scene.delete_geometry(name)
|
|
scene.add_geometry(processed_pc, node_name=name)
|
|
processing_log.append(f"替换几何体 {name},尝试保持变换")
|
|
else:
|
|
# 新增几何体
|
|
scene.add_geometry(processed_pc, node_name=name)
|
|
processing_log.append(f"添加新点云 {name}")
|
|
else:
|
|
# 单个几何体的情况
|
|
new_scene = processed_pc
|
|
processing_log.append("单个几何体,直接使用处理后的点云")
|
|
|
|
processing_log.append("直接修改原始场景,所有变换矩阵和几何属性完全保持不变")
|
|
|
|
# 生成输出路径
|
|
output_path = self._generate_output_path(output_filename)
|
|
processing_log.append(f"输出文件路径: {output_path}")
|
|
|
|
# 保存处理后的GLB文件
|
|
processing_log.append("正在保存处理后的GLB文件...")
|
|
new_scene.export(output_path)
|
|
|
|
# 验证文件是否保存成功
|
|
if os.path.exists(output_path):
|
|
file_size = os.path.getsize(output_path)
|
|
processing_log.append(f"GLB文件保存成功,文件大小: {file_size} bytes")
|
|
|
|
# 验证输出文件的一致性
|
|
try:
|
|
# 加载输出文件进行验证
|
|
verification_scene = trimesh.load(output_path)
|
|
processing_log.append("文件验证: 输出文件可正常加载")
|
|
|
|
# 检查是否保留了场景结构
|
|
if isinstance(scene, trimesh.Scene) and isinstance(verification_scene, trimesh.Scene):
|
|
processing_log.append(f"场景结构验证: 原始 {len(scene.geometry)} 个对象 -> 输出 {len(verification_scene.geometry)} 个对象")
|
|
|
|
except Exception as e:
|
|
processing_log.append(f"文件验证警告: {str(e)}")
|
|
|
|
processing_log.append("GLB点云黑色点清理完成! 已保留原始模型的大小、朝向和几何属性")
|
|
return (output_path,)
|
|
else:
|
|
error_msg = "GLB文件保存失败"
|
|
processing_log.append(f"错误: {error_msg}")
|
|
return ("",)
|
|
else:
|
|
error_msg = "处理后没有剩余的点云数据"
|
|
processing_log.append(f"错误: {error_msg}")
|
|
return ("",)
|
|
|
|
except Exception as e:
|
|
error_msg = f"处理GLB文件时发生错误: {str(e)}"
|
|
logger.error(error_msg)
|
|
processing_log.append(f"错误: {error_msg}")
|
|
import traceback
|
|
traceback.print_exc()
|
|
return ("",)
|
|
|
|
def _resolve_file_path(self, file_path: str) -> str:
|
|
"""解析文件路径,支持绝对路径和相对路径"""
|
|
if os.path.isabs(file_path):
|
|
return file_path
|
|
|
|
# 尝试相对于ComfyUI输出目录
|
|
if FOLDER_PATHS_AVAILABLE:
|
|
output_dir = folder_paths.get_output_directory()
|
|
candidate_path = os.path.join(output_dir, file_path)
|
|
if os.path.exists(candidate_path):
|
|
return candidate_path
|
|
|
|
# 尝试相对于当前工作目录
|
|
if os.path.exists(file_path):
|
|
return os.path.abspath(file_path)
|
|
|
|
# 返回原始路径(让后续检查处理错误)
|
|
return file_path
|
|
|
|
def _generate_output_path(self, filename: str) -> str:
|
|
"""生成输出文件路径"""
|
|
if FOLDER_PATHS_AVAILABLE:
|
|
output_dir = folder_paths.get_output_directory()
|
|
else:
|
|
output_dir = "output"
|
|
|
|
# 确保输出目录存在
|
|
os.makedirs(output_dir, exist_ok=True)
|
|
|
|
# 处理文件名,确保是.glb扩展名
|
|
if not filename.lower().endswith('.glb'):
|
|
filename = f"{filename}.glb"
|
|
|
|
return os.path.join(output_dir, filename)
|
|
|
|
def _remove_dark_points(self, vertices: np.ndarray, colors: np.ndarray, keep_mask: np.ndarray,
|
|
black_threshold: int, processing_log: List[str]) -> int:
|
|
"""删除暗色点"""
|
|
removed_count = 0
|
|
|
|
# 确保颜色数据格式正确
|
|
if colors.shape[1] == 4: # RGBA
|
|
rgb_colors = colors[:, :3]
|
|
else: # RGB
|
|
rgb_colors = colors
|
|
|
|
# 如果颜色值在0-1范围内,转换为0-255
|
|
if rgb_colors.max() <= 1.0:
|
|
rgb_colors = (rgb_colors * 255).astype(np.uint8)
|
|
else:
|
|
rgb_colors = rgb_colors.astype(np.uint8)
|
|
|
|
# 计算RGB总和
|
|
rgb_sum = rgb_colors.sum(axis=1)
|
|
|
|
# 应用黑色阈值过滤
|
|
if black_threshold > 0:
|
|
dark_mask = rgb_sum >= black_threshold
|
|
before_count = keep_mask.sum()
|
|
keep_mask &= dark_mask
|
|
after_count = keep_mask.sum()
|
|
removed_by_black = before_count - after_count
|
|
removed_count += removed_by_black
|
|
processing_log.append(f" 按RGB总和阈值({black_threshold})删除: {removed_by_black} 个点")
|
|
|
|
return removed_count
|
|
|
|
|
|
class GLBPointCloudBounds:
|
|
"""GLB点云包围盒计算器 - 计算包围盒并生成带可视化的预览文件(原模型数据保持不变)"""
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"glb_file_path": ("STRING", {
|
|
"default": "",
|
|
"tooltip": "GLB点云文件路径:输入需要计算包围盒的GLB格式点云文件。支持绝对路径或相对于ComfyUI output目录的相对路径。注意:此节点在原模型基础上添加预览信息,原模型数据保持不变"
|
|
}),
|
|
},
|
|
"optional": {
|
|
"bounding_box_type": (["axis_aligned", "oriented"], {
|
|
"default": "axis_aligned",
|
|
"tooltip": "包围盒类型:axis_aligned=轴对齐包围盒(AABB),计算快速;oriented=有向包围盒(OBB),体积最小"
|
|
}),
|
|
"units": (["meters", "centimeters", "millimeters"], {
|
|
"default": "meters",
|
|
"tooltip": "输出单位:指定包围盒尺寸的输出单位"
|
|
}),
|
|
"add_bounding_box_visualization": ("BOOLEAN", {
|
|
"default": True,
|
|
"tooltip": "添加包围盒可视化:是否在输出的点云文件中添加红色包围盒线框点云,便于直接预览验证"
|
|
}),
|
|
"add_coordinate_axes": ("BOOLEAN", {
|
|
"default": False,
|
|
"tooltip": "添加坐标轴:是否添加RGB坐标轴(X=红色,Y=绿色,Z=蓝色)到输出点云中"
|
|
}),
|
|
"wireframe_density": ("INT", {
|
|
"default": 150, "min": 20, "max": 200, "step": 10,
|
|
"tooltip": "线框密度:每条包围盒边的点数,越高线框越清晰但点数越多。推荐50-100获得清晰效果"
|
|
}),
|
|
"enhance_visibility": ("BOOLEAN", {
|
|
"default": True,
|
|
"tooltip": "增强可见性:添加顶点高亮和面中心标记,让包围盒更容易识别"
|
|
}),
|
|
"output_filename": ("STRING", {
|
|
"default": "pointcloud_with_bounds",
|
|
"tooltip": "输出文件名:带包围盒可视化的点云文件名,系统会自动添加.glb扩展名"
|
|
}),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = (
|
|
"STRING", # 输出的GLB文件路径
|
|
"STRING", # JSON格式的scale数组
|
|
)
|
|
RETURN_NAMES = (
|
|
"output_glb_path",
|
|
"scale_json",
|
|
)
|
|
OUTPUT_TOOLTIPS = [
|
|
"带预览的GLB文件路径 - 包含原始点云+包围盒可视化+坐标轴的完整点云文件",
|
|
"纯净的scale数组JSON: {\"scale\": [长, 宽, 高]} - 仅包含scale信息,可直接用于3D引擎缩放",
|
|
]
|
|
OUTPUT_NODE = True
|
|
FUNCTION = "calculate_bounds_and_visualize"
|
|
CATEGORY = "💃VVL/Point Cloud Analysis"
|
|
|
|
def calculate_bounds_and_visualize(self,
|
|
glb_file_path: str,
|
|
bounding_box_type: str = "axis_aligned",
|
|
units: str = "meters",
|
|
add_bounding_box_visualization: bool = True,
|
|
add_coordinate_axes: bool = True,
|
|
wireframe_density: int = 50,
|
|
enhance_visibility: bool = True,
|
|
output_filename: str = "pointcloud_with_bounds"):
|
|
"""
|
|
计算GLB点云文件的包围盒尺寸并生成带可视化的点云文件
|
|
"""
|
|
|
|
processing_log = []
|
|
processing_log.append("开始GLB点云包围盒计算和可视化生成...")
|
|
|
|
# 检查依赖
|
|
if not TRIMESH_AVAILABLE:
|
|
error_msg = "trimesh库不可用,无法处理GLB文件"
|
|
logger.error(error_msg)
|
|
processing_log.append(f"错误: {error_msg}")
|
|
return ("", "")
|
|
|
|
# 验证输入文件路径
|
|
if not glb_file_path or not glb_file_path.strip():
|
|
error_msg = "GLB文件路径为空"
|
|
logger.error(error_msg)
|
|
processing_log.append(f"错误: {error_msg}")
|
|
return ("", "")
|
|
|
|
# 处理文件路径
|
|
input_path = self._resolve_file_path(glb_file_path.strip())
|
|
self._current_input_path = input_path # 保存当前输入路径供后续使用
|
|
processing_log.append(f"输入文件路径: {input_path}")
|
|
|
|
if not os.path.exists(input_path):
|
|
error_msg = f"GLB文件不存在: {input_path}"
|
|
logger.error(error_msg)
|
|
processing_log.append(f"错误: {error_msg}")
|
|
return ("", "")
|
|
|
|
try:
|
|
# 加载GLB文件
|
|
processing_log.append("正在加载GLB文件...")
|
|
scene = trimesh.load(input_path)
|
|
|
|
# 收集所有点云数据,考虑变换矩阵
|
|
all_vertices = []
|
|
point_cloud_count = 0
|
|
|
|
if isinstance(scene, trimesh.Scene):
|
|
for node_name in scene.graph.nodes:
|
|
if node_name in scene.geometry:
|
|
geometry = scene.geometry[node_name]
|
|
transform_matrix = scene.graph[node_name][0] # 获取变换矩阵
|
|
|
|
if hasattr(geometry, 'vertices') and geometry.vertices is not None:
|
|
# 应用变换矩阵获取真实世界坐标
|
|
if transform_matrix is not None and not np.allclose(transform_matrix, np.eye(4)):
|
|
world_vertices = trimesh.transformations.transform_points(geometry.vertices, transform_matrix)
|
|
processing_log.append(f"发现几何体: {node_name}, 点数: {len(geometry.vertices)}, 已应用变换矩阵")
|
|
else:
|
|
world_vertices = geometry.vertices.copy()
|
|
processing_log.append(f"发现几何体: {node_name}, 点数: {len(geometry.vertices)}, 无变换")
|
|
|
|
all_vertices.append(world_vertices)
|
|
point_cloud_count += 1
|
|
elif isinstance(scene, trimesh.PointCloud):
|
|
all_vertices.append(scene.vertices)
|
|
point_cloud_count = 1
|
|
processing_log.append(f"发现点云: main_pointcloud, 点数: {len(scene.vertices)}")
|
|
elif hasattr(scene, 'vertices') and scene.vertices is not None:
|
|
all_vertices.append(scene.vertices)
|
|
point_cloud_count = 1
|
|
processing_log.append(f"发现几何体(作为点云处理): main_geometry, 点数: {len(scene.vertices)}")
|
|
else:
|
|
error_msg = "GLB文件中未找到点云或几何体数据"
|
|
processing_log.append(f"错误: {error_msg}")
|
|
return ("", "")
|
|
|
|
if not all_vertices:
|
|
error_msg = "GLB文件中没有可用的顶点数据"
|
|
processing_log.append(f"错误: {error_msg}")
|
|
return ("", "")
|
|
|
|
# 合并所有顶点(已经是世界坐标)
|
|
combined_vertices = np.vstack(all_vertices)
|
|
total_points = len(combined_vertices)
|
|
processing_log.append(f"合并了 {point_cloud_count} 个几何体,总点数: {total_points} (已应用变换矩阵)")
|
|
|
|
# 计算包围盒
|
|
processing_log.append(f"计算包围盒类型: {bounding_box_type}")
|
|
|
|
if bounding_box_type == "axis_aligned":
|
|
# 轴对齐包围盒 (AABB)
|
|
min_point = np.min(combined_vertices, axis=0)
|
|
max_point = np.max(combined_vertices, axis=0)
|
|
extents = max_point - min_point
|
|
center = (min_point + max_point) / 2
|
|
|
|
processing_log.append(f"AABB计算完成:")
|
|
processing_log.append(f" 最小点: [{min_point[0]:.6f}, {min_point[1]:.6f}, {min_point[2]:.6f}]")
|
|
processing_log.append(f" 最大点: [{max_point[0]:.6f}, {max_point[1]:.6f}, {max_point[2]:.6f}]")
|
|
processing_log.append(f" 中心点: [{center[0]:.6f}, {center[1]:.6f}, {center[2]:.6f}]")
|
|
processing_log.append(f" 尺寸: [{extents[0]:.6f}, {extents[1]:.6f}, {extents[2]:.6f}]")
|
|
processing_log.append(f" 体积: {np.prod(extents):.6f}")
|
|
|
|
else: # oriented
|
|
# 有向包围盒 (OBB)
|
|
try:
|
|
to_origin, obb_extents = trimesh.bounds.oriented_bounds(combined_vertices)
|
|
|
|
# 计算OBB的中心点
|
|
obb_center = -to_origin[:3, 3] # 变换矩阵的平移部分的负值
|
|
center = obb_center # 设置center变量
|
|
extents = obb_extents
|
|
|
|
processing_log.append(f"OBB计算完成:")
|
|
processing_log.append(f" 中心点: [{center[0]:.6f}, {center[1]:.6f}, {center[2]:.6f}]")
|
|
processing_log.append(f" 尺寸: [{extents[0]:.6f}, {extents[1]:.6f}, {extents[2]:.6f}]")
|
|
processing_log.append(f" 体积: {np.prod(extents):.6f}")
|
|
|
|
except Exception as e:
|
|
processing_log.append(f"OBB计算失败,回退到AABB: {str(e)}")
|
|
# 回退到AABB
|
|
min_point = np.min(combined_vertices, axis=0)
|
|
max_point = np.max(combined_vertices, axis=0)
|
|
extents = max_point - min_point
|
|
center = (min_point + max_point) / 2
|
|
|
|
|
|
|
|
# 应用单位转换
|
|
unit_scale = {"meters": 1.0, "centimeters": 100.0, "millimeters": 1000.0}
|
|
scale_factor = unit_scale.get(units, 1.0)
|
|
|
|
scaled_extents = extents * scale_factor
|
|
|
|
# 生成scale JSON (按照 [长, 宽, 高] 的顺序,通常是 [X, Y, Z])
|
|
scale_array = [float(scaled_extents[0]), float(scaled_extents[1]), float(scaled_extents[2])]
|
|
scale_json = {
|
|
"name": output_filename,
|
|
"scale": scale_array
|
|
}
|
|
|
|
|
|
|
|
processing_log.append(f"")
|
|
processing_log.append(f"输出结果 (单位: {units}):")
|
|
processing_log.append(f" Scale数组: [{scale_array[0]:.6f}, {scale_array[1]:.6f}, {scale_array[2]:.6f}]")
|
|
processing_log.append(f" 长度(X): {scale_array[0]:.6f} {units}")
|
|
processing_log.append(f" 宽度(Y): {scale_array[1]:.6f} {units}")
|
|
processing_log.append(f" 高度(Z): {scale_array[2]:.6f} {units}")
|
|
|
|
# 生成带可视化的点云文件(原模型数据不变,仅添加预览)
|
|
output_glb_path = ""
|
|
|
|
try:
|
|
processing_log.append("正在生成带包围盒可视化的点云文件...")
|
|
output_glb_path = self._generate_visualization_pointcloud(
|
|
combined_vertices, extents, center, bounding_box_type,
|
|
add_bounding_box_visualization, add_coordinate_axes,
|
|
wireframe_density, enhance_visibility, output_filename, processing_log
|
|
)
|
|
processing_log.append("可视化点云文件生成完成!")
|
|
except Exception as e:
|
|
processing_log.append(f"可视化点云生成失败: {str(e)}")
|
|
import traceback
|
|
traceback.print_exc()
|
|
|
|
processing_log.append("GLB点云包围盒计算和可视化完成!")
|
|
|
|
return (
|
|
output_glb_path,
|
|
json.dumps(scale_json, indent=2),
|
|
)
|
|
|
|
except Exception as e:
|
|
error_msg = f"计算包围盒时发生错误: {str(e)}"
|
|
logger.error(error_msg)
|
|
processing_log.append(f"错误: {error_msg}")
|
|
import traceback
|
|
traceback.print_exc()
|
|
return ("", "")
|
|
|
|
def _resolve_file_path(self, file_path: str) -> str:
|
|
"""解析文件路径,支持绝对路径和相对路径"""
|
|
if os.path.isabs(file_path):
|
|
return file_path
|
|
|
|
# 尝试相对于ComfyUI输出目录
|
|
if FOLDER_PATHS_AVAILABLE:
|
|
output_dir = folder_paths.get_output_directory()
|
|
candidate_path = os.path.join(output_dir, file_path)
|
|
if os.path.exists(candidate_path):
|
|
return candidate_path
|
|
|
|
# 尝试相对于当前工作目录
|
|
if os.path.exists(file_path):
|
|
return os.path.abspath(file_path)
|
|
|
|
# 返回原始路径(让后续检查处理错误)
|
|
return file_path
|
|
|
|
def _get_current_input_path(self):
|
|
"""获取当前输入文件路径"""
|
|
return getattr(self, '_current_input_path', None)
|
|
|
|
def _normalize_colors(self, colors):
|
|
"""标准化颜色数组为RGBA格式,0-255范围"""
|
|
if colors is None:
|
|
return None
|
|
|
|
colors = np.array(colors)
|
|
|
|
# 如果是0-1范围,转换为0-255
|
|
if colors.max() <= 1.0:
|
|
colors = (colors * 255).astype(np.uint8)
|
|
else:
|
|
colors = colors.astype(np.uint8)
|
|
|
|
# 确保是RGBA格式
|
|
if colors.shape[1] == 3: # RGB -> RGBA
|
|
alpha_channel = np.full((colors.shape[0], 1), 255, dtype=np.uint8)
|
|
colors = np.hstack([colors, alpha_channel])
|
|
elif colors.shape[1] != 4:
|
|
# 如果不是3或4通道,创建白色RGBA
|
|
colors = np.tile([255, 255, 255, 255], (colors.shape[0], 1)).astype(np.uint8)
|
|
|
|
return colors
|
|
|
|
def _generate_visualization_pointcloud(self, combined_vertices, extents, center, bounds_type,
|
|
add_bounding_box_visualization, add_coordinate_axes,
|
|
wireframe_density, enhance_visibility, output_filename, processing_log):
|
|
"""生成包含原始点云、包围盒线框和坐标轴的可视化点云文件"""
|
|
try:
|
|
# 直接使用已加载的原始场景数据,避免重新加载可能改变朝向
|
|
input_path = self._get_current_input_path()
|
|
|
|
# 提取原始点云数据(完全保持原始状态)
|
|
all_vertices = []
|
|
all_colors = []
|
|
original_point_count = 0
|
|
|
|
# 使用之前已经加载的combined_vertices作为基础,重新从原始场景提取完整数据
|
|
original_scene = trimesh.load(input_path)
|
|
|
|
# 创建一个新的场景,完全保持原始场景的结构和变换
|
|
visualization_scene = trimesh.Scene()
|
|
|
|
# 保持原始场景的完整结构
|
|
if isinstance(original_scene, trimesh.Scene):
|
|
# 完全复制原始场景的所有几何体和变换
|
|
for node_name in original_scene.graph.nodes:
|
|
if node_name in original_scene.geometry:
|
|
geometry = original_scene.geometry[node_name]
|
|
transform_matrix = original_scene.graph[node_name][0] # 获取变换矩阵
|
|
|
|
# 完全复制几何体,保持所有属性
|
|
copied_geometry = geometry.copy()
|
|
|
|
# 添加到新场景,保持原始的变换矩阵
|
|
visualization_scene.add_geometry(copied_geometry, node_name=node_name, transform=transform_matrix)
|
|
|
|
# 提取顶点用于统计(注意:这里用于显示统计,真实包围盒计算已在前面完成)
|
|
if hasattr(geometry, 'vertices') and geometry.vertices is not None:
|
|
# 这里不需要再次应用变换,因为包围盒计算已经在combined_vertices中处理过了
|
|
all_vertices.append(geometry.vertices)
|
|
|
|
# 保持原始颜色
|
|
if isinstance(geometry, trimesh.PointCloud):
|
|
if hasattr(geometry.visual, 'vertex_colors') and geometry.visual.vertex_colors is not None:
|
|
colors = geometry.visual.vertex_colors.copy()
|
|
elif hasattr(geometry, 'colors') and geometry.colors is not None:
|
|
colors = geometry.colors.copy()
|
|
else:
|
|
colors = np.tile([255, 255, 255, 255], (len(geometry.vertices), 1))
|
|
else:
|
|
if hasattr(geometry.visual, 'vertex_colors') and geometry.visual.vertex_colors is not None:
|
|
colors = geometry.visual.vertex_colors.copy()
|
|
else:
|
|
colors = np.tile([200, 200, 200, 255], (len(geometry.vertices), 1))
|
|
|
|
# 确保颜色格式一致 (RGBA, 0-255)
|
|
colors = self._normalize_colors(colors)
|
|
all_colors.append(colors)
|
|
original_point_count += len(geometry.vertices)
|
|
|
|
processing_log.append(f"完全保持原始几何体: {node_name}, 变换矩阵和所有属性已保留")
|
|
|
|
elif isinstance(original_scene, trimesh.PointCloud):
|
|
# 单个点云,直接复制
|
|
visualization_scene = original_scene.copy()
|
|
all_vertices.append(original_scene.vertices)
|
|
|
|
if hasattr(original_scene.visual, 'vertex_colors') and original_scene.visual.vertex_colors is not None:
|
|
colors = original_scene.visual.vertex_colors.copy()
|
|
elif hasattr(original_scene, 'colors') and original_scene.colors is not None:
|
|
colors = original_scene.colors.copy()
|
|
else:
|
|
colors = np.tile([255, 255, 255, 255], (len(original_scene.vertices), 1))
|
|
|
|
colors = self._normalize_colors(colors)
|
|
all_colors.append(colors)
|
|
original_point_count = len(original_scene.vertices)
|
|
processing_log.append(f"完全保持原始点云: {original_point_count:,} 个点")
|
|
|
|
else:
|
|
# 单个几何体,转换为场景保持结构
|
|
visualization_scene.add_geometry(original_scene.copy(), node_name="main_geometry")
|
|
if hasattr(original_scene, 'vertices') and original_scene.vertices is not None:
|
|
all_vertices.append(original_scene.vertices)
|
|
|
|
if hasattr(original_scene.visual, 'vertex_colors') and original_scene.visual.vertex_colors is not None:
|
|
colors = original_scene.visual.vertex_colors.copy()
|
|
else:
|
|
colors = np.tile([200, 200, 200, 255], (len(original_scene.vertices), 1))
|
|
|
|
colors = self._normalize_colors(colors)
|
|
all_colors.append(colors)
|
|
original_point_count = len(original_scene.vertices)
|
|
processing_log.append(f"完全保持原始几何体: {original_point_count:,} 个点")
|
|
|
|
processing_log.append(f"原始模型结构完全保留: {original_point_count:,} 个点,所有变换矩阵和朝向不变")
|
|
|
|
# 添加包围盒线框点云到场景
|
|
if add_bounding_box_visualization:
|
|
box_vertices, box_colors = self._create_bounding_box_pointcloud(
|
|
extents, center, bounds_type, wireframe_density, enhance_visibility, processing_log
|
|
)
|
|
if len(box_vertices) > 0:
|
|
# 创建包围盒点云并添加到场景
|
|
bounding_box_pointcloud = trimesh.PointCloud(vertices=box_vertices, colors=box_colors)
|
|
visualization_scene.add_geometry(bounding_box_pointcloud, node_name="bounding_box_visualization")
|
|
processing_log.append(f"包围盒线框: {len(box_vertices):,} 个点")
|
|
|
|
# 添加坐标轴点云到场景
|
|
if add_coordinate_axes:
|
|
axes_vertices, axes_colors = self._create_coordinate_axes_pointcloud(
|
|
extents, center, wireframe_density, processing_log
|
|
)
|
|
if len(axes_vertices) > 0:
|
|
# 创建坐标轴点云并添加到场景
|
|
coordinate_axes_pointcloud = trimesh.PointCloud(vertices=axes_vertices, colors=axes_colors)
|
|
visualization_scene.add_geometry(coordinate_axes_pointcloud, node_name="coordinate_axes")
|
|
processing_log.append(f"坐标轴: {len(axes_vertices):,} 个点")
|
|
|
|
# 统计总点数
|
|
total_visualization_points = original_point_count
|
|
box_point_count = 0
|
|
axes_point_count = 0
|
|
|
|
if add_bounding_box_visualization:
|
|
box_point_count = len(box_vertices) if 'box_vertices' in locals() and len(box_vertices) > 0 else 0
|
|
total_visualization_points += box_point_count
|
|
|
|
if add_coordinate_axes:
|
|
axes_point_count = len(axes_vertices) if 'axes_vertices' in locals() and len(axes_vertices) > 0 else 0
|
|
total_visualization_points += axes_point_count
|
|
|
|
processing_log.append(f"总点数: {total_visualization_points:,} 个点")
|
|
|
|
# 生成输出路径
|
|
output_path = self._generate_output_path(output_filename)
|
|
|
|
# 保存场景,完全保持原始模型的结构和朝向
|
|
visualization_scene.export(output_path)
|
|
|
|
processing_log.append(f"可视化点云已保存: {output_path}")
|
|
return output_path
|
|
|
|
except Exception as e:
|
|
processing_log.append(f"可视化点云生成失败: {str(e)}")
|
|
raise e
|
|
|
|
def _create_bounding_box_pointcloud(self, extents, center, bounds_type, wireframe_density, enhance_visibility, processing_log):
|
|
"""创建包围盒线框的点云表示"""
|
|
try:
|
|
# 计算包围盒的8个顶点
|
|
min_point = center - extents/2
|
|
max_point = center + extents/2
|
|
|
|
box_vertices = np.array([
|
|
[min_point[0], min_point[1], min_point[2]], # 0: min corner
|
|
[max_point[0], min_point[1], min_point[2]], # 1
|
|
[max_point[0], max_point[1], min_point[2]], # 2
|
|
[min_point[0], max_point[1], min_point[2]], # 3
|
|
[min_point[0], min_point[1], max_point[2]], # 4
|
|
[max_point[0], min_point[1], max_point[2]], # 5
|
|
[max_point[0], max_point[1], max_point[2]], # 6: max corner
|
|
[min_point[0], max_point[1], max_point[2]], # 7
|
|
])
|
|
|
|
# 定义包围盒的12条边
|
|
edges = [
|
|
(0, 1), (1, 2), (2, 3), (3, 0), # 底面
|
|
(4, 5), (5, 6), (6, 7), (7, 4), # 顶面
|
|
(0, 4), (1, 5), (2, 6), (3, 7), # 垂直边
|
|
]
|
|
|
|
wireframe_points = []
|
|
|
|
# 1. 生成线框边上的点(更密集)
|
|
for start_idx, end_idx in edges:
|
|
start_point = box_vertices[start_idx]
|
|
end_point = box_vertices[end_idx]
|
|
|
|
# 在每条边上生成密集的点
|
|
for i in range(wireframe_density):
|
|
t = i / (wireframe_density - 1) if wireframe_density > 1 else 0
|
|
point = start_point + t * (end_point - start_point)
|
|
wireframe_points.append(point)
|
|
|
|
# 2. 添加顶点高亮和面中心标记(可选增强可见性)
|
|
if enhance_visibility:
|
|
# 顶点高亮(每个顶点周围添加小点云)
|
|
vertex_highlight_radius = np.min(extents) * 0.01 # 顶点高亮半径
|
|
highlight_density = max(5, wireframe_density // 10) # 顶点周围的点数
|
|
|
|
for vertex in box_vertices:
|
|
# 在每个顶点周围创建小的点云球
|
|
for i in range(highlight_density):
|
|
# 生成随机方向
|
|
theta = np.random.uniform(0, 2 * np.pi)
|
|
phi = np.random.uniform(0, np.pi)
|
|
r = np.random.uniform(0, vertex_highlight_radius)
|
|
|
|
# 球坐标转换为笛卡尔坐标
|
|
x = vertex[0] + r * np.sin(phi) * np.cos(theta)
|
|
y = vertex[1] + r * np.sin(phi) * np.sin(theta)
|
|
z = vertex[2] + r * np.cos(phi)
|
|
|
|
wireframe_points.append([x, y, z])
|
|
|
|
# 面中心点标记
|
|
face_centers = [
|
|
# 底面中心
|
|
(box_vertices[0] + box_vertices[1] + box_vertices[2] + box_vertices[3]) / 4,
|
|
# 顶面中心
|
|
(box_vertices[4] + box_vertices[5] + box_vertices[6] + box_vertices[7]) / 4,
|
|
# 前面中心
|
|
(box_vertices[0] + box_vertices[1] + box_vertices[4] + box_vertices[5]) / 4,
|
|
# 后面中心
|
|
(box_vertices[2] + box_vertices[3] + box_vertices[6] + box_vertices[7]) / 4,
|
|
# 左面中心
|
|
(box_vertices[0] + box_vertices[3] + box_vertices[4] + box_vertices[7]) / 4,
|
|
# 右面中心
|
|
(box_vertices[1] + box_vertices[2] + box_vertices[5] + box_vertices[6]) / 4,
|
|
]
|
|
|
|
# 在每个面中心添加标记点
|
|
face_mark_density = max(3, wireframe_density // 20)
|
|
face_mark_radius = np.min(extents) * 0.005
|
|
|
|
for face_center in face_centers:
|
|
for i in range(face_mark_density):
|
|
# 在面中心周围添加小的点云
|
|
theta = np.random.uniform(0, 2 * np.pi)
|
|
phi = np.random.uniform(0, np.pi)
|
|
r = np.random.uniform(0, face_mark_radius)
|
|
|
|
x = face_center[0] + r * np.sin(phi) * np.cos(theta)
|
|
y = face_center[1] + r * np.sin(phi) * np.sin(theta)
|
|
z = face_center[2] + r * np.cos(phi)
|
|
|
|
wireframe_points.append([x, y, z])
|
|
|
|
wireframe_vertices = np.array(wireframe_points)
|
|
|
|
# 为包围盒线框设置红色
|
|
wireframe_colors = np.tile([255, 0, 0, 255], (len(wireframe_vertices), 1))
|
|
|
|
processing_log.append(f"包围盒线框生成: {len(wireframe_vertices):,} 个点 (密度={wireframe_density})")
|
|
|
|
return wireframe_vertices, wireframe_colors
|
|
|
|
except Exception as e:
|
|
processing_log.append(f"创建包围盒点云失败: {str(e)}")
|
|
return np.array([]), np.array([])
|
|
|
|
def _create_coordinate_axes_pointcloud(self, extents, origin_center, wireframe_density, processing_log):
|
|
"""创建坐标轴的点云表示"""
|
|
try:
|
|
axis_length = np.max(extents) * 0.4
|
|
axis_thickness = np.min(extents) * 0.005 # 坐标轴厚度
|
|
axes_points = []
|
|
axes_colors = []
|
|
|
|
# 计算坐标轴密度
|
|
axis_line_density = wireframe_density
|
|
axis_thickness_points = max(3, wireframe_density // 15) # 厚度方向的点数
|
|
|
|
# X轴 - 红色(粗线条)- 从点云中心开始
|
|
for i in range(axis_line_density):
|
|
t = i / (axis_line_density - 1) if axis_line_density > 1 else 0
|
|
base_point = [origin_center[0] + t * axis_length, origin_center[1], origin_center[2]]
|
|
|
|
# 主轴线
|
|
axes_points.append(base_point)
|
|
axes_colors.append([255, 0, 0, 255])
|
|
|
|
# 增加厚度(在YZ平面上添加点)
|
|
for j in range(axis_thickness_points):
|
|
for k in range(axis_thickness_points):
|
|
offset_y = (j - axis_thickness_points//2) * axis_thickness / axis_thickness_points
|
|
offset_z = (k - axis_thickness_points//2) * axis_thickness / axis_thickness_points
|
|
thick_point = [base_point[0], base_point[1] + offset_y, base_point[2] + offset_z]
|
|
axes_points.append(thick_point)
|
|
axes_colors.append([255, 0, 0, 255])
|
|
|
|
# X轴箭头头部
|
|
arrow_length = axis_length * 0.1
|
|
arrow_base = axis_length * 0.9
|
|
for i in range(axis_line_density // 2):
|
|
t = i / (axis_line_density // 2 - 1) if axis_line_density > 2 else 0
|
|
# 箭头点
|
|
arrow_x = origin_center[0] + arrow_base + t * arrow_length
|
|
arrow_offset = (1 - t) * axis_thickness * 2
|
|
|
|
axes_points.append([arrow_x, origin_center[1] + arrow_offset, origin_center[2]])
|
|
axes_points.append([arrow_x, origin_center[1] - arrow_offset, origin_center[2]])
|
|
axes_points.append([arrow_x, origin_center[1], origin_center[2] + arrow_offset])
|
|
axes_points.append([arrow_x, origin_center[1], origin_center[2] - arrow_offset])
|
|
axes_colors.extend([[255, 0, 0, 255]] * 4)
|
|
|
|
# Y轴 - 绿色(粗线条)- 从点云中心开始
|
|
for i in range(axis_line_density):
|
|
t = i / (axis_line_density - 1) if axis_line_density > 1 else 0
|
|
base_point = [origin_center[0], origin_center[1] + t * axis_length, origin_center[2]]
|
|
|
|
# 主轴线
|
|
axes_points.append(base_point)
|
|
axes_colors.append([0, 255, 0, 255])
|
|
|
|
# 增加厚度(在XZ平面上添加点)
|
|
for j in range(axis_thickness_points):
|
|
for k in range(axis_thickness_points):
|
|
offset_x = (j - axis_thickness_points//2) * axis_thickness / axis_thickness_points
|
|
offset_z = (k - axis_thickness_points//2) * axis_thickness / axis_thickness_points
|
|
thick_point = [base_point[0] + offset_x, base_point[1], base_point[2] + offset_z]
|
|
axes_points.append(thick_point)
|
|
axes_colors.append([0, 255, 0, 255])
|
|
|
|
# Y轴箭头头部
|
|
for i in range(axis_line_density // 2):
|
|
t = i / (axis_line_density // 2 - 1) if axis_line_density > 2 else 0
|
|
arrow_y = origin_center[1] + arrow_base + t * arrow_length
|
|
arrow_offset = (1 - t) * axis_thickness * 2
|
|
|
|
axes_points.append([origin_center[0] + arrow_offset, arrow_y, origin_center[2]])
|
|
axes_points.append([origin_center[0] - arrow_offset, arrow_y, origin_center[2]])
|
|
axes_points.append([origin_center[0], arrow_y, origin_center[2] + arrow_offset])
|
|
axes_points.append([origin_center[0], arrow_y, origin_center[2] - arrow_offset])
|
|
axes_colors.extend([[0, 255, 0, 255]] * 4)
|
|
|
|
# Z轴 - 蓝色(粗线条)- 从点云中心开始
|
|
for i in range(axis_line_density):
|
|
t = i / (axis_line_density - 1) if axis_line_density > 1 else 0
|
|
base_point = [origin_center[0], origin_center[1], origin_center[2] + t * axis_length]
|
|
|
|
# 主轴线
|
|
axes_points.append(base_point)
|
|
axes_colors.append([0, 0, 255, 255])
|
|
|
|
# 增加厚度(在XY平面上添加点)
|
|
for j in range(axis_thickness_points):
|
|
for k in range(axis_thickness_points):
|
|
offset_x = (j - axis_thickness_points//2) * axis_thickness / axis_thickness_points
|
|
offset_y = (k - axis_thickness_points//2) * axis_thickness / axis_thickness_points
|
|
thick_point = [base_point[0] + offset_x, base_point[1] + offset_y, base_point[2]]
|
|
axes_points.append(thick_point)
|
|
axes_colors.append([0, 0, 255, 255])
|
|
|
|
# Z轴箭头头部
|
|
for i in range(axis_line_density // 2):
|
|
t = i / (axis_line_density // 2 - 1) if axis_line_density > 2 else 0
|
|
arrow_z = origin_center[2] + arrow_base + t * arrow_length
|
|
arrow_offset = (1 - t) * axis_thickness * 2
|
|
|
|
axes_points.append([origin_center[0] + arrow_offset, origin_center[1], arrow_z])
|
|
axes_points.append([origin_center[0] - arrow_offset, origin_center[1], arrow_z])
|
|
axes_points.append([origin_center[0], origin_center[1] + arrow_offset, arrow_z])
|
|
axes_points.append([origin_center[0], origin_center[1] - arrow_offset, arrow_z])
|
|
axes_colors.extend([[0, 0, 255, 255]] * 4)
|
|
|
|
axes_vertices = np.array(axes_points)
|
|
axes_colors_array = np.array(axes_colors)
|
|
|
|
processing_log.append(f"坐标轴生成: {len(axes_vertices):,} 个点 (长度={axis_length:.3f}, 原点=[{origin_center[0]:.3f}, {origin_center[1]:.3f}, {origin_center[2]:.3f}])")
|
|
|
|
return axes_vertices, axes_colors_array
|
|
|
|
except Exception as e:
|
|
processing_log.append(f"创建坐标轴点云失败: {str(e)}")
|
|
return np.array([]), np.array([])
|
|
|
|
def _generate_output_path(self, output_filename):
|
|
"""生成输出文件路径"""
|
|
# 确保文件名有正确的扩展名
|
|
if not output_filename.lower().endswith('.glb'):
|
|
output_filename += '.glb'
|
|
|
|
# 生成输出路径
|
|
output_dir = folder_paths.get_output_directory() if FOLDER_PATHS_AVAILABLE else "output"
|
|
os.makedirs(output_dir, exist_ok=True)
|
|
|
|
# 添加时间戳避免文件名冲突
|
|
import time
|
|
timestamp = str(int(time.time()))
|
|
name_parts = output_filename.rsplit('.', 1)
|
|
if len(name_parts) == 2:
|
|
timestamped_filename = f"{name_parts[0]}_{timestamp}.{name_parts[1]}"
|
|
else:
|
|
timestamped_filename = f"{output_filename}_{timestamp}"
|
|
|
|
return os.path.join(output_dir, timestamped_filename)
|
|
|
|
|
|
class GLBPointCloudOriginAdjuster:
|
|
"""GLB点云原点调整计算器 - 计算原点调整信息并生成带坐标轴的预览文件(原模型数据保持不变)"""
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"glb_file_path": ("STRING", {
|
|
"default": "",
|
|
"tooltip": "GLB点云文件路径:输入需要调整原点的GLB格式点云文件。注意:此节点在原模型基础上添加坐标轴预览,原模型数据保持不变"
|
|
}),
|
|
},
|
|
"optional": {
|
|
"origin_mode": (["center", "bottom_center"], {
|
|
"default": "bottom_center",
|
|
"tooltip": "原点模式:center=点云几何中心;bottom_center=点云底部中心(脚底)"
|
|
}),
|
|
"output_units": (["meters", "centimeters", "millimeters"], {
|
|
"default": "centimeters",
|
|
"tooltip": "输出单位:变换信息的输出单位"
|
|
}),
|
|
"add_coordinate_axes": ("BOOLEAN", {
|
|
"default": True,
|
|
"tooltip": "添加坐标轴:是否添加RGB坐标轴(X=红色,Y=绿色,Z=蓝色)到输出点云中,显示调整后的原点位置"
|
|
}),
|
|
"wireframe_density": ("INT", {
|
|
"default": 100, "min": 20, "max": 200, "step": 10,
|
|
"tooltip": "坐标轴密度:每条坐标轴的点数,越高坐标轴越清晰但点数越多。推荐80-120获得清晰效果"
|
|
}),
|
|
"output_filename": ("STRING", {
|
|
"default": "adjusted_pointcloud",
|
|
"tooltip": "输出文件名:调整后的点云文件名,系统会自动添加.glb扩展名"
|
|
}),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = (
|
|
"STRING", # 处理后的GLB文件路径
|
|
"STRING", # 变换信息JSON (position)
|
|
)
|
|
RETURN_NAMES = (
|
|
"adjusted_glb_path",
|
|
"transform_info",
|
|
)
|
|
OUTPUT_TOOLTIPS = [
|
|
"带预览的GLB文件路径 - 包含原始点云+坐标轴预览的完整点云文件",
|
|
"纯净的position信息JSON: {\"position\": [x, y, z]} - 仅包含位置信息,可直接用于UE等引擎",
|
|
]
|
|
OUTPUT_NODE = True
|
|
FUNCTION = "adjust_origin"
|
|
CATEGORY = "💃VVL/Point Cloud Transform"
|
|
|
|
def adjust_origin(self,
|
|
glb_file_path: str,
|
|
origin_mode: str = "bottom_center",
|
|
output_units: str = "centimeters",
|
|
add_coordinate_axes: bool = False,
|
|
wireframe_density: int = 100,
|
|
output_filename: str = "adjusted_pointcloud"):
|
|
"""
|
|
调整GLB点云的原点位置和旋转
|
|
"""
|
|
|
|
processing_log = []
|
|
processing_log.append("开始GLB点云原点调整...")
|
|
|
|
# 检查依赖
|
|
if not TRIMESH_AVAILABLE:
|
|
error_msg = "trimesh库不可用,无法处理GLB文件"
|
|
logger.error(error_msg)
|
|
processing_log.append(f"错误: {error_msg}")
|
|
return ("", "")
|
|
|
|
# 验证输入文件路径
|
|
if not glb_file_path or not glb_file_path.strip():
|
|
error_msg = "GLB文件路径为空"
|
|
logger.error(error_msg)
|
|
processing_log.append(f"错误: {error_msg}")
|
|
return ("", "")
|
|
|
|
# 处理文件路径
|
|
input_path = self._resolve_file_path(glb_file_path.strip())
|
|
processing_log.append(f"输入文件路径: {input_path}")
|
|
|
|
if not os.path.exists(input_path):
|
|
error_msg = f"GLB文件不存在: {input_path}"
|
|
logger.error(error_msg)
|
|
processing_log.append(f"错误: {error_msg}")
|
|
return ("", "")
|
|
|
|
try:
|
|
# 加载GLB文件
|
|
processing_log.append("正在加载GLB文件...")
|
|
scene = trimesh.load(input_path)
|
|
|
|
# 收集所有几何体和点云,考虑变换矩阵
|
|
geometries_to_transform = []
|
|
all_vertices = []
|
|
total_points = 0
|
|
|
|
if isinstance(scene, trimesh.Scene):
|
|
for node_name in scene.graph.nodes:
|
|
if node_name in scene.geometry:
|
|
geometry = scene.geometry[node_name]
|
|
transform_matrix = scene.graph[node_name][0] # 获取变换矩阵
|
|
geometries_to_transform.append((node_name, geometry))
|
|
|
|
if hasattr(geometry, 'vertices') and geometry.vertices is not None:
|
|
# 应用变换矩阵获取真实世界坐标
|
|
if transform_matrix is not None and not np.allclose(transform_matrix, np.eye(4)):
|
|
world_vertices = trimesh.transformations.transform_points(geometry.vertices, transform_matrix)
|
|
processing_log.append(f"发现几何体: {node_name}, 顶点数: {len(geometry.vertices)}, 已应用变换矩阵")
|
|
else:
|
|
world_vertices = geometry.vertices.copy()
|
|
processing_log.append(f"发现几何体: {node_name}, 顶点数: {len(geometry.vertices)}, 无变换")
|
|
|
|
all_vertices.append(world_vertices)
|
|
total_points += len(geometry.vertices)
|
|
else:
|
|
# 单个几何体
|
|
geometries_to_transform.append(("main_geometry", scene))
|
|
if hasattr(scene, 'vertices') and scene.vertices is not None:
|
|
all_vertices.append(scene.vertices)
|
|
total_points = len(scene.vertices)
|
|
processing_log.append(f"发现几何体: main_geometry, 顶点数: {total_points}")
|
|
|
|
if not all_vertices:
|
|
error_msg = "GLB文件中没有可用的顶点数据"
|
|
processing_log.append(f"错误: {error_msg}")
|
|
return ("", "")
|
|
|
|
# 合并所有顶点计算包围盒(已经是世界坐标)
|
|
combined_vertices = np.vstack(all_vertices)
|
|
processing_log.append(f"总顶点数: {total_points:,} (已应用变换矩阵)")
|
|
|
|
# 计算原始包围盒
|
|
original_min = np.min(combined_vertices, axis=0)
|
|
original_max = np.max(combined_vertices, axis=0)
|
|
original_center = (original_min + original_max) / 2
|
|
original_size = original_max - original_min
|
|
|
|
processing_log.append(f"原始包围盒:")
|
|
processing_log.append(f" 最小点: [{original_min[0]:.6f}, {original_min[1]:.6f}, {original_min[2]:.6f}]")
|
|
processing_log.append(f" 最大点: [{original_max[0]:.6f}, {original_max[1]:.6f}, {original_max[2]:.6f}]")
|
|
processing_log.append(f" 中心点: [{original_center[0]:.6f}, {original_center[1]:.6f}, {original_center[2]:.6f}]")
|
|
processing_log.append(f" 尺寸: [{original_size[0]:.6f}, {original_size[1]:.6f}, {original_size[2]:.6f}]")
|
|
|
|
# 确定新的原点位置
|
|
if origin_mode == "center":
|
|
new_origin = original_center.copy()
|
|
processing_log.append("原点模式: 几何中心")
|
|
else: # bottom_center
|
|
new_origin = np.array([original_center[0], original_center[1], original_min[2]])
|
|
processing_log.append("原点模式: 底部中心(脚底)")
|
|
|
|
# 计算平移向量
|
|
translation = -new_origin
|
|
processing_log.append(f"平移向量: [{translation[0]:.6f}, {translation[1]:.6f}, {translation[2]:.6f}]")
|
|
|
|
# 创建平移变换矩阵(仅平移,无旋转)
|
|
transform_matrix = np.eye(4)
|
|
transform_matrix[:3, 3] = translation
|
|
|
|
# 应用变换到所有几何体
|
|
new_scene = trimesh.Scene()
|
|
transformed_vertices_list = []
|
|
|
|
for name, geometry in geometries_to_transform:
|
|
# 复制几何体
|
|
new_geometry = geometry.copy()
|
|
|
|
# 应用变换
|
|
new_geometry.apply_transform(transform_matrix)
|
|
|
|
# 添加到新场景
|
|
new_scene.add_geometry(new_geometry, node_name=name)
|
|
|
|
# 收集变换后的顶点用于统计
|
|
if hasattr(new_geometry, 'vertices') and new_geometry.vertices is not None:
|
|
transformed_vertices_list.append(new_geometry.vertices)
|
|
|
|
processing_log.append(f"已变换几何体: {name}")
|
|
|
|
# 添加坐标轴预览(在变换后的原点位置,即(0,0,0))
|
|
if add_coordinate_axes:
|
|
try:
|
|
# 计算变换后的包围盒来确定坐标轴长度
|
|
if transformed_vertices_list:
|
|
all_transformed_vertices = np.vstack(transformed_vertices_list)
|
|
transformed_extents = np.max(all_transformed_vertices, axis=0) - np.min(all_transformed_vertices, axis=0)
|
|
axis_length = np.max(transformed_extents) * 0.4
|
|
else:
|
|
axis_length = 1.0 # 默认长度
|
|
|
|
# 在新的原点(0,0,0)处生成坐标轴
|
|
axes_vertices, axes_colors = self._create_coordinate_axes_pointcloud_at_origin(
|
|
axis_length, wireframe_density, processing_log
|
|
)
|
|
|
|
if len(axes_vertices) > 0:
|
|
# 创建坐标轴点云
|
|
axes_pointcloud = trimesh.PointCloud(vertices=axes_vertices, colors=axes_colors)
|
|
new_scene.add_geometry(axes_pointcloud, node_name="coordinate_axes")
|
|
processing_log.append(f"坐标轴预览: {len(axes_vertices):,} 个点 (显示新的原点位置)")
|
|
|
|
except Exception as e:
|
|
processing_log.append(f"坐标轴生成失败: {str(e)}")
|
|
|
|
# 计算变换后的包围盒
|
|
if transformed_vertices_list:
|
|
transformed_vertices = np.vstack(transformed_vertices_list)
|
|
new_min = np.min(transformed_vertices, axis=0)
|
|
new_max = np.max(transformed_vertices, axis=0)
|
|
new_center = (new_min + new_max) / 2
|
|
new_size = new_max - new_min
|
|
|
|
processing_log.append(f"变换后包围盒:")
|
|
processing_log.append(f" 最小点: [{new_min[0]:.6f}, {new_min[1]:.6f}, {new_min[2]:.6f}]")
|
|
processing_log.append(f" 最大点: [{new_max[0]:.6f}, {new_max[1]:.6f}, {new_max[2]:.6f}]")
|
|
processing_log.append(f" 中心点: [{new_center[0]:.6f}, {new_center[1]:.6f}, {new_center[2]:.6f}]")
|
|
processing_log.append(f" 尺寸: [{new_size[0]:.6f}, {new_size[1]:.6f}, {new_size[2]:.6f}]")
|
|
|
|
# 生成带预览的输出文件(原模型数据不变,仅添加坐标轴预览)
|
|
output_path = self._generate_output_path(output_filename)
|
|
processing_log.append(f"输出文件路径: {output_path}")
|
|
|
|
# 创建包含原始点云和坐标轴的预览场景,完全保持原始结构
|
|
original_scene = trimesh.load(input_path) # 重新加载原始场景
|
|
|
|
# 创建预览场景,完全复制原始场景的结构
|
|
if isinstance(original_scene, trimesh.Scene):
|
|
preview_scene = trimesh.Scene()
|
|
# 完全复制原始场景的所有几何体和变换矩阵
|
|
for node_name in original_scene.graph.nodes:
|
|
if node_name in original_scene.geometry:
|
|
geometry = original_scene.geometry[node_name]
|
|
transform_matrix = original_scene.graph[node_name][0] # 获取变换矩阵
|
|
|
|
# 完全复制几何体,保持所有属性
|
|
copied_geometry = geometry.copy()
|
|
|
|
# 添加到预览场景,保持原始的变换矩阵
|
|
preview_scene.add_geometry(copied_geometry, node_name=node_name, transform=transform_matrix)
|
|
processing_log.append(f"完全保持原始几何体: {node_name}, 变换矩阵和朝向已保留")
|
|
else:
|
|
# 单个几何体的情况
|
|
preview_scene = trimesh.Scene()
|
|
preview_scene.add_geometry(original_scene.copy(), node_name="main_geometry")
|
|
processing_log.append("完全保持原始几何体: main_geometry, 所有属性已保留")
|
|
|
|
# 添加坐标轴预览(如果需要)
|
|
if add_coordinate_axes:
|
|
try:
|
|
# 使用原始包围盒来确定坐标轴长度
|
|
axis_length = np.max(original_size) * 0.4
|
|
|
|
# 在计算出的新原点位置生成坐标轴
|
|
axes_vertices, axes_colors = self._create_coordinate_axes_pointcloud_at_position(
|
|
new_origin, axis_length, wireframe_density, processing_log
|
|
)
|
|
|
|
if len(axes_vertices) > 0:
|
|
# 创建坐标轴点云并添加到场景
|
|
axes_pointcloud = trimesh.PointCloud(vertices=axes_vertices, colors=axes_colors)
|
|
preview_scene.add_geometry(axes_pointcloud, node_name="coordinate_axes_preview")
|
|
processing_log.append(f"坐标轴预览: {len(axes_vertices):,} 个点 (显示建议的新原点位置)")
|
|
|
|
except Exception as e:
|
|
processing_log.append(f"坐标轴生成失败: {str(e)}")
|
|
|
|
# 保存预览文件
|
|
processing_log.append("正在保存带预览的GLB文件...")
|
|
preview_scene.export(output_path)
|
|
|
|
if os.path.exists(output_path):
|
|
file_size = os.path.getsize(output_path)
|
|
processing_log.append(f"预览GLB文件保存成功,文件大小: {file_size} bytes")
|
|
else:
|
|
error_msg = "预览GLB文件保存失败"
|
|
processing_log.append(f"错误: {error_msg}")
|
|
return ("", "")
|
|
|
|
# 应用单位转换
|
|
unit_scale = {"meters": 1.0, "centimeters": 100.0, "millimeters": 1000.0}
|
|
scale_factor = unit_scale.get(output_units, 1.0)
|
|
|
|
# 生成变换信息(UE格式)
|
|
final_position = new_origin * scale_factor # 相对于原始坐标系的位置
|
|
transform_info = {
|
|
"name": output_filename,
|
|
"position": [float(final_position[0]), float(final_position[1]), float(final_position[2])]
|
|
}
|
|
|
|
processing_log.append("")
|
|
processing_log.append(f"变换信息 (单位: {output_units}):")
|
|
processing_log.append(f" Position: [{transform_info['position'][0]:.2f}, {transform_info['position'][1]:.2f}, {transform_info['position'][2]:.2f}]")
|
|
processing_log.append("原点调整预览生成完成!")
|
|
|
|
return (
|
|
output_path,
|
|
json.dumps(transform_info, indent=2),
|
|
)
|
|
|
|
except Exception as e:
|
|
error_msg = f"调整原点时发生错误: {str(e)}"
|
|
logger.error(error_msg)
|
|
processing_log.append(f"错误: {error_msg}")
|
|
import traceback
|
|
traceback.print_exc()
|
|
return ("", "")
|
|
|
|
def _resolve_file_path(self, file_path: str) -> str:
|
|
"""解析文件路径,支持绝对路径和相对路径"""
|
|
if os.path.isabs(file_path):
|
|
return file_path
|
|
|
|
# 尝试相对于ComfyUI输出目录
|
|
if FOLDER_PATHS_AVAILABLE:
|
|
output_dir = folder_paths.get_output_directory()
|
|
candidate_path = os.path.join(output_dir, file_path)
|
|
if os.path.exists(candidate_path):
|
|
return candidate_path
|
|
|
|
# 尝试相对于当前工作目录
|
|
if os.path.exists(file_path):
|
|
return os.path.abspath(file_path)
|
|
|
|
# 返回原始路径(让后续检查处理错误)
|
|
return file_path
|
|
|
|
def _generate_output_path(self, filename: str) -> str:
|
|
"""生成输出文件路径"""
|
|
# 确保文件名有正确的扩展名
|
|
if not filename.lower().endswith('.glb'):
|
|
filename += '.glb'
|
|
|
|
# 生成输出路径
|
|
output_dir = folder_paths.get_output_directory() if FOLDER_PATHS_AVAILABLE else "output"
|
|
os.makedirs(output_dir, exist_ok=True)
|
|
|
|
# 添加时间戳避免文件名冲突
|
|
timestamp = str(int(time.time()))
|
|
name_parts = filename.rsplit('.', 1)
|
|
if len(name_parts) == 2:
|
|
timestamped_filename = f"{name_parts[0]}_{timestamp}.{name_parts[1]}"
|
|
else:
|
|
timestamped_filename = f"{filename}_{timestamp}"
|
|
|
|
return os.path.join(output_dir, timestamped_filename)
|
|
|
|
|
|
|
|
def _create_coordinate_axes_pointcloud_at_position(self, position, axis_length, wireframe_density, processing_log):
|
|
"""在指定位置创建坐标轴的点云表示"""
|
|
try:
|
|
center = np.array(position) # 指定的位置
|
|
axis_thickness = axis_length * 0.01 # 坐标轴厚度
|
|
axes_points = []
|
|
axes_colors = []
|
|
|
|
# 计算坐标轴密度
|
|
axis_line_density = wireframe_density # 使用参数控制密度
|
|
axis_thickness_points = max(3, axis_line_density // 15) # 厚度方向的点数
|
|
|
|
# X轴 - 红色(从指定位置开始)
|
|
for i in range(axis_line_density):
|
|
t = i / (axis_line_density - 1) if axis_line_density > 1 else 0
|
|
base_point = [center[0] + t * axis_length, center[1], center[2]]
|
|
|
|
# 主轴线
|
|
axes_points.append(base_point)
|
|
axes_colors.append([255, 0, 0, 255])
|
|
|
|
# 增加厚度(在YZ平面上添加点)
|
|
for j in range(axis_thickness_points):
|
|
for k in range(axis_thickness_points):
|
|
offset_y = (j - axis_thickness_points//2) * axis_thickness / axis_thickness_points
|
|
offset_z = (k - axis_thickness_points//2) * axis_thickness / axis_thickness_points
|
|
thick_point = [base_point[0], base_point[1] + offset_y, base_point[2] + offset_z]
|
|
axes_points.append(thick_point)
|
|
axes_colors.append([255, 0, 0, 255])
|
|
|
|
# X轴箭头头部
|
|
arrow_length = axis_length * 0.1
|
|
arrow_base = axis_length * 0.9
|
|
for i in range(axis_line_density // 2):
|
|
t = i / (axis_line_density // 2 - 1) if axis_line_density > 2 else 0
|
|
arrow_x = center[0] + arrow_base + t * arrow_length
|
|
arrow_offset = (1 - t) * axis_thickness * 2
|
|
|
|
axes_points.append([arrow_x, center[1] + arrow_offset, center[2]])
|
|
axes_points.append([arrow_x, center[1] - arrow_offset, center[2]])
|
|
axes_points.append([arrow_x, center[1], center[2] + arrow_offset])
|
|
axes_points.append([arrow_x, center[1], center[2] - arrow_offset])
|
|
axes_colors.extend([[255, 0, 0, 255]] * 4)
|
|
|
|
# Y轴 - 绿色(从指定位置开始)
|
|
for i in range(axis_line_density):
|
|
t = i / (axis_line_density - 1) if axis_line_density > 1 else 0
|
|
base_point = [center[0], center[1] + t * axis_length, center[2]]
|
|
|
|
# 主轴线
|
|
axes_points.append(base_point)
|
|
axes_colors.append([0, 255, 0, 255])
|
|
|
|
# 增加厚度(在XZ平面上添加点)
|
|
for j in range(axis_thickness_points):
|
|
for k in range(axis_thickness_points):
|
|
offset_x = (j - axis_thickness_points//2) * axis_thickness / axis_thickness_points
|
|
offset_z = (k - axis_thickness_points//2) * axis_thickness / axis_thickness_points
|
|
thick_point = [base_point[0] + offset_x, base_point[1], base_point[2] + offset_z]
|
|
axes_points.append(thick_point)
|
|
axes_colors.append([0, 255, 0, 255])
|
|
|
|
# Y轴箭头头部
|
|
for i in range(axis_line_density // 2):
|
|
t = i / (axis_line_density // 2 - 1) if axis_line_density > 2 else 0
|
|
arrow_y = center[1] + arrow_base + t * arrow_length
|
|
arrow_offset = (1 - t) * axis_thickness * 2
|
|
|
|
axes_points.append([center[0] + arrow_offset, arrow_y, center[2]])
|
|
axes_points.append([center[0] - arrow_offset, arrow_y, center[2]])
|
|
axes_points.append([center[0], arrow_y, center[2] + arrow_offset])
|
|
axes_points.append([center[0], arrow_y, center[2] - arrow_offset])
|
|
axes_colors.extend([[0, 255, 0, 255]] * 4)
|
|
|
|
# Z轴 - 蓝色(从指定位置开始)
|
|
for i in range(axis_line_density):
|
|
t = i / (axis_line_density - 1) if axis_line_density > 1 else 0
|
|
base_point = [center[0], center[1], center[2] + t * axis_length]
|
|
|
|
# 主轴线
|
|
axes_points.append(base_point)
|
|
axes_colors.append([0, 0, 255, 255])
|
|
|
|
# 增加厚度(在XY平面上添加点)
|
|
for j in range(axis_thickness_points):
|
|
for k in range(axis_thickness_points):
|
|
offset_x = (j - axis_thickness_points//2) * axis_thickness / axis_thickness_points
|
|
offset_y = (k - axis_thickness_points//2) * axis_thickness / axis_thickness_points
|
|
thick_point = [base_point[0] + offset_x, base_point[1] + offset_y, base_point[2]]
|
|
axes_points.append(thick_point)
|
|
axes_colors.append([0, 0, 255, 255])
|
|
|
|
# Z轴箭头头部
|
|
for i in range(axis_line_density // 2):
|
|
t = i / (axis_line_density // 2 - 1) if axis_line_density > 2 else 0
|
|
arrow_z = center[2] + arrow_base + t * arrow_length
|
|
arrow_offset = (1 - t) * axis_thickness * 2
|
|
|
|
axes_points.append([center[0] + arrow_offset, center[1], arrow_z])
|
|
axes_points.append([center[0] - arrow_offset, center[1], arrow_z])
|
|
axes_points.append([center[0], center[1] + arrow_offset, arrow_z])
|
|
axes_points.append([center[0], center[1] - arrow_offset, arrow_z])
|
|
axes_colors.extend([[0, 0, 255, 255]] * 4)
|
|
|
|
axes_vertices = np.array(axes_points)
|
|
axes_colors_array = np.array(axes_colors)
|
|
|
|
processing_log.append(f"坐标轴生成: {len(axes_vertices):,} 个点 (长度={axis_length:.3f}, 密度={wireframe_density}, 位置=[{center[0]:.3f}, {center[1]:.3f}, {center[2]:.3f}])")
|
|
|
|
return axes_vertices, axes_colors_array
|
|
|
|
except Exception as e:
|
|
processing_log.append(f"创建坐标轴点云失败: {str(e)}")
|
|
return np.array([]), np.array([])
|
|
|
|
def _create_coordinate_axes_pointcloud_at_origin(self, axis_length, wireframe_density, processing_log):
|
|
"""在原点(0,0,0)创建坐标轴的点云表示"""
|
|
try:
|
|
origin_center = np.array([0.0, 0.0, 0.0]) # 新的原点位置
|
|
axis_thickness = axis_length * 0.01 # 坐标轴厚度
|
|
axes_points = []
|
|
axes_colors = []
|
|
|
|
# 计算坐标轴密度
|
|
axis_line_density = wireframe_density # 使用参数控制密度
|
|
axis_thickness_points = max(3, axis_line_density // 15) # 厚度方向的点数
|
|
|
|
# X轴 - 红色(从原点开始)
|
|
for i in range(axis_line_density):
|
|
t = i / (axis_line_density - 1) if axis_line_density > 1 else 0
|
|
base_point = [origin_center[0] + t * axis_length, origin_center[1], origin_center[2]]
|
|
|
|
# 主轴线
|
|
axes_points.append(base_point)
|
|
axes_colors.append([255, 0, 0, 255])
|
|
|
|
# 增加厚度(在YZ平面上添加点)
|
|
for j in range(axis_thickness_points):
|
|
for k in range(axis_thickness_points):
|
|
offset_y = (j - axis_thickness_points//2) * axis_thickness / axis_thickness_points
|
|
offset_z = (k - axis_thickness_points//2) * axis_thickness / axis_thickness_points
|
|
thick_point = [base_point[0], base_point[1] + offset_y, base_point[2] + offset_z]
|
|
axes_points.append(thick_point)
|
|
axes_colors.append([255, 0, 0, 255])
|
|
|
|
# X轴箭头头部
|
|
arrow_length = axis_length * 0.1
|
|
arrow_base = axis_length * 0.9
|
|
for i in range(axis_line_density // 2):
|
|
t = i / (axis_line_density // 2 - 1) if axis_line_density > 2 else 0
|
|
arrow_x = origin_center[0] + arrow_base + t * arrow_length
|
|
arrow_offset = (1 - t) * axis_thickness * 2
|
|
|
|
axes_points.append([arrow_x, origin_center[1] + arrow_offset, origin_center[2]])
|
|
axes_points.append([arrow_x, origin_center[1] - arrow_offset, origin_center[2]])
|
|
axes_points.append([arrow_x, origin_center[1], origin_center[2] + arrow_offset])
|
|
axes_points.append([arrow_x, origin_center[1], origin_center[2] - arrow_offset])
|
|
axes_colors.extend([[255, 0, 0, 255]] * 4)
|
|
|
|
# Y轴 - 绿色(从原点开始)
|
|
for i in range(axis_line_density):
|
|
t = i / (axis_line_density - 1) if axis_line_density > 1 else 0
|
|
base_point = [origin_center[0], origin_center[1] + t * axis_length, origin_center[2]]
|
|
|
|
# 主轴线
|
|
axes_points.append(base_point)
|
|
axes_colors.append([0, 255, 0, 255])
|
|
|
|
# 增加厚度(在XZ平面上添加点)
|
|
for j in range(axis_thickness_points):
|
|
for k in range(axis_thickness_points):
|
|
offset_x = (j - axis_thickness_points//2) * axis_thickness / axis_thickness_points
|
|
offset_z = (k - axis_thickness_points//2) * axis_thickness / axis_thickness_points
|
|
thick_point = [base_point[0] + offset_x, base_point[1], base_point[2] + offset_z]
|
|
axes_points.append(thick_point)
|
|
axes_colors.append([0, 255, 0, 255])
|
|
|
|
# Y轴箭头头部
|
|
for i in range(axis_line_density // 2):
|
|
t = i / (axis_line_density // 2 - 1) if axis_line_density > 2 else 0
|
|
arrow_y = origin_center[1] + arrow_base + t * arrow_length
|
|
arrow_offset = (1 - t) * axis_thickness * 2
|
|
|
|
axes_points.append([origin_center[0] + arrow_offset, arrow_y, origin_center[2]])
|
|
axes_points.append([origin_center[0] - arrow_offset, arrow_y, origin_center[2]])
|
|
axes_points.append([origin_center[0], arrow_y, origin_center[2] + arrow_offset])
|
|
axes_points.append([origin_center[0], arrow_y, origin_center[2] - arrow_offset])
|
|
axes_colors.extend([[0, 255, 0, 255]] * 4)
|
|
|
|
# Z轴 - 蓝色(从原点开始)
|
|
for i in range(axis_line_density):
|
|
t = i / (axis_line_density - 1) if axis_line_density > 1 else 0
|
|
base_point = [origin_center[0], origin_center[1], origin_center[2] + t * axis_length]
|
|
|
|
# 主轴线
|
|
axes_points.append(base_point)
|
|
axes_colors.append([0, 0, 255, 255])
|
|
|
|
# 增加厚度(在XY平面上添加点)
|
|
for j in range(axis_thickness_points):
|
|
for k in range(axis_thickness_points):
|
|
offset_x = (j - axis_thickness_points//2) * axis_thickness / axis_thickness_points
|
|
offset_y = (k - axis_thickness_points//2) * axis_thickness / axis_thickness_points
|
|
thick_point = [base_point[0] + offset_x, base_point[1] + offset_y, base_point[2]]
|
|
axes_points.append(thick_point)
|
|
axes_colors.append([0, 0, 255, 255])
|
|
|
|
# Z轴箭头头部
|
|
for i in range(axis_line_density // 2):
|
|
t = i / (axis_line_density // 2 - 1) if axis_line_density > 2 else 0
|
|
arrow_z = origin_center[2] + arrow_base + t * arrow_length
|
|
arrow_offset = (1 - t) * axis_thickness * 2
|
|
|
|
axes_points.append([origin_center[0] + arrow_offset, origin_center[1], arrow_z])
|
|
axes_points.append([origin_center[0] - arrow_offset, origin_center[1], arrow_z])
|
|
axes_points.append([origin_center[0], origin_center[1] + arrow_offset, arrow_z])
|
|
axes_points.append([origin_center[0], origin_center[1] - arrow_offset, arrow_z])
|
|
axes_colors.extend([[0, 0, 255, 255]] * 4)
|
|
|
|
axes_vertices = np.array(axes_points)
|
|
axes_colors_array = np.array(axes_colors)
|
|
|
|
processing_log.append(f"坐标轴生成: {len(axes_vertices):,} 个点 (长度={axis_length:.3f}, 密度={wireframe_density}, 原点=[0.000, 0.000, 0.000])")
|
|
|
|
return axes_vertices, axes_colors_array
|
|
|
|
except Exception as e:
|
|
processing_log.append(f"创建坐标轴点云失败: {str(e)}")
|
|
return np.array([]), np.array([])
|
|
|
|
|
|
class GLBPointCloudDensityFilter:
|
|
"""GLB点云密度过滤器 - 根据局部密度删除稀疏区域,保留密度最高的核心区域"""
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"glb_file_path": ("STRING", {
|
|
"default": "",
|
|
"tooltip": "GLB点云文件路径:输入需要进行密度过滤的GLB格式点云文件。支持绝对路径或相对于ComfyUI output目录的相对路径"
|
|
}),
|
|
},
|
|
"optional": {
|
|
"density_threshold": ("FLOAT", {
|
|
"default": 0.3, "min": 0.01, "max": 0.99, "step": 0.01,
|
|
"tooltip": "密度阈值:保留密度高于此比例的点。0.3=保留密度前70%的点;0.5=保留密度前50%的点。值越大删除越多稀疏点"
|
|
}),
|
|
"neighborhood_radius": ("FLOAT", {
|
|
"default": 0.1, "min": 0.01, "max": 1.0, "step": 0.01,
|
|
"tooltip": "邻域半径:计算密度时的搜索半径。较小值=精细密度检测;较大值=平滑密度检测。建议0.05-0.2"
|
|
}),
|
|
"min_neighbors": ("INT", {
|
|
"default": 5, "min": 1, "max": 50, "step": 1,
|
|
"tooltip": "最小邻居数:一个点在邻域内至少需要的邻居点数量才不被视为稀疏点。值越大要求密度越高"
|
|
}),
|
|
"preserve_core_percentage": ("FLOAT", {
|
|
"default": 0.8, "min": 0.1, "max": 1.0, "step": 0.05,
|
|
"tooltip": "核心区域保留比例:无论密度如何,都会保留密度最高的这部分点。0.8=保证保留最密集的80%区域"
|
|
}),
|
|
"use_adaptive_radius": ("BOOLEAN", {
|
|
"default": True,
|
|
"tooltip": "自适应半径:根据点云整体尺寸自动调整邻域半径,确保在不同尺寸的点云上都有良好效果"
|
|
}),
|
|
"output_filename": ("STRING", {
|
|
"default": "density_filtered_pointcloud",
|
|
"tooltip": "输出GLB文件名:密度过滤后点云的保存文件名,系统会自动添加.glb扩展名"
|
|
}),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = (
|
|
"STRING", # 处理后的GLB文件路径
|
|
"STRING", # 过滤统计信息JSON
|
|
)
|
|
RETURN_NAMES = (
|
|
"filtered_glb_path",
|
|
"filter_stats",
|
|
)
|
|
OUTPUT_TOOLTIPS = [
|
|
"密度过滤后的GLB文件完整路径",
|
|
"过滤统计信息JSON:包含原始点数、保留点数、删除点数等统计数据",
|
|
]
|
|
OUTPUT_NODE = True
|
|
FUNCTION = "filter_by_density"
|
|
CATEGORY = "💃VVL/Point Cloud Filtering"
|
|
|
|
def filter_by_density(self,
|
|
glb_file_path: str,
|
|
density_threshold: float = 0.3,
|
|
neighborhood_radius: float = 0.1,
|
|
min_neighbors: int = 5,
|
|
preserve_core_percentage: float = 0.8,
|
|
use_adaptive_radius: bool = True,
|
|
output_filename: str = "density_filtered_pointcloud"):
|
|
"""
|
|
根据局部密度过滤GLB点云文件,删除稀疏区域
|
|
"""
|
|
|
|
processing_log = []
|
|
processing_log.append("开始GLB点云密度过滤...")
|
|
|
|
# 检查依赖
|
|
if not TRIMESH_AVAILABLE:
|
|
error_msg = "trimesh库不可用,无法处理GLB文件"
|
|
logger.error(error_msg)
|
|
processing_log.append(f"错误: {error_msg}")
|
|
return ("", "")
|
|
|
|
# 验证输入文件路径
|
|
if not glb_file_path or not glb_file_path.strip():
|
|
error_msg = "GLB文件路径为空"
|
|
logger.error(error_msg)
|
|
processing_log.append(f"错误: {error_msg}")
|
|
return ("", "")
|
|
|
|
# 处理文件路径
|
|
input_path = self._resolve_file_path(glb_file_path.strip())
|
|
processing_log.append(f"输入文件路径: {input_path}")
|
|
|
|
if not os.path.exists(input_path):
|
|
error_msg = f"GLB文件不存在: {input_path}"
|
|
logger.error(error_msg)
|
|
processing_log.append(f"错误: {error_msg}")
|
|
return ("", "")
|
|
|
|
try:
|
|
# 加载GLB文件
|
|
processing_log.append("正在加载GLB文件...")
|
|
scene = trimesh.load(input_path)
|
|
|
|
# 提取点云数据
|
|
point_clouds = []
|
|
other_geometries = []
|
|
|
|
if isinstance(scene, trimesh.Scene):
|
|
for name, geometry in scene.geometry.items():
|
|
if isinstance(geometry, trimesh.PointCloud):
|
|
point_clouds.append((name, geometry))
|
|
processing_log.append(f"发现点云: {name}, 点数: {len(geometry.vertices)}")
|
|
else:
|
|
other_geometries.append((name, geometry))
|
|
elif isinstance(scene, trimesh.PointCloud):
|
|
point_clouds.append(("main_pointcloud", scene))
|
|
processing_log.append(f"发现点云: main_pointcloud, 点数: {len(scene.vertices)}")
|
|
else:
|
|
# 尝试转换为点云
|
|
if hasattr(scene, 'vertices'):
|
|
pc = trimesh.PointCloud(vertices=scene.vertices, colors=getattr(scene.visual, 'vertex_colors', None))
|
|
point_clouds.append(("converted_pointcloud", pc))
|
|
processing_log.append(f"转换为点云: converted_pointcloud, 点数: {len(pc.vertices)}")
|
|
else:
|
|
error_msg = "GLB文件中未找到点云数据"
|
|
processing_log.append(f"错误: {error_msg}")
|
|
return ("", "")
|
|
|
|
if not point_clouds:
|
|
error_msg = "GLB文件中没有点云数据"
|
|
processing_log.append(f"错误: {error_msg}")
|
|
return ("", "")
|
|
|
|
# 处理每个点云
|
|
filtered_point_clouds = []
|
|
total_original_points = 0
|
|
total_filtered_points = 0
|
|
|
|
for name, point_cloud in point_clouds:
|
|
processing_log.append(f"\n处理点云: {name}")
|
|
original_count = len(point_cloud.vertices)
|
|
total_original_points += original_count
|
|
|
|
# 获取原始点云的所有属性和状态
|
|
original_vertices = point_cloud.vertices.copy()
|
|
colors = None
|
|
|
|
# 获取颜色信息
|
|
if hasattr(point_cloud.visual, 'vertex_colors') and point_cloud.visual.vertex_colors is not None:
|
|
colors = point_cloud.visual.vertex_colors.copy()
|
|
elif hasattr(point_cloud, 'colors') and point_cloud.colors is not None:
|
|
colors = point_cloud.colors.copy()
|
|
|
|
# 应用密度过滤(仅基于密度,不改变顶点坐标)
|
|
keep_mask, filter_info = self._apply_density_filter(
|
|
original_vertices, colors, density_threshold, neighborhood_radius, min_neighbors,
|
|
preserve_core_percentage, use_adaptive_radius, processing_log
|
|
)
|
|
|
|
# 应用掩码,保留原始的顶点坐标
|
|
filtered_vertices = original_vertices[keep_mask]
|
|
filtered_colors = colors[keep_mask] if colors is not None else None
|
|
|
|
filtered_count = len(filtered_vertices)
|
|
total_filtered_points += filtered_count
|
|
|
|
processing_log.append(f"点云 {name}: 原始 {original_count} -> 保留 {filtered_count} (删除 {original_count - filtered_count})")
|
|
|
|
# 创建过滤后的点云,保留所有原始属性
|
|
if filtered_count > 0:
|
|
# 使用原始点云作为模板,只更新顶点和颜色
|
|
filtered_pc = point_cloud.copy()
|
|
filtered_pc.vertices = filtered_vertices
|
|
if filtered_colors is not None:
|
|
filtered_pc.visual.vertex_colors = filtered_colors
|
|
|
|
filtered_point_clouds.append((name, filtered_pc))
|
|
processing_log.append(f"保留点云 {name} 的所有原始属性和状态")
|
|
else:
|
|
processing_log.append(f"警告: 点云 {name} 过滤后没有剩余点")
|
|
|
|
# 直接修改原始场景,保持所有几何信息不变
|
|
if filtered_point_clouds:
|
|
# 使用原始场景作为基础,直接替换点云数据
|
|
new_scene = scene
|
|
|
|
# 直接修改原始场景中的点云几何体,保持变换不变
|
|
for name, filtered_pc in filtered_point_clouds:
|
|
if isinstance(scene, trimesh.Scene):
|
|
# 获取原始几何体
|
|
if name in scene.geometry:
|
|
original_geometry = scene.geometry[name]
|
|
|
|
# 如果是点云,直接修改顶点和颜色
|
|
if isinstance(original_geometry, trimesh.PointCloud):
|
|
original_geometry.vertices = filtered_pc.vertices
|
|
if hasattr(filtered_pc.visual, 'vertex_colors') and filtered_pc.visual.vertex_colors is not None:
|
|
original_geometry.visual.vertex_colors = filtered_pc.visual.vertex_colors
|
|
|
|
processing_log.append(f"直接修改原始点云 {name},保持所有变换和属性不变")
|
|
else:
|
|
# 如果不是点云类型,替换整个几何体但保持变换
|
|
scene.delete_geometry(name)
|
|
scene.add_geometry(filtered_pc, node_name=name)
|
|
processing_log.append(f"替换几何体 {name},尝试保持变换")
|
|
else:
|
|
# 新增几何体
|
|
scene.add_geometry(filtered_pc, node_name=name)
|
|
processing_log.append(f"添加新点云 {name}")
|
|
else:
|
|
# 单个几何体的情况
|
|
new_scene = filtered_pc
|
|
processing_log.append("单个几何体,直接使用过滤后的点云")
|
|
|
|
processing_log.append("直接修改原始场景,所有变换矩阵和几何属性完全保持不变")
|
|
|
|
# 生成输出路径
|
|
output_path = self._generate_output_path(output_filename)
|
|
processing_log.append(f"输出文件路径: {output_path}")
|
|
|
|
# 保存过滤后的GLB文件
|
|
processing_log.append("正在保存过滤后的GLB文件...")
|
|
new_scene.export(output_path)
|
|
|
|
# 验证文件是否保存成功
|
|
if os.path.exists(output_path):
|
|
file_size = os.path.getsize(output_path)
|
|
processing_log.append(f"GLB文件保存成功,文件大小: {file_size} bytes")
|
|
|
|
# 生成统计信息
|
|
filter_stats = {
|
|
"original_points": total_original_points,
|
|
"filtered_points": total_filtered_points,
|
|
"removed_points": total_original_points - total_filtered_points,
|
|
"retention_rate": total_filtered_points / total_original_points if total_original_points > 0 else 0,
|
|
"parameters": {
|
|
"density_threshold": density_threshold,
|
|
"neighborhood_radius": neighborhood_radius,
|
|
"min_neighbors": min_neighbors,
|
|
"preserve_core_percentage": preserve_core_percentage,
|
|
"use_adaptive_radius": use_adaptive_radius
|
|
}
|
|
}
|
|
|
|
stats_json = json.dumps(filter_stats, indent=2)
|
|
|
|
processing_log.append("GLB点云密度过滤完成! 已保留原始模型的大小、朝向和几何属性")
|
|
processing_log.append(f"统计: 原始{total_original_points}点 -> 保留{total_filtered_points}点 (保留率: {filter_stats['retention_rate']:.1%})")
|
|
|
|
return (output_path, stats_json)
|
|
else:
|
|
error_msg = "GLB文件保存失败"
|
|
processing_log.append(f"错误: {error_msg}")
|
|
return ("", "")
|
|
else:
|
|
error_msg = "过滤后没有剩余的点云数据"
|
|
processing_log.append(f"错误: {error_msg}")
|
|
return ("", "")
|
|
|
|
except Exception as e:
|
|
error_msg = f"处理GLB文件时发生错误: {str(e)}"
|
|
logger.error(error_msg)
|
|
processing_log.append(f"错误: {error_msg}")
|
|
import traceback
|
|
traceback.print_exc()
|
|
return ("", "")
|
|
|
|
def _apply_density_filter(self, vertices, colors, density_threshold, neighborhood_radius,
|
|
min_neighbors, preserve_core_percentage, use_adaptive_radius, processing_log):
|
|
"""
|
|
应用密度过滤算法,返回保留点的掩码
|
|
"""
|
|
|
|
n_points = len(vertices)
|
|
processing_log.append(f"开始密度分析,总点数: {n_points}")
|
|
|
|
# 自适应半径调整
|
|
if use_adaptive_radius:
|
|
# 计算点云的包围盒对角线长度
|
|
bbox_min = np.min(vertices, axis=0)
|
|
bbox_max = np.max(vertices, axis=0)
|
|
bbox_diagonal = np.linalg.norm(bbox_max - bbox_min)
|
|
|
|
# 根据包围盒大小调整半径
|
|
adaptive_radius = neighborhood_radius * bbox_diagonal / 10.0 # 可调整的比例因子
|
|
processing_log.append(f"自适应半径: {neighborhood_radius} -> {adaptive_radius:.4f} (基于包围盒对角线 {bbox_diagonal:.4f})")
|
|
neighborhood_radius = adaptive_radius
|
|
|
|
# 使用KDTree进行高效的邻域搜索
|
|
try:
|
|
from scipy.spatial import cKDTree
|
|
kdtree = cKDTree(vertices)
|
|
processing_log.append("使用scipy.spatial.cKDTree进行邻域搜索")
|
|
except ImportError:
|
|
processing_log.append("scipy不可用,使用简化的距离计算")
|
|
kdtree = None
|
|
|
|
# 计算每个点的局部密度
|
|
densities = np.zeros(n_points)
|
|
|
|
if kdtree is not None:
|
|
# 高效的KDTree搜索
|
|
for i in range(n_points):
|
|
# 查找邻域内的点
|
|
neighbor_indices = kdtree.query_ball_point(vertices[i], neighborhood_radius)
|
|
densities[i] = len(neighbor_indices) - 1 # 减去自己
|
|
else:
|
|
# 简化的距离计算(适用于小点云)
|
|
for i in range(n_points):
|
|
distances = np.linalg.norm(vertices - vertices[i], axis=1)
|
|
neighbor_count = np.sum(distances <= neighborhood_radius) - 1 # 减去自己
|
|
densities[i] = neighbor_count
|
|
|
|
processing_log.append(f"密度统计: 最小={densities.min():.1f}, 最大={densities.max():.1f}, 平均={densities.mean():.1f}")
|
|
|
|
# 应用多层过滤策略
|
|
keep_mask = np.ones(n_points, dtype=bool)
|
|
|
|
# 1. 基于最小邻居数的过滤
|
|
min_neighbor_mask = densities >= min_neighbors
|
|
removed_by_min_neighbors = np.sum(~min_neighbor_mask)
|
|
keep_mask &= min_neighbor_mask
|
|
processing_log.append(f"最小邻居数过滤: 删除 {removed_by_min_neighbors} 个点")
|
|
|
|
# 2. 基于密度阈值的过滤
|
|
if density_threshold > 0 and np.sum(keep_mask) > 0:
|
|
valid_densities = densities[keep_mask]
|
|
density_percentile = np.percentile(valid_densities, (1 - density_threshold) * 100)
|
|
density_mask = densities >= density_percentile
|
|
|
|
# 保留当前mask中的点,再应用密度过滤
|
|
temp_mask = keep_mask.copy()
|
|
keep_mask[temp_mask] &= density_mask[temp_mask]
|
|
|
|
removed_by_density = np.sum(temp_mask) - np.sum(keep_mask)
|
|
processing_log.append(f"密度阈值过滤: 删除 {removed_by_density} 个点 (阈值密度: {density_percentile:.1f})")
|
|
|
|
# 3. 核心区域保护
|
|
if preserve_core_percentage < 1.0 and np.sum(keep_mask) > 0:
|
|
# 计算需要保留的核心点数
|
|
target_core_points = int(n_points * preserve_core_percentage)
|
|
current_points = np.sum(keep_mask)
|
|
|
|
if current_points < target_core_points:
|
|
# 当前保留的点数少于核心要求,需要添加高密度点
|
|
need_more_points = target_core_points - current_points
|
|
|
|
# 找到密度最高的点
|
|
excluded_indices = np.where(~keep_mask)[0]
|
|
if len(excluded_indices) > 0:
|
|
excluded_densities = densities[excluded_indices]
|
|
# 按密度排序,选择密度最高的点
|
|
sorted_indices = excluded_indices[np.argsort(-excluded_densities)]
|
|
points_to_restore = min(need_more_points, len(sorted_indices))
|
|
|
|
keep_mask[sorted_indices[:points_to_restore]] = True
|
|
processing_log.append(f"核心区域保护: 恢复 {points_to_restore} 个高密度点")
|
|
|
|
# 统计信息
|
|
original_count = n_points
|
|
filtered_count = np.sum(keep_mask)
|
|
removed_count = original_count - filtered_count
|
|
|
|
filter_info = {
|
|
"original_points": original_count,
|
|
"filtered_points": filtered_count,
|
|
"removed_points": removed_count,
|
|
"retention_rate": filtered_count / original_count if original_count > 0 else 0,
|
|
"density_stats": {
|
|
"min": float(densities.min()),
|
|
"max": float(densities.max()),
|
|
"mean": float(densities.mean()),
|
|
"std": float(densities.std())
|
|
}
|
|
}
|
|
|
|
processing_log.append(f"密度过滤完成: 保留 {filtered_count}/{original_count} 点 (保留率: {filter_info['retention_rate']:.1%})")
|
|
|
|
return keep_mask, filter_info
|
|
|
|
def _resolve_file_path(self, file_path: str) -> str:
|
|
"""解析文件路径,支持绝对路径和相对路径"""
|
|
if os.path.isabs(file_path):
|
|
return file_path
|
|
|
|
# 尝试相对于ComfyUI输出目录
|
|
if FOLDER_PATHS_AVAILABLE:
|
|
output_dir = folder_paths.get_output_directory()
|
|
candidate_path = os.path.join(output_dir, file_path)
|
|
if os.path.exists(candidate_path):
|
|
return candidate_path
|
|
|
|
# 尝试相对于当前工作目录
|
|
if os.path.exists(file_path):
|
|
return os.path.abspath(file_path)
|
|
|
|
# 返回原始路径(让后续检查处理错误)
|
|
return file_path
|
|
|
|
def _generate_output_path(self, filename: str) -> str:
|
|
"""生成输出文件路径"""
|
|
if FOLDER_PATHS_AVAILABLE:
|
|
output_dir = folder_paths.get_output_directory()
|
|
else:
|
|
output_dir = "output"
|
|
|
|
# 确保输出目录存在
|
|
os.makedirs(output_dir, exist_ok=True)
|
|
|
|
# 处理文件名,确保是.glb扩展名
|
|
if not filename.lower().endswith('.glb'):
|
|
filename = f"{filename}.glb"
|
|
|
|
return os.path.join(output_dir, filename)
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# 节点注册 - 更新注册信息
|
|
# -----------------------------------------------------------------------------
|
|
|
|
NODE_CLASS_MAPPINGS = {
|
|
"GLBPointCloudProcessor": GLBPointCloudProcessor,
|
|
"GLBPointCloudBounds": GLBPointCloudBounds,
|
|
"GLBPointCloudOriginAdjuster": GLBPointCloudOriginAdjuster,
|
|
"GLBPointCloudDensityFilter": GLBPointCloudDensityFilter,
|
|
}
|
|
|
|
NODE_DISPLAY_NAME_MAPPINGS = {
|
|
"GLBPointCloudProcessor": "VVL GLB Point Cloud Processor",
|
|
"GLBPointCloudBounds": "VVL GLB Point Cloud Bounds Visualizer",
|
|
"GLBPointCloudOriginAdjuster": "VVL GLB Point Cloud Origin Adjuster",
|
|
"GLBPointCloudDensityFilter": "VVL GLB Point Cloud Density Filter",
|
|
}
|
|
|
|
# # 添加节点信息,帮助ComfyUI更好地识别
|
|
# WEB_DIRECTORY = "./web"
|
|
# __all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"] |