fenge2.py
import trimesh import numpy as np def prepare_mesh(mesh): """预处理网格,使其适合布尔运算""" mesh = mesh.copy() mesh.merge_vertices() mesh.fix_normals() if not mesh.is_watertight: mesh = mesh.fill_holes() if hasattr(mesh, 'remove_degenerate_faces'): mesh.remove_degenerate_faces() mesh.remove_unreferenced_vertices() mesh.remove_infinite_values() return mesh def bounds_intersect(a_bounds, b_bounds): """检查两个AABB是否相交""" return not (a_bounds[0][0] > b_bounds[1][0] or a_bounds[1][0] < b_bounds[0][0] or a_bounds[0][1] > b_bounds[1][1] or a_bounds[1][1] < b_bounds[0][1] or a_bounds[0][2] > b_bounds[1][2] or a_bounds[1][2] < b_bounds[0][2]) def is_valid_mesh(geom, check_volume=True, min_vertices=3, min_faces=1): """检查网格是否有效(原函数不变)""" if not isinstance(geom, trimesh.Trimesh): return False if geom.vertices is None or geom.faces is None: return False if geom.vertices.shape[0] < min_vertices: return False if geom.faces.shape[0] < min_faces: return False if not np.all(np.isfinite(geom.vertices)): return False if not np.all(np.isfinite(geom.faces)): return False if geom.faces.max() >= geom.vertices.shape[0]: return False if geom.faces.min() < 0: return False try: bounds = geom.bounds if not np.all(np.isfinite(bounds)): return False size = bounds[1] - bounds[0] if np.any(size < 0): return False except: return False if check_volume: try: volume = geom.volume if abs(volume) < 1e-8: size = geom.bounds[1] - geom.bounds[0] if np.all(size > 1e-6): pass except: return False try: triangles = geom.vertices[geom.faces] v0 = triangles[:, 1] - triangles[:, 0] v1 = triangles[:, 2] - triangles[:, 0] cross = np.cross(v0, v1) areas = 0.5 * np.linalg.norm(cross, axis=1) if np.mean(areas) < 1e-10: return False except: pass try: used_vertices = np.unique(geom.faces.flatten()) if len(used_vertices) < geom.vertices.shape[0] * 0.5: pass except: pass return True def cut_scene_geometries(scene, engine='manifold'): """ 对场景中的所有部件进行两两切割(后面的被前面的切),并记录交面中心。 返回 (新场景, 交面信息列表) 交面信息: [(name_i, name_j, center_global, area), ...] # area 为交面总面积 """ if not isinstance(scene, trimesh.Scene): raise ValueError("输入必须是 trimesh.Scene") # 1. 将所有部件变换到世界坐标系 geom_names = [] world_geoms = [] for name, geom in scene.geometry.items(): if not isinstance(geom, trimesh.Trimesh): continue if name in scene.graph: transform = scene.graph[name][0] else: transform = np.eye(4) vertices = trimesh.transformations.transform_points(geom.vertices, transform) world_geom = trimesh.Trimesh(vertices=vertices, faces=geom.faces, process=False) try: world_geom = prepare_mesh(world_geom) print(f"预处理 {name} 成功") except Exception as e: print(f"预处理几何体 {name} 失败: {e}") geom_names.append(name) world_geoms.append(world_geom) # 2. 两两切割并记录交面 intersections = [] # 存储 (name_i, name_j, center_global, area) for i in range(len(world_geoms)): for j in range(i+1, len(world_geoms)): A = world_geoms[i] B = world_geoms[j] if not bounds_intersect(A.bounds, B.bounds): continue print(f"处理: {geom_names[i]} vs {geom_names[j]}") # --- 计算交集,记录交面中心 --- try: inter = trimesh.boolean.intersection([A, B], engine=engine) if inter is not None: if isinstance(inter, list) and len(inter) > 0: combined = trimesh.util.concatenate(inter) else: combined = inter if isinstance(combined, trimesh.Trimesh) and combined.vertices.shape[0] > 0 and combined.faces.shape[0] > 0: # 计算交面中心 face_centers = combined.vertices[combined.faces].mean(axis=1) center = np.mean(face_centers, axis=0) # 计算交面总面积(所有三角形面积之和) area = combined.area intersections.append((geom_names[i], geom_names[j], center, area)) print(f" 记录交面中心: {center}, 面积: {area:.6f}") else: print(" 交集为空或无效") else: print(" 交集返回 None") except Exception as e: print(f" 计算交集失败: {e}") # --- 用 A 切割 B: B = B - A --- try: result = trimesh.boolean.difference([B, A], engine=engine) if result is not None: if isinstance(result, list) and len(result) > 0: if len(result) > 1: volumes = [r.volume for r in result] result = result[np.argmax(volumes)] else: result = result[0] if isinstance(result, trimesh.Trimesh): world_geoms[j] = result print(f" 成功切割 {geom_names[j]}") else: print(f" 切割结果类型异常: {type(result)}") except Exception as e: print(f" 切割失败: {e}") # 3. 构建新场景(所有部件已是世界坐标,变换设为单位矩阵) new_scene = trimesh.Scene() for name, geom in zip(geom_names, world_geoms): new_scene.add_geometry(geom, geom_name=name, transform=np.eye(4)) return new_scene, intersections def cut_glb(input_path, output_path, engine='manifold'): """加载 GLB,切割所有部件,返回 (切割后的场景, 交面信息)""" scene = trimesh.load(input_path, force='scene') if not isinstance(scene, trimesh.Scene): mesh = trimesh.load(input_path) if isinstance(mesh, trimesh.Trimesh): scene = trimesh.Scene(mesh) else: raise ValueError("无法加载为场景或网格") # 检查几何体有效性(仅打印,不剔除) valid_geoms = [] invalid_geoms = [] empty_geoms = [] for name, geom in scene.geometry.items(): if not isinstance(geom, trimesh.Trimesh): invalid_geoms.append((name, f"类型错误: {type(geom)}")) elif geom.vertices.shape[0] == 0 or geom.faces.shape[0] == 0: empty_geoms.append((name, f"顶点:{geom.vertices.shape[0]}, 面:{geom.faces.shape[0]}")) elif not is_valid_mesh(geom, check_volume=False): invalid_geoms.append((name, "几何结构无效")) else: valid_geoms.append(name) if not valid_geoms: raise ValueError("警告:场景中没有有效的几何体") print(f"有效几何体: {len(valid_geoms)} 个") if invalid_geoms: print(f"⚠ 无效几何体: {len(invalid_geoms)} 个") for name, reason in invalid_geoms[:5]: print(f" - {name}: {reason}") if len(invalid_geoms) > 5: print(f" ... 还有 {len(invalid_geoms) - 5} 个无效几何体") if empty_geoms: print(f"⚠ 空几何体: {len(empty_geoms)} 个") print(f"加载场景,包含 {len(scene.geometry)} 个子部件") cut_scene, intersections = cut_scene_geometries(scene, engine=engine) cut_scene.export(output_path) print(f"切割后的场景已保存至: {output_path}") return cut_scene, intersections def explode_mesh(mesh, intersections=None, explosion_scale=0.4, area_threshold_ratio=0.06): """ 生成爆炸图,并在原本接触的部件之间绘制连接线。 intersections: 从 cut_glb 返回的交面信息列表 [(name_i, name_j, center_global, area), ...] area_threshold_ratio: 面积小于最大面积*ratio 的交面将被忽略(不绘制连接线) """ if isinstance(mesh, trimesh.Scene): scene = mesh elif isinstance(mesh, trimesh.Trimesh): print("Warning: Single mesh provided, can't create exploded view") scene = trimesh.Scene(mesh) return scene else: print(f"Warning: Unexpected mesh type: {type(mesh)}") scene = mesh if len(scene.geometry) <= 1: print("Only one geometry found - nothing to explode") return scene print(f"[EXPLODE_MESH] Starting mesh explosion with scale {explosion_scale}") print(f"[EXPLODE_MESH] Processing {len(scene.geometry)} parts") exploded_scene = trimesh.Scene() part_centers = [] geometry_names = [] for geometry_name, geometry in scene.geometry.items(): if hasattr(geometry, "vertices"): center = np.mean(geometry.vertices, axis=0) part_centers.append(center) geometry_names.append(geometry_name) print(f"[EXPLODE_MESH] Part {geometry_name}: center = {center}") if not part_centers: print("No valid geometries with vertices found") return scene part_centers = np.array(part_centers) global_center = np.mean(part_centers, axis=0) print(f"[EXPLODE_MESH] Global center: {global_center}") offsets = {} for i, (geometry_name, geometry) in enumerate(scene.geometry.items()): if hasattr(geometry, "vertices"): if i < len(part_centers): part_center = part_centers[i] direction = part_center - global_center direction_norm = np.linalg.norm(direction) if direction_norm > 1e-6: direction = direction / direction_norm else: direction = np.random.randn(3) direction = direction / np.linalg.norm(direction) offset = direction * explosion_scale offsets[geometry_name] = offset else: offset = np.zeros(3) offsets[geometry_name] = offset transform = np.eye(4) transform[:3, 3] = offset exploded_scene.add_geometry(geometry, transform=transform, geom_name=geometry_name) print(f"[EXPLODE_MESH] Part {geometry_name}: moved by {np.linalg.norm(offset):.4f}") # ---- 绘制连接线 ---- if intersections is not None and len(intersections) > 0: # 1. 计算最大面积(仅考虑有面积的项) areas = [item[3] for item in intersections if len(item) >= 4] if areas: max_area = max(areas) threshold = max_area * area_threshold_ratio print(f"[EXPLODE_MESH] 最大交面面积: {max_area:.6f}, 阈值(>{threshold:.6f})将保留连接线") else: max_area = None threshold = None # 收集所有线段的端点坐标和索引 all_points = [] line_indices = [] filtered_count = 0 for item in intersections: if len(item) >= 3: name_i, name_j, center = item[0], item[1], item[2] else: continue # 判断是否过滤 if max_area is not None and len(item) >= 4: area = item[3] if area < threshold: filtered_count += 1 print(f"[EXPLODE_MESH] 忽略小面积交面: {name_i} ∩ {name_j} (面积={area:.6f})") continue if name_i in offsets and name_j in offsets: p1 = center + offsets[name_i] p2 = center + offsets[name_j] idx1 = len(all_points) all_points.append(p1) idx2 = len(all_points) all_points.append(p2) line_indices.append([idx1, idx2]) print(f"[EXPLODE_MESH] Line between {name_i} and {name_j}") if filtered_count > 0: print(f"[EXPLODE_MESH] 共过滤掉 {filtered_count} 个小面积交面") if line_indices: vertices = np.array(all_points) # 为每条线段单独创建实体,避免歧义 entities = [] for idx_pair in line_indices: entities.append(trimesh.path.entities.Line(points=np.array(idx_pair))) path = trimesh.path.Path3D(entities=entities, vertices=vertices) exploded_scene.add_geometry(path, geom_name='connection_lines', transform=np.eye(4)) print(f"[EXPLODE_MESH] Added {len(line_indices)} connection lines") else: print("[EXPLODE_MESH] No connection lines to add (all filtered out or none)") print("[EXPLODE_MESH] Mesh explosion complete") return exploded_scene if __name__ == "__main__": input_file = r"baozha.glb" output_cut = "output_cut.glb" output_explode = "output_explode.glb" cut_scene, intersections = cut_glb(input_file, output_cut, engine='manifold') # 打印所有切割面的面积汇总 if intersections: total_area = 0.0 print("\n===== 切割面面积统计 =====") for item in intersections: if len(item) >= 4: name_i, name_j, center, area = item print(f" {name_i} ∩ {name_j}: 面积 = {area:.6f}") total_area += area else: # 兼容旧格式(无面积) print(f" {item[0]} ∩ {item[1]}: 面积 = (未记录)") print(f"总切割面积: {total_area:.6f}") print("==========================\n") else: print("没有检测到切割面。") explode_scene = explode_mesh(cut_scene, intersections=intersections, explosion_scale=0.1, area_threshold_ratio=0.06) explode_scene.export(output_explode) print(f"爆炸图已保存至: {output_explode}")