Files
wTechArtist-ComfyUI_VVL_Vid…/glb_point_cloud_processor.py
T
2025-06-18 15:02:10 +08:00

1382 lines
66 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.glb",
"tooltip": "输出GLB文件名:处理后点云的保存文件名,自动添加.glb扩展名。文件将保存到ComfyUI的output目录中。建议使用描述性名称如'clean_pointcloud.glb'、'filtered_scan.glb'等便于识别"
}),
}
}
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"):
"""
处理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
# 获取顶点和颜色
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(vertices), dtype=bool)
# 删除暗色点(默认启用)
if colors is not None:
removed_count = self._remove_dark_points(vertices, colors, keep_mask, black_threshold, processing_log)
processing_log.append(f"删除黑色点: {removed_count} 个")
else:
processing_log.append("跳过黑色点过滤: 点云无颜色信息")
# 应用掩码
filtered_vertices = 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 = trimesh.PointCloud(vertices=filtered_vertices, colors=filtered_colors)
processed_point_clouds.append((name, processed_pc))
else:
processing_log.append(f"警告: 点云 {name} 处理后没有剩余点")
# 创建新的场景
if processed_point_clouds:
new_scene = trimesh.Scene()
# 添加处理后的点云
for name, pc in processed_point_clouds:
new_scene.add_geometry(pc, node_name=name)
# 添加其他几何体(如相机模型)
for name, geometry in other_geometries:
new_scene.add_geometry(geometry, node_name=name)
processing_log.append(f"保留几何体: {name}")
# 生成输出路径
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")
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.glb",
"tooltip": "输出文件名:带包围盒可视化的点云文件名"
}),
}
}
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"):
"""
计算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 name, geometry in scene.geometry.items():
if isinstance(geometry, trimesh.PointCloud):
all_vertices.append(geometry.vertices)
point_cloud_count += 1
processing_log.append(f"发现点云: {name}, 点数: {len(geometry.vertices)}")
elif hasattr(geometry, 'vertices') and geometry.vertices is not None:
# 处理可能被转换为Mesh的点云
all_vertices.append(geometry.vertices)
point_cloud_count += 1
processing_log.append(f"发现几何体(作为点云处理): {name}, 点数: {len(geometry.vertices)}")
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] # 变换矩阵的平移部分的负值
extents = obb_extents
processing_log.append(f"OBB计算完成:")
processing_log.append(f" 中心点: [{obb_center[0]:.6f}, {obb_center[1]:.6f}, {obb_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 = {
"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:
# 重新加载原始GLB文件以保持颜色信息
input_path = self._get_current_input_path()
original_scene = trimesh.load(input_path)
# 提取原始点云数据(保持颜色)
all_vertices = []
all_colors = []
original_point_count = 0
if isinstance(original_scene, trimesh.Scene):
for name, geometry in original_scene.geometry.items():
if isinstance(geometry, trimesh.PointCloud):
all_vertices.append(geometry.vertices)
# 保持原始颜色
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))
# 确保颜色格式一致 (RGBA, 0-255)
colors = self._normalize_colors(colors)
all_colors.append(colors)
original_point_count += len(geometry.vertices)
processing_log.append(f"保留原始点云: {name}, {len(geometry.vertices):,} 个点,颜色已保持")
elif hasattr(geometry, 'vertices') and geometry.vertices is not None:
all_vertices.append(geometry.vertices)
# 处理可能被转换为Mesh的点云
if hasattr(geometry.visual, 'vertex_colors') and geometry.visual.vertex_colors is not None:
colors = geometry.visual.vertex_colors.copy()
else:
colors = np.tile([255, 255, 255, 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"保留原始几何体: {name}, {len(geometry.vertices):,} 个点,颜色已保持")
elif isinstance(original_scene, trimesh.PointCloud):
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))
# 确保颜色格式一致 (RGBA, 0-255)
colors = self._normalize_colors(colors)
all_colors.append(colors)
original_point_count = len(original_scene.vertices)
processing_log.append(f"保留原始点云: {original_point_count:,} 个点,颜色已保持")
elif 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([255, 255, 255, 255], (len(original_scene.vertices), 1))
# 确保颜色格式一致 (RGBA, 0-255)
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:
all_vertices.append(box_vertices)
all_colors.append(box_colors)
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:
all_vertices.append(axes_vertices)
all_colors.append(axes_colors)
processing_log.append(f"坐标轴: {len(axes_vertices):,} 个点")
# 合并所有点云
final_vertices = np.vstack(all_vertices)
final_colors = np.vstack(all_colors)
processing_log.append(f"总点数: {len(final_vertices):,} 个点")
# 生成输出路径
output_path = self._generate_output_path(output_filename)
# 创建trimesh点云对象
visualization_pointcloud = trimesh.PointCloud(
vertices=final_vertices,
colors=final_colors
)
# 保存为GLB文件
visualization_pointcloud.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.glb",
"tooltip": "输出文件名:调整后的点云文件名"
}),
}
}
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"):
"""
调整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 name, geometry in scene.geometry.items():
geometries_to_transform.append((name, geometry))
if hasattr(geometry, 'vertices') and geometry.vertices is not None:
all_vertices.append(geometry.vertices)
total_points += len(geometry.vertices)
processing_log.append(f"发现几何体: {name}, 顶点数: {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}")
# 保存变换后的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")
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 = {
"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("GLB点云原点调整完成!")
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_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([])
# -----------------------------------------------------------------------------
# 节点注册 - 更新注册信息
# -----------------------------------------------------------------------------
NODE_CLASS_MAPPINGS = {
"GLBPointCloudProcessor": GLBPointCloudProcessor,
"GLBPointCloudBounds": GLBPointCloudBounds,
"GLBPointCloudOriginAdjuster": GLBPointCloudOriginAdjuster,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"GLBPointCloudProcessor": "VVL GLB Point Cloud Processor",
"GLBPointCloudBounds": "VVL GLB Point Cloud Bounds Visualizer",
"GLBPointCloudOriginAdjuster": "VVL GLB Point Cloud Origin Adjuster",
}
# # 添加节点信息,帮助ComfyUI更好地识别
# WEB_DIRECTORY = "./web"
# __all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"]