EG/scene/scene_manager.py
Hector f4b16c36c6 Merge remote-tracking branch 'origin/main_ch_eg' into addRender
# Conflicts:
#	gui/gui_manager.py
#	main.py
#	scene/scene_manager.py
#	ui/main_window.py
#	ui/property_panel.py
2025-08-28 16:32:26 +08:00

2044 lines
80 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
场景管理器 - 负责场景和模型管理的核心功能
处理模型导入、场景树构建、材质系统、碰撞设置等
"""
import os
from PyQt5.QtCore import Qt
from panda3d.core import (
ModelPool, ModelRoot, Filename, NodePath, GeomNode, Material, Vec4, Vec3,
MaterialAttrib, ColorAttrib, Point3, CollisionNode, CollisionSphere,
BitMask32, TransparencyAttrib,LColor
)
import json
import aiohttp
import asyncio
from pathlib import Path
from panda3d.egg import EggData, EggVertexPool
from direct.actor.Actor import Actor
from QPanda3D.Panda3DWorld import get_render_pipeline
from scene import util
class CesiumIntegration:
def __init__(self, scene_manager):
self.scene_manager = scene_manager
self.world = scene_manager.world
self.tilesets = {}
def add_tileset(self,name,url,position=(0,0,0)):
try:
tileset_node = self.scene_manager.load_cesium_tileset(url,position)
if tileset_node:
self.tilesets[name] = {
'node':tileset_node,
'url':url,
'position':position
}
print(f"✓ 添加 Cesium tileset: {name}")
return tileset_node
else:
print(f"✗ 添加 Cesium tileset 失败: {name}")
return None
except Exception as e:
print(f"✗ 添加 Cesium tileset 出错: {e}")
return None
def remove_tileset(self, name):
"""移除 tileset"""
if name in self.tilesets:
tileset_info = self.tilesets[name]
tileset_info['node'].removeNode()
del self.tilesets[name]
print(f"✓ 移除 Cesium tileset: {name}")
return True
return False
def get_tileset(self, name):
"""获取 tileset"""
return self.tilesets.get(name, None)
def list_tilesets(self):
"""列出所有 tilesets"""
return list(self.tilesets.keys())
class SceneManager:
"""场景管理器 - 统一管理场景中的所有元素"""
def __init__(self, world):
"""初始化场景管理器
Args:
world: 主程序world对象引用
"""
self.world = world
self.models = [] # 模型列表
self.Spotlight = []
self.Pointlight = []
self.tilesets = [] #来存储tilesets
self.cesium_integration = CesiumIntegration(self)
print("✓ 场景管理系统初始化完成")
# ==================== 模型导入和处理 ====================
def importModel(self, filepath, apply_unit_conversion=False, normalize_scales=True, auto_convert_to_glb=True):
"""导入模型到场景 - 只在根节点下创建
Args:
filepath: 模型文件路径
apply_unit_conversion: 是否应用单位转换主要针对FBX文件
normalize_scales: 是否标准化子节点缩放(推荐开启)
auto_convert_to_glb: 是否自动将非GLB格式转换为GLB以获得更好的动画支持
"""
try:
print(f"\n💾 开始导入模型: {os.path.basename(filepath)}")
print(f"单位转换: {'开启' if apply_unit_conversion else '关闭'}")
print(f"缩放标准化: {'开启' if normalize_scales else '关闭'}")
print(f"自动转换GLB: {'开启' if auto_convert_to_glb else '关闭'}")
# 预处理文件路径和转换
filepath = util.normalize_model_path(filepath)
original_filepath = filepath
# 检查是否需要转换为GLB
if auto_convert_to_glb and self._shouldConvertToGLB(filepath):
print(f"🔄 检测到需要转换的格式尝试转换为GLB...")
converted_path = self._convertToGLBWithProgress(filepath)
if converted_path:
print(f"✅ 转换成功: {converted_path}")
filepath = converted_path
# 显示成功消息
try:
from PyQt5.QtWidgets import QMessageBox
original_ext = os.path.splitext(original_filepath)[1].upper()
QMessageBox.information(None, "转换成功",
f"已将 {original_ext} 格式自动转换为 GLB 格式\n以获得更好的动画支持!")
except:
pass
else:
print(f"⚠️ 转换失败,使用原始文件")
# 直接在render根节点下创建模型
print("--- 在根节点下创建模型实例 ---")
# 加载模型
print("直接从文件加载模型...")
model = self.world.loader.loadModel(filepath)
if not model:
print("❌ 加载模型失败")
return None
# 设置模型名称
model_name = os.path.basename(filepath)
model.setName(model_name)
# 将模型挂载到render根节点
model.reparentTo(self.world.render)
# 设置标签和路径信息
model.setTag("model_path", filepath)
model.setTag("original_path", original_filepath)
model.setTag("file", model.getName())
model.setTag("is_model_root", "1")
model.setTag("is_scene_element", "1")
model.setTag("created_by_user", "1")
if filepath != original_filepath:
model.setTag("converted_from", os.path.splitext(original_filepath)[1])
model.setTag("converted_to_glb", "true")
# 应用处理选项
if apply_unit_conversion and filepath.lower().endswith('.fbx'):
print("应用FBX单位转换厘米到米...")
self._applyUnitConversion(model, 0.01)
model.setTag("unit_conversion_applied", "true")
if normalize_scales and filepath.lower().endswith('.fbx'):
print("标准化FBX模型缩放层级...")
self._normalizeModelScales(model)
model.setTag("scale_normalization_applied", "true")
# 调整模型位置到地面
self._adjustModelToGround(model)
# 创建并设置基础材质
print("设置材质...")
self._applyMaterialsToModel(model)
# 设置碰撞检测(重要!用于选择功能)
print("设置碰撞检测...")
self.setupCollision(model)
# 添加到模型列表
self.models.append(model)
print(f"✅ 创建模型成功: {model.getName()}")
# 获取树形控件并添加到Qt树中
tree_widget = self._get_tree_widget()
if tree_widget:
# 找到根节点项
root_item = None
for i in range(tree_widget.topLevelItemCount()):
item = tree_widget.topLevelItem(i)
if item.text(0) == "render" or item.data(0, Qt.UserRole) == self.world.render:
root_item = item
break
if root_item:
qt_item = tree_widget.add_node_to_tree_widget(model, root_item, "IMPORTED_MODEL_NODE")
if qt_item:
tree_widget.setCurrentItem(qt_item)
# 更新选择和属性面板
tree_widget.update_selection_and_properties(model, qt_item)
print("✅ Qt树节点添加成功")
else:
print("⚠️ Qt树节点添加失败但Panda3D对象已创建")
else:
print("⚠️ 未找到根节点项无法添加到Qt树")
else:
print("⚠️ 无法访问树形控件")
print(f"🎉 模型导入完成")
return model
except Exception as e:
print(f"❌ 导入模型过程失败: {str(e)}")
import traceback
traceback.print_exc()
return None
# def importAnimatedFBX(self, filepath, scale=0.01, auto_play=True):
# """导入带动画的FBX模型使用assimp
#
# Args:
# filepath: FBX文件路径
# scale: 缩放比例默认0.01,从厘米转换到米)
# auto_play: 是否自动播放第一个动画
#
# Returns:
# Actor对象如果加载失败返回None
# """
# try:
# print(f"\n=== 导入动画FBX模型: {filepath} ===")
# filepath = util.normalize_model_path(filepath)
#
# # 使用动画管理器加载FBX
# actor = self.animation_manager.load_fbx_with_animations(filepath, scale)
#
# if actor:
# # 设置模型名称
# model_name = os.path.basename(filepath)
# actor.setName(model_name)
#
# # 调整模型位置到地面
# self._adjustModelToGround(actor)
#
# # 设置碰撞检测
# self.setupCollision(actor)
#
# # 添加文件标签
# actor.setTag("file", model_name)
# actor.setTag("is_animated_model", "1")
#
# # 添加到模型列表
# self.models.append(actor)
#
# # 自动播放第一个动画
# if auto_play:
# available_anims = self.animation_manager.get_available_animations(actor)
# if available_anims:
# first_anim = available_anims[0]
# self.animation_manager.play_animation(actor, first_anim, loop=True)
# print(f"🎬 自动播放动画: {first_anim}")
#
# # 更新场景树
# self.updateSceneTree()
#
# print(f"=== 动画FBX模型导入成功: {model_name} ===\n")
# return actor
# else:
# print("❌ 动画FBX模型导入失败")
# return None
#
# except Exception as e:
# print(f"导入动画FBX模型失败: {str(e)}")
# import traceback
# traceback.print_exc()
# return None
def _applyMaterialsToModel(self, model):
"""递归应用材质到模型的所有GeomNode"""
def apply_material(node_path, depth=0):
indent = " " * depth
print(f"{indent}处理节点: {node_path.getName()}")
print(f"{indent}节点类型: {node_path.node().__class__.__name__}")
if isinstance(node_path.node(), GeomNode):
print(f"{indent}发现GeomNode处理材质")
geom_node = node_path.node()
# 检查所有几何体的状态
has_color = False
color = None
# 首先检查节点自身的状态
node_state = node_path.getState()
if node_state.hasAttrib(MaterialAttrib.getClassType()):
mat_attrib = node_state.getAttrib(MaterialAttrib.getClassType())
node_material = mat_attrib.getMaterial()
if node_material and node_material.hasDiffuse():
color = node_material.getDiffuse()
has_color = True
print(f"{indent}从节点材质获取颜色: {color}")
# 检查FBX特有的属性
for tag_key in node_path.getTagKeys():
print(f"{indent}发现标签: {tag_key}")
if "color" in tag_key.lower() or "diffuse" in tag_key.lower():
tag_value = node_path.getTag(tag_key)
print(f"{indent}颜色相关标签: {tag_key} = {tag_value}")
# 如果还没找到颜色,检查几何体
if not has_color:
for i in range(geom_node.getNumGeoms()):
geom = geom_node.getGeom(i)
state = geom_node.getGeomState(i)
# 检查顶点颜色
vdata = geom.getVertexData()
format = vdata.getFormat()
for j in range(format.getNumColumns()):
column = format.getColumn(j)
# InternalName对象需要使用getName()转换为字符串
column_name = column.getName().getName()
if "color" in column_name.lower():
print(f"{indent}发现顶点颜色数据: {column_name}")
# 这里可以读取顶点颜色,但先记录发现
# 检查材质属性
if state.hasAttrib(MaterialAttrib.getClassType()):
mat_attrib = state.getAttrib(MaterialAttrib.getClassType())
orig_material = mat_attrib.getMaterial()
if orig_material:
if orig_material.hasBaseColor():
color = orig_material.getBaseColor()
has_color = True
print(f"{indent}从基础颜色获取: {color}")
break
elif orig_material.hasDiffuse():
color = orig_material.getDiffuse()
has_color = True
print(f"{indent}从漫反射颜色获取: {color}")
break
# 检查颜色属性
if not has_color and state.hasAttrib(ColorAttrib.getClassType()):
color_attrib = state.getAttrib(ColorAttrib.getClassType())
if not color_attrib.isOff():
color = color_attrib.getColor()
has_color = True
print(f"{indent}从颜色属性获取: {color}")
break
# 创建新材质
material = Material()
if has_color:
print(f"{indent}应用找到的颜色: {color}")
material.setDiffuse(color)
material.setBaseColor(color) # 同时设置基础颜色
node_path.setColor(color)
else:
print(f"{indent}使用默认颜色")
material.setDiffuse((0.8, 0.8, 0.8, 1.0))
# 设置其他材质属性
material.setAmbient((0.2, 0.2, 0.2, 1.0))
material.setSpecular((0.5, 0.5, 0.5, 1.0))
material.setShininess(32.0)
#material.set_metallic(1)
#material.set_roughness(0)
# 应用材质
node_path.setMaterial(material)
print(f"{indent}几何体数量: {geom_node.getNumGeoms()}")
# 递归处理子节点
child_count = node_path.getNumChildren()
print(f"{indent}子节点数量: {child_count}")
for i in range(child_count):
child = node_path.getChild(i)
apply_material(child, depth + 1)
# 应用材质
print("\n开始递归应用材质...")
apply_material(model)
print("=== 材质设置完成 ===\n")
def _adjustModelToGround(self, model):
"""智能调整模型到地面,但保持原有缩放结构"""
try:
print("调整模型位置到地面...")
# 获取模型的边界框
bounds = model.getBounds()
if not bounds or bounds.isEmpty():
print("无法获取模型边界,使用默认位置")
model.setPos(0, 0, 0)
return
# 获取边界框的最低点
min_point = bounds.getMin()
center = bounds.getCenter()
# 计算需要移动的距离使模型底部贴合地面Z=0
# 这里不涉及缩放,只是简单的位置调整
ground_offset = -min_point.getZ()
# 设置模型位置X,Y居中Z调整到地面
model.setPos(0, 0, ground_offset)
print(f"模型边界: 最小点{min_point}, 中心{center}")
print(f"地面偏移: {ground_offset}")
print(f"最终位置: {model.getPos()}")
except Exception as e:
print(f"调整模型位置失败: {str(e)}")
# 失败时使用默认位置
model.setPos(0, 0, 0)
def _applyUnitConversion(self, model, scale_factor):
"""应用单位转换缩放
Args:
model: 要转换的模型
scale_factor: 缩放因子如0.01表示从厘米转换到米)
"""
try:
print(f"应用单位转换缩放: {scale_factor}")
# 检查模型是否已经应用过单位转换
if model.hasTag("unit_conversion_applied"):
print("模型已应用过单位转换,跳过")
return
# 获取当前边界用于后续位置调整
original_bounds = model.getBounds()
# 应用缩放
model.setScale(scale_factor)
# 重新调整位置(因为缩放会影响边界)
if original_bounds and not original_bounds.isEmpty():
new_bounds = model.getBounds()
min_point = new_bounds.getMin()
ground_offset = -min_point.getZ()
model.setZ(ground_offset)
print(f"缩放后重新调整位置: Z偏移 = {ground_offset}")
print(f"单位转换完成,缩放因子: {scale_factor}")
except Exception as e:
print(f"应用单位转换失败: {str(e)}")
def _normalizeModelScales(self, model):
"""智能标准化模型缩放层级
检测并修复FBX模型中子节点的大缩放值问题
"""
try:
print("开始分析模型缩放结构...")
# 收集所有节点的缩放信息
scale_info = []
self._collectScaleInfo(model, scale_info)
if not scale_info:
print("没有找到需要处理的缩放信息")
return
# 分析缩放模式
large_scales = [info for info in scale_info if max(abs(info['scale'].x), abs(info['scale'].y), abs(info['scale'].z)) > 10]
if not large_scales:
print("没有发现大缩放值,无需标准化")
return
print(f"发现 {len(large_scales)} 个节点有大缩放值")
# 计算标准化因子(基于最常见的大缩放值)
common_large_scale = self._findCommonLargeScale(large_scales)
if common_large_scale:
normalize_factor = 1.0 / common_large_scale
print(f"检测到常见大缩放值: {common_large_scale}, 标准化因子: {normalize_factor}")
# 应用标准化
self._applyScaleNormalization(model, normalize_factor)
print("✓ 缩放标准化完成")
else:
print("无法确定合适的标准化因子,跳过标准化")
except Exception as e:
print(f"缩放标准化失败: {str(e)}")
def _collectScaleInfo(self, node, scale_info, depth=0):
"""递归收集节点缩放信息"""
try:
scale = node.getScale()
scale_info.append({
'node': node,
'name': node.getName(),
'scale': scale,
'depth': depth
})
# 递归处理子节点
for i in range(node.getNumChildren()):
child = node.getChild(i)
self._collectScaleInfo(child, scale_info, depth + 1)
except Exception as e:
print(f"收集缩放信息失败 ({node.getName()}): {str(e)}")
def _findCommonLargeScale(self, large_scales):
"""找到最常见的大缩放值"""
try:
# 提取缩放值(取绝对值的最大分量)
scale_values = []
for info in large_scales:
scale = info['scale']
max_scale = max(abs(scale.x), abs(scale.y), abs(scale.z))
scale_values.append(round(max_scale)) # 四舍五入到整数
if not scale_values:
return None
# 找到最常见的值
from collections import Counter
counter = Counter(scale_values)
most_common = counter.most_common(1)[0]
print(f"缩放值统计: {dict(counter)}")
print(f"最常见的大缩放值: {most_common[0]} (出现{most_common[1]}次)")
# 只有当最常见的值确实很大时才返回
if most_common[0] >= 10:
return float(most_common[0])
return None
except Exception as e:
print(f"分析常见缩放值失败: {str(e)}")
return None
def _applyScaleNormalization(self, node, normalize_factor, depth=0):
"""递归应用缩放标准化,同时调整位置以保持视觉一致性"""
try:
indent = " " * depth
current_scale = node.getScale()
current_pos = node.getPos()
# 检查是否需要标准化(只处理明显的大缩放)
max_scale_component = max(abs(current_scale.x), abs(current_scale.y), abs(current_scale.z))
if max_scale_component > 10: # 只标准化明显的大缩放
# 应用新的缩放
new_scale = current_scale * normalize_factor
node.setScale(new_scale)
# 同时调整位置:当缩放变小时,位置也应该相应变小以保持视觉相对位置
# 这确保了子节点之间的相对距离在视觉上保持一致
new_pos = current_pos * normalize_factor
node.setPos(new_pos)
print(f"{indent}标准化 {node.getName()}:")
print(f"{indent} 缩放: {current_scale} -> {new_scale}")
print(f"{indent} 位置: {current_pos} -> {new_pos}")
# 递归处理子节点
for i in range(node.getNumChildren()):
child = node.getChild(i)
self._applyScaleNormalization(child, normalize_factor, depth + 1)
except Exception as e:
print(f"应用缩放标准化失败 ({node.getName()}): {str(e)}")
def importModelAsync(self, filepath):
"""异步导入模型"""
try:
# 创建异步加载请求
request = self.world.loader.makeAsyncRequest(filepath)
# 添加完成回调
def modelLoaded(task):
if task.isReady():
model = task.result()
if model:
# 处理加载完成的模型
self.processLoadedModel(model)
return task.done()
request.done_event = modelLoaded
# 开始异步加载
self.world.loader.loadAsync(request)
except Exception as e:
print(f"异步加载模型失败: {str(e)}")
def loadAnimatedModel(self, model_path, anims=None, auto_play=True):
"""加载带动画的模型
Args:
model_path: 模型文件路径
anims: 动画字典,格式为 {"动画名": "动画文件路径"}
auto_play: 是否自动播放第一个动画
"""
try:
print(f"🎬 加载动画模型: {model_path}")
# 如果没有指定动画,尝试自动检测
if anims is None:
anims = self._detectAnimations(model_path)
# 创建Actor对象
actor = Actor(model_path, anims)
if actor:
actor.reparentTo(self.world.render)
# 设置碰撞检测
self.setupCollision(actor)
# 获取可用的动画列表
available_anims = actor.getAnimNames()
print(f"📋 检测到动画: {available_anims}")
# 自动播放第一个动画
if auto_play and available_anims:
first_anim = available_anims[0]
actor.loop(first_anim)
print(f"▶️ 自动播放动画: {first_anim}")
# 添加动画控制标签
actor.setTag("animated", "true")
actor.setTag("current_anim", first_anim)
actor.setTag("available_anims", str(available_anims))
# 调整模型位置(让它站在地面上)
self._adjustModelPosition(actor)
self.models.append(actor)
# 更新场景树
self.updateSceneTree()
print(f"✅ 动画模型加载完成: {actor.getName()}")
return actor
except Exception as e:
print(f"❌ 加载动画模型失败: {str(e)}")
import traceback
traceback.print_exc()
return None
# ==================== 材质和几何体处理 ====================
def processMaterials(self, model):
"""处理模型材质"""
if isinstance(model.node(), GeomNode):
# 创建基础材质
material = Material()
material.setAmbient((0.2, 0.2, 0.2, 1.0))
material.setDiffuse((0.8, 0.8, 0.8, 1.0))
material.setSpecular((0.5, 0.5, 0.5, 1.0))
material.setShininess(32.0)
# 检查FBX材质
state = model.node().getGeomState(0)
if state.hasAttrib(MaterialAttrib.getClassType()):
fbx_material = state.getAttrib(MaterialAttrib.getClassType()).getMaterial()
if fbx_material:
# 复制FBX材质属性
material.setAmbient(fbx_material.getAmbient())
material.setDiffuse(fbx_material.getDiffuse())
material.setSpecular(fbx_material.getSpecular())
material.setShininess(fbx_material.getShininess())
# 应用材质
model.setMaterial(material)
def processModelGeometry(self, model):
"""处理模型几何体"""
# 创建EggData对象
egg_data = EggData()
# 处理顶点数据
vertex_pool = EggVertexPool("vpool")
egg_data.addChild(vertex_pool)
# 处理几何体
if isinstance(model.node(), GeomNode):
for i in range(model.node().getNumGeoms()):
geom = model.node().getGeom(i)
# 处理几何体数据
# ...
# ==================== 碰撞系统 ====================
def setupCollision(self, model):
"""为模型设置碰撞检测(增强版本)"""
try:
# 创建碰撞节点
cNode = CollisionNode(f'modelCollision_{model.getName()}')
# 设置碰撞掩码
cNode.setIntoCollideMask(BitMask32.bit(2)) # 用于鼠标选择
# 如果启用了模型间碰撞检测,添加额外的掩码
if (hasattr(self.world, 'collision_manager') and
self.world.collision_manager.model_collision_enabled):
# 同时设置模型间碰撞掩码
current_mask = cNode.getIntoCollideMask()
model_collision_mask = BitMask32.bit(6) # MODEL_COLLISION
cNode.setIntoCollideMask(current_mask | model_collision_mask)
print(f"{model.getName()} 启用模型间碰撞检测")
# 获取模型的边界
bounds = model.getBounds()
if bounds.isEmpty():
print(f"⚠️ 模型 {model.getName()} 边界为空,使用默认碰撞体")
# 使用默认的小球体
cSphere = CollisionSphere(Point3(0, 0, 0), 1.0)
else:
center = bounds.getCenter()
radius = bounds.getRadius()
# 确保半径不为零
if radius <= 0:
radius = 1.0
print(f"⚠️ 模型 {model.getName()} 半径为零,使用默认半径 1.0")
#
# # 添加碰撞球体
# cSphere = CollisionSphere(center, radius)
cSphere = self.world.collision_manager.createCollisionShape(model, 'polygon')
cNode.addSolid(cSphere)
# 将碰撞节点附加到模型上
cNodePath = model.attachNewNode(cNode)
# 根据调试设置决定是否显示碰撞体
if hasattr(self.world, 'debug_collision') and self.world.debug_collision:
cNodePath.show()
else:
cNodePath.hide()
# 为模型添加碰撞相关标签
model.setTag("has_collision", "true")
model.setTag("collision_radius", str(radius if 'radius' in locals() else 1.0))
print(f"✅ 为模型 {model.getName()} 设置碰撞检测完成")
return cNodePath
except Exception as e:
print(f"❌ 为模型 {model.getName()} 设置碰撞检测失败: {str(e)}")
import traceback
traceback.print_exc()
return None
# ==================== 场景树管理 ====================
def updateSceneTree(self):
"""更新场景树显示 - 代理到interface_manager"""
if hasattr(self.world, 'interface_manager'):
return self.world.interface_manager.updateSceneTree()
else:
print("界面管理器未初始化,无法更新场景树")
# ==================== 场景保存和加载 ====================
def saveScene(self, filename):
"""保存场景到BAM文件"""
try:
print(f"\n=== 开始保存场景到: {filename} ===")
# 遍历所有模型,保存材质状态和变换信息
for model in self.models:
# 保存变换信息(关键!)
model.setTag("transform_pos", str(model.getPos()))
model.setTag("transform_hpr", str(model.getHpr()))
model.setTag("transform_scale", str(model.getScale()))
print(f"保存模型 {model.getName()} 的变换信息:")
print(f" 位置: {model.getPos()}")
print(f" 旋转: {model.getHpr()}")
print(f" 缩放: {model.getScale()}")
# 获取当前状态
state = model.getState()
# 如果有材质属性,保存为标签
if state.hasAttrib(MaterialAttrib.getClassType()):
mat_attrib = state.getAttrib(MaterialAttrib.getClassType())
material = mat_attrib.getMaterial()
if material:
# 保存材质属性到标签
model.setTag("material_ambient", str(material.getAmbient()))
model.setTag("material_diffuse", str(material.getDiffuse()))
model.setTag("material_specular", str(material.getSpecular()))
model.setTag("material_emission", str(material.getEmission()))
model.setTag("material_shininess", str(material.getShininess()))
if material.hasBaseColor():
model.setTag("material_basecolor", str(material.getBaseColor()))
# 如果有颜色属性,保存为标签
if state.hasAttrib(ColorAttrib.getClassType()):
color_attrib = state.getAttrib(ColorAttrib.getClassType())
if not color_attrib.isOff():
model.setTag("color", str(color_attrib.getColor()))
# 保存场景
success = self.world.render.writeBamFile(filename)
return success
except Exception as e:
print(f"保存场景时发生错误: {str(e)}")
return False
def loadScene(self, filename):
"""从BAM文件加载场景"""
try:
print(f"\n=== 开始加载场景: {filename} ===")
# 清除当前场景
print("\n清除当前场景...")
for model in self.models:
model.removeNode()
self.models.clear()
# 加载场景
scene = self.world.loader.loadModel(filename)
if not scene:
return False
# 遍历场景中的所有模型节点
def processNode(nodePath, depth=0):
indent = " " * depth
print(f"{indent}处理节点: {nodePath.getName()}")
# 跳过render节点的递归
if nodePath.getName() == "render" and depth > 0:
print(f"{indent}跳过重复的render节点")
return
# 跳过光源节点
if nodePath.getName() in ["alight", "dlight"]:
print(f"{indent}跳过光源节点: {nodePath.getName()}")
return
# 跳过相机节点
if nodePath.getName() in ["camera", "cam"]:
print(f"{indent}跳过相机节点: {nodePath.getName()}")
return
if isinstance(nodePath.node(), ModelRoot):
print(f"{indent}找到模型根节点!")
# 清除现有材质状态
nodePath.clearMaterial()
nodePath.clearColor()
# 创建新材质
material = Material()
# 从标签恢复材质属性
def parseColor(color_str):
"""解析颜色字符串为Vec4"""
try:
# 移除LVecBase4f标记只保留数值
color_str = color_str.replace('LVecBase4f', '').strip('()')
r, g, b, a = map(float, color_str.split(','))
return Vec4(r, g, b, a)
except:
return Vec4(1, 1, 1, 1) # 默认白色
if nodePath.hasTag("material_ambient"):
material.setAmbient(parseColor(nodePath.getTag("material_ambient")))
if nodePath.hasTag("material_diffuse"):
material.setDiffuse(parseColor(nodePath.getTag("material_diffuse")))
if nodePath.hasTag("material_specular"):
material.setSpecular(parseColor(nodePath.getTag("material_specular")))
if nodePath.hasTag("material_emission"):
material.setEmission(parseColor(nodePath.getTag("material_emission")))
if nodePath.hasTag("material_shininess"):
material.setShininess(float(nodePath.getTag("material_shininess")))
if nodePath.hasTag("material_basecolor"):
material.setBaseColor(parseColor(nodePath.getTag("material_basecolor")))
# 应用材质
nodePath.setMaterial(material)
# 恢复颜色属性
if nodePath.hasTag("color"):
nodePath.setColor(parseColor(nodePath.getTag("color")))
# 恢复变换信息(关键!)
def parseVec3(vec_str):
"""解析向量字符串为Vec3"""
try:
# 移除LVecBase3f标记只保留数值
vec_str = vec_str.replace('LVecBase3f', '').replace('LPoint3f', '').strip('()')
x, y, z = map(float, vec_str.split(','))
return Vec3(x, y, z)
except Exception as e:
print(f"解析向量失败: {vec_str}, 错误: {e}")
return Vec3(0, 0, 0) # 默认值
if nodePath.hasTag("transform_pos"):
pos = parseVec3(nodePath.getTag("transform_pos"))
nodePath.setPos(pos)
print(f"{indent}恢复位置: {pos}")
if nodePath.hasTag("transform_hpr"):
hpr = parseVec3(nodePath.getTag("transform_hpr"))
nodePath.setHpr(hpr)
print(f"{indent}恢复旋转: {hpr}")
if nodePath.hasTag("transform_scale"):
scale = parseVec3(nodePath.getTag("transform_scale"))
nodePath.setScale(scale)
print(f"{indent}恢复缩放: {scale}")
# 将模型重新挂载到render下
nodePath.wrtReparentTo(self.world.render)
# 为加载的模型设置碰撞检测
self.setupCollision(nodePath)
self.models.append(nodePath)
# 递归处理子节点
for child in nodePath.getChildren():
processNode(child, depth + 1)
print("\n开始处理场景节点...")
processNode(scene)
# 移除临时场景节点
scene.removeNode()
# 更新场景树
self.updateSceneTree()
print("=== 场景加载完成 ===\n")
return True
except Exception as e:
print(f"加载场景时发生错误: {str(e)}")
return False
# ==================== 模型管理 ====================
def deleteModel(self, model):
"""删除模型"""
try:
if model in self.models:
model.removeNode()
self.models.remove(model)
self.updateSceneTree()
print(f"删除模型: {model.getName()}")
return True
except Exception as e:
print(f"删除模型失败: {str(e)}")
return False
def clearAllModels(self):
"""清除所有模型"""
try:
for model in self.models:
model.removeNode()
self.models.clear()
self.updateSceneTree()
print("清除所有模型完成")
except Exception as e:
print(f"清除所有模型失败: {str(e)}")
def getModels(self):
"""获取模型列表"""
return self.models.copy()
def getModelCount(self):
"""获取模型数量"""
return len(self.models)
def findModelByName(self, name):
"""根据名称查找模型"""
for model in self.models:
if model.getName() == name:
return model
return None
# ==================== 帮助方法 ====================
def processLoadedModel(self, model):
"""处理加载完成的模型(用于异步加载回调)"""
if model:
# 添加到模型列表
self.models.append(model)
# 设置基础变换
model.setPos(0, 0, 0)
model.setHpr(0, 0, 0)
model.setScale(1, 1, 1)
# 应用材质
self.processMaterials(model)
# 设置碰撞检测
self.setupCollision(model)
# 更新场景树
self.updateSceneTree()
print(f"异步加载模型完成: {model.getName()}")
def createSpotLight(self, pos=(0, 0, 0)):
"""创建聚光灯 - 支持多选创建,优化版本"""
try:
from RenderPipelineFile.rpcore import SpotLight
from QPanda3D.Panda3DWorld import get_render_pipeline
from panda3d.core import Vec3, NodePath
from PyQt5.QtCore import Qt
print(f"🔆 开始创建聚光灯,位置: {pos}")
# 获取树形控件
tree_widget = self._get_tree_widget()
if not tree_widget:
print("❌ 无法访问树形控件")
return None
# 获取目标父节点列表
target_parents = tree_widget.get_target_parents_for_creation()
if not target_parents:
print("❌ 没有找到有效的父节点")
return None
created_lights = []
render_pipeline = get_render_pipeline()
# 为每个有效的父节点创建聚光灯
for parent_item, parent_node in target_parents:
try:
# 生成唯一名称
light_name = f"Spotlight_{len(self.Spotlight)}"
# 创建挂载节点 - 挂载到选中的父节点
light_np = NodePath(light_name)
light_np.reparentTo(parent_node) # 挂载到父节点而不是render
light_np.setPos(*pos)
# 创建聚光灯对象
light = SpotLight()
light.direction = Vec3(0, 0, -1)
light.fov = 70
light.set_color_from_temperature(5 * 1000.0)
light.energy = 5000
light.radius = 1000
light.casts_shadows = True
light.shadow_map_resolution = 256
# 设置光源的世界坐标位置
world_pos = light_np.getPos(self.world.render)
light.setPos(world_pos)
# 添加到渲染管线
render_pipeline.add_light(light)
# 设置节点属性和标签
light_np.setTag("light_type", "spot_light")
light_np.setTag("is_scene_element", "1")
light_np.setTag("light_energy", str(light.energy))
light_np.setTag("created_by_user", "1")
# 保存光源对象引用
light_np.setPythonTag("rp_light_object", light)
# 添加到管理列表
self.Spotlight.append(light_np)
print(f"✅ 为 {parent_item.text(0)} 创建聚光灯成功: {light_name}")
# 在Qt树形控件中添加对应节点
qt_item = tree_widget.add_node_to_tree_widget(light_np, parent_item, "LIGHT_NODE")
if qt_item:
created_lights.append((light_np, qt_item))
else:
created_lights.append((light_np, None))
print("⚠️ Qt树节点添加失败但Panda3D对象已创建")
except Exception as e:
print(f"❌ 为 {parent_item.text(0)} 创建聚光灯失败: {str(e)}")
continue
# 处理创建结果
if not created_lights:
print("❌ 没有成功创建任何聚光灯")
return None
# 选中最后创建的光源
if created_lights:
last_light_np, last_qt_item = created_lights[-1]
if last_qt_item:
tree_widget.setCurrentItem(last_qt_item)
# 更新选择和属性面板
tree_widget.update_selection_and_properties(last_light_np, last_qt_item)
print(f"🎉 总共创建了 {len(created_lights)} 个聚光灯")
# 返回值处理
if len(created_lights) == 1:
return created_lights[0][0] # 单个光源返回NodePath
else:
return [light_np for light_np, _ in created_lights] # 多个光源返回列表
except Exception as e:
print(f"❌ 创建聚光灯过程失败: {str(e)}")
import traceback
traceback.print_exc()
return None
def createPointLight(self, pos=(0, 0, 0)):
"""创建点光源 - 支持多选创建,优化版本"""
try:
from RenderPipelineFile.rpcore import PointLight
from QPanda3D.Panda3DWorld import get_render_pipeline
from panda3d.core import Vec3, NodePath
from PyQt5.QtCore import Qt
print(f"💡 开始创建点光源,位置: {pos}")
# 获取树形控件
tree_widget = self._get_tree_widget()
if not tree_widget:
print("❌ 无法访问树形控件")
return None
# 获取目标父节点列表
target_parents = tree_widget.get_target_parents_for_creation()
if not target_parents:
print("❌ 没有找到有效的父节点")
return None
created_lights = []
render_pipeline = get_render_pipeline()
# 为每个有效的父节点创建点光源
for parent_item, parent_node in target_parents:
try:
# 生成唯一名称
light_name = f"Pointlight_{len(self.Pointlight)}"
# 创建挂载节点 - 挂载到选中的父节点
light_np = NodePath(light_name)
light_np.reparentTo(parent_node) # 挂载到父节点而不是render
light_np.setPos(*pos)
# 创建点光源对象
light = PointLight()
# 设置光源的世界坐标位置
world_pos = light_np.getPos(self.world.render)
light.setPos(world_pos)
light.energy = 5000
light.radius = 1000
light.inner_radius = 0.4
light.set_color_from_temperature(5 * 1000.0)
light.casts_shadows = True
light.shadow_map_resolution = 256
# 添加到渲染管线
render_pipeline.add_light(light)
# 设置节点属性和标签
light_np.setTag("light_type", "point_light")
light_np.setTag("is_scene_element", "1")
light_np.setTag("light_energy", str(light.energy))
light_np.setTag("created_by_user", "1")
# 保存光源对象引用
light_np.setPythonTag("rp_light_object", light)
# 添加到管理列表
self.Pointlight.append(light_np)
print(f"✅ 为 {parent_item.text(0)} 创建点光源成功: {light_name}")
# 在Qt树形控件中添加对应节点
qt_item =tree_widget.add_node_to_tree_widget(light_np, parent_item, "LIGHT_NODE")
if qt_item:
created_lights.append((light_np, qt_item))
else:
created_lights.append((light_np, None))
print("⚠️ Qt树节点添加失败但Panda3D对象已创建")
except Exception as e:
print(f"❌ 为 {parent_item.text(0)} 创建点光源失败: {str(e)}")
continue
# 处理创建结果
if not created_lights:
print("❌ 没有成功创建任何点光源")
return None
# 选中最后创建的光源
if created_lights:
last_light_np, last_qt_item = created_lights[-1]
if last_qt_item:
tree_widget.setCurrentItem(last_qt_item)
# 更新选择和属性面板
tree_widget.update_selection_and_properties(last_light_np, last_qt_item)
print(f"🎉 总共创建了 {len(created_lights)} 个点光源")
# 返回值处理
if len(created_lights) == 1:
return created_lights[0][0] # 单个光源返回NodePath
else:
return [light_np for light_np, _ in created_lights] # 多个光源返回列表
except Exception as e:
print(f"❌ 创建点光源过程失败: {str(e)}")
import traceback
traceback.print_exc()
return None
def _get_tree_widget(self):
"""安全获取树形控件"""
try:
if (hasattr(self.world, 'interface_manager') and
hasattr(self.world.interface_manager, 'treeWidget')):
return self.world.interface_manager.treeWidget
except AttributeError:
pass
return None
def _importModelSingle(self, filepath, apply_unit_conversion=False, normalize_scales=True, auto_convert_to_glb=True):
"""传统单一模型导入方法(兼容性保留)"""
try:
print(f"\n=== 使用传统模式导入模型: {filepath} ===")
filepath = util.normalize_model_path(filepath)
original_filepath = filepath
# 检查是否需要转换为GLB
if auto_convert_to_glb and self._shouldConvertToGLB(filepath):
print(f"🔄 检测到需要转换的格式尝试转换为GLB...")
converted_path = self._convertToGLBWithProgress(filepath)
if converted_path:
print(f"✅ 转换成功: {converted_path}")
filepath = converted_path
try:
from PyQt5.QtWidgets import QMessageBox
original_ext = os.path.splitext(original_filepath)[1].upper()
QMessageBox.information(None, "转换成功",
f"已将 {original_ext} 格式自动转换为 GLB 格式\n以获得更好的动画支持!")
except:
pass
else:
print(f"⚠️ 转换失败,使用原始文件")
# 加载模型
print("直接从文件加载模型...")
model = self.world.loader.loadModel(filepath)
if not model:
print("加载模型失败")
return None
# 设置模型名称
model_name = os.path.basename(filepath)
model.setName(model_name)
# 将模型添加到场景
model.reparentTo(self.world.render)
# 设置标签和路径信息
model.setTag("model_path", filepath)
model.setTag("original_path", original_filepath)
model.setTag("file", model_name)
model.setTag("is_model_root", "1")
model.setTag("is_scene_element", "1")
model.setTag("created_by_user", "1")
if filepath != original_filepath:
model.setTag("converted_from", os.path.splitext(original_filepath)[1])
model.setTag("converted_to_glb", "true")
# 应用处理选项
if apply_unit_conversion and filepath.lower().endswith('.fbx'):
print("应用FBX单位转换厘米到米...")
self._applyUnitConversion(model, 0.01)
model.setTag("unit_conversion_applied", "true")
if normalize_scales and filepath.lower().endswith('.fbx'):
print("标准化FBX模型缩放层级...")
self._normalizeModelScales(model)
model.setTag("scale_normalization_applied", "true")
# 调整模型位置到地面
self._adjustModelToGround(model)
# 创建并设置基础材质
print("\n=== 开始设置材质 ===")
self._applyMaterialsToModel(model)
# 设置碰撞检测(重要!用于选择功能)
print("\n=== 设置碰撞检测 ===")
self.setupCollision(model)
# 添加到模型列表
self.models.append(model)
# 更新场景树
self.updateSceneTree()
print(f"=== 模型导入成功: {model_name} ===\n")
return model
except Exception as e:
print(f"导入模型失败: {str(e)}")
return None
# def createSpotLight(self, pos=(0, 0, 0)):
# """创建聚光灯 - 使用统一的create_item方法"""
# try:
# # 调用CustomTreeWidget的create_item方法创建聚光灯节点
# if hasattr(self.world, 'interface_manager') and hasattr(self.world.interface_manager, 'treeWidget'):
# tree_widget = self.world.interface_manager.treeWidget
# if tree_widget and hasattr(tree_widget, 'create_item'):
# # 创建聚光灯节点
# created_nodes = tree_widget.create_item("spot_light")
#
# if created_nodes:
# # 获取创建的节点
# light_np, qt_item = created_nodes[0]
#
# # 设置位置(如果指定了非默认位置)
# if pos != (0, 0, 0):
# light_np.setPos(*pos)
# # 同时更新光源对象的位置
# light_obj = light_np.getPythonTag("rp_light_object")
# if light_obj:
# light_obj.setPos(*pos)
#
# print(f"✅ 通过create_item创建聚光灯成功: {light_np.getName()}")
# return light_np
# else:
# print("❌ create_item创建聚光灯失败")
# return None
# else:
# print("❌ 无法访问树形控件的create_item方法")
# return None
# else:
# print("❌ 无法访问界面管理器或树形控件")
# return None
#
# except Exception as e:
# print(f"❌ 创建聚光灯时发生错误: {str(e)}")
# import traceback
# traceback.print_exc()
# return None
# def createSpotLight(self, pos=(0, 0, 0)):
# from RenderPipelineFile.rpcore import SpotLight, RenderPipeline
# from panda3d.core import Vec3,NodePath
#
# render_pipeline = get_render_pipeline()
#
# # 创建一个挂载节点(你控制的)
# light_np = NodePath("SpotlightAttachNode")
# light_np.reparentTo(self.world.render)
# #light_np.setPos(*pos)
#
# self.half_energy = 5000
# self.lamp_fov = 70
# self.lamp_radius = 1000
#
# light = SpotLight()
# light.direction = Vec3(0, 0, -1) # 光照方向
# light.fov = self.lamp_fov # 光源角度(类似手电筒)
# light.set_color_from_temperature(5 * 1000.0) # 色温K
# light.energy = self.half_energy # 光照强度
# light.radius = self.lamp_radius # 影响范围
# light.casts_shadows = True # 是否投射阴影
# light.shadow_map_resolution = 256 # 阴影分辨率
# light.setPos(*pos)
# #light_np.setPos(*pos)
#
# #light_np = render_pipeline.add_light(light, parent=self.world.render)
# render_pipeline.add_light(light) # 添加到渲染管线
#
# light_name = f"Spotlight_{len(self.Spotlight)}"
#
# light_np.setName(light_name) # 设置唯一名称
# #light_np.reparentTo(self.world.render) # 挂载到场景根节点
#
# light_np.setTag("light_type", "spot_light")
# light_np.setTag("is_scene_element", "1")
# light_np.setTag("light_energy", str(light.energy))
#
# light_np.setPythonTag("rp_light_object", light)
#
# self.Spotlight.append(light_np)
#
# if hasattr(self.world, 'updateSceneTree'):
# self.world.updateSceneTree()
#
# #print("nikan"+light_np.getHpr())
# def createPointLight(self, pos=(0, 0, 0)):
# """创建点光源 - 使用统一的create_item方法"""
# try:
# # 调用CustomTreeWidget的create_item方法创建点光源节点
# if hasattr(self.world, 'interface_manager') and hasattr(self.world.interface_manager, 'treeWidget'):
# tree_widget = self.world.interface_manager.treeWidget
# if tree_widget and hasattr(tree_widget, 'create_item'):
# # 创建点光源节点
# created_nodes = tree_widget.create_item("point_light")
#
# if created_nodes:
# # 获取创建的节点
# light_np, qt_item = created_nodes[0]
#
# # 设置位置(如果指定了非默认位置)
# if pos != (0, 0, 0):
# light_np.setPos(*pos)
# # 同时更新光源对象的位置
# light_obj = light_np.getPythonTag("rp_light_object")
# if light_obj:
# light_obj.setPos(*pos)
#
# print(f"✅ 通过create_item创建点光源成功: {light_np.getName()}")
# return light_np
# else:
# print("❌ create_item创建点光源失败")
# return None
# else:
# print("❌ 无法访问树形控件的create_item方法")
# return None
# else:
# print("❌ 无法访问界面管理器或树形控件")
# return None
#
# except Exception as e:
# print(f"❌ 创建点光源时发生错误: {str(e)}")
# import traceback
# traceback.print_exc()
# return None
# def createPointLight(self, pos=(0, 0, 0)):
# from RenderPipelineFile.rpcore import PointLight, RenderPipeline
# from panda3d.core import Vec3, NodePath
#
# render_pipeline = get_render_pipeline()
#
# # 创建一个挂载节点(你控制的)
# light_np = NodePath("PointlightAttachNode")
# light_np.reparentTo(self.world.render)
#
#
# light = PointLight()
# light.setPos(*pos)
# light_np.setPos(*pos)
# light.energy = 5000
# light.radius = 1000
# light.inner_radius = 0.4
# light.set_color_from_temperature(5 * 1000.0) # 色温K
# light.casts_shadows = True # 是否投射阴影
# light.shadow_map_resolution = 256 # 阴影分辨率
#
# render_pipeline.add_light(light) # 添加到渲染管线
#
# light_name = f"Pointlight{len(self.Pointlight)}"
#
# light_np.setName(light_name) # 设置唯一名称
#
# #light_np = NodePath(f"PointLight_{len(self.Pointlight)}")
# #light_np.reparentTo(self.world.render)
# #light_np.setPos(*pos)
#
# light_np.setTag("light_type", "point_light")
# light_np.setTag("is_scene_element", "1")
# light_np.setTag("light_energy", str(light.energy))
#
# # 保存光源对象引用(重要!用于属性面板)
# light_np.setPythonTag("rp_light_object", light)
#
# self.Pointlight.append(light_np)
#
# if hasattr(self.world, 'updateSceneTree'):
# self.world.updateSceneTree()
#
# return light,light_np
# ==================== GLB 转换方法 ====================
#===================================================
def _shouldConvertToGLB(self, filepath):
"""判断是否应该转换为GLB格式"""
ext = os.path.splitext(filepath)[1].lower()
# 需要转换的格式FBX, OBJ, DAE等但不转换已经是GLB/GLTF的
convert_formats = ['.fbx', '.obj', '.dae', '.3ds', '.blend']
return ext in convert_formats
def _convertToGLBWithProgress(self, filepath):
"""带进度显示的GLB转换"""
try:
from PyQt5.QtWidgets import QProgressDialog, QApplication
from PyQt5.QtCore import Qt
# 创建进度对话框
progress = QProgressDialog("正在转换模型格式以获得更好的动画支持...", "取消", 0, 100)
progress.setWindowTitle("模型格式转换")
progress.setWindowModality(Qt.WindowModal)
progress.show()
QApplication.processEvents()
try:
result = self._convertToGLB(filepath, progress)
progress.hide()
return result
except Exception as e:
progress.hide()
print(f"转换过程出错: {e}")
return None
except ImportError:
# 如果没有 PyQt5直接转换
return self._convertToGLB(filepath)
def _convertToGLB(self, filepath, progress=None):
"""将模型文件转换为GLB格式"""
try:
print(f"[GLB转换] 开始转换: {filepath}")
if progress:
progress.setValue(10)
progress.setLabelText("准备转换...")
from PyQt5.QtWidgets import QApplication
QApplication.processEvents()
# 准备输出路径
base_name = os.path.splitext(os.path.basename(filepath))[0]
output_dir = os.path.dirname(filepath)
glb_path = os.path.join(output_dir, f"{base_name}_auto_converted.glb")
# 如果已经存在转换后的文件,直接使用
if os.path.exists(glb_path):
# 检查文件时间,如果原文件更新则重新转换
original_time = os.path.getmtime(filepath)
converted_time = os.path.getmtime(glb_path)
if converted_time > original_time:
print(f"[GLB转换] 使用现有转换文件: {glb_path}")
if progress:
progress.setValue(100)
return glb_path
if progress:
progress.setValue(20)
progress.setLabelText("尝试 Blender 转换...")
QApplication.processEvents()
# 方法1: 使用 Blender 进行转换
if self._convertWithBlender(filepath, glb_path, progress):
return glb_path
if progress:
progress.setValue(60)
progress.setLabelText("尝试 FBX2glTF 转换...")
QApplication.processEvents()
# 方法2: 使用 FBX2glTF (如果可用)
if self._convertWithFBX2glTF(filepath, glb_path, progress):
return glb_path
if progress:
progress.setValue(80)
progress.setLabelText("尝试 Assimp 转换...")
QApplication.processEvents()
# 方法3: 使用 Assimp
if self._convertWithAssimp(filepath, glb_path, progress):
return glb_path
#print(f"[GLB转换] 所有转换方法都失败,既然没有可以转换格式的工具和环境那么就用原始文件,不一定非要转换")
return None
except Exception as e:
print(f"[GLB转换] 转换过程出错: {e}")
return None
def _convertWithBlender(self, input_path, output_path, progress=None):
"""使用 Blender 进行转换"""
try:
import subprocess
import tempfile
print(f"[Blender转换] {input_path}{output_path}")
# 创建 Blender 脚本
script_content = f'''
import bpy
import sys
import os
# 清理默认场景
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete(use_global=False)
print("开始导入文件...")
# 根据文件类型选择导入方法
input_file = "{input_path}"
output_file = "{output_path}"
try:
ext = os.path.splitext(input_file)[1].lower()
if ext == '.fbx':
bpy.ops.import_scene.fbx(filepath=input_file)
elif ext == '.obj':
bpy.ops.import_scene.obj(filepath=input_file)
elif ext == '.dae':
bpy.ops.wm.collada_import(filepath=input_file)
elif ext == '.blend':
bpy.ops.wm.open_mainfile(filepath=input_file)
else:
print(f"不支持的格式: {{ext}}")
sys.exit(1)
print("导入成功开始导出GLB...")
# 导出为 GLB保留动画
bpy.ops.export_scene.gltf(
filepath=output_file,
export_format='GLB',
export_animations=True,
export_force_sampling=True,
export_frame_range=True,
export_current_frame=False,
export_skins=True,
export_morph=True,
export_lights=True,
export_cameras=False
)
print("GLB导出成功")
except Exception as e:
print(f"转换失败: {{e}}")
sys.exit(1)
'''
# 写入临时脚本文件
with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as temp_file:
temp_file.write(script_content)
script_path = temp_file.name
try:
# 执行 Blender 转换
result = subprocess.run([
'blender', '--background', '--python', script_path
], capture_output=True, text=True, timeout=180)
# 清理临时文件
os.unlink(script_path)
if result.returncode == 0 and os.path.exists(output_path):
print(f"[Blender转换] 转换成功")
return True
else:
print(f"[Blender转换] 转换失败: {result.stderr}")
return False
except subprocess.TimeoutExpired:
print(f"[Blender转换] 转换超时")
return False
except FileNotFoundError:
print(f"[Blender转换] Blender 未安装")
return False
except Exception as e:
print(f"[Blender转换] 转换过程出错: {e}")
return False
def _convertWithFBX2glTF(self, input_path, output_path, progress=None):
"""使用 FBX2glTF 进行转换仅支持FBX"""
try:
import subprocess
if not input_path.lower().endswith('.fbx'):
return False
print(f"[FBX2glTF转换] {input_path}{output_path}")
# 使用 FBX2glTF 转换
result = subprocess.run([
'FBX2glTF', input_path, '--output', output_path, '--binary'
], capture_output=True, text=True, timeout=120)
if result.returncode == 0 and os.path.exists(output_path):
print(f"[FBX2glTF转换] 转换成功")
return True
else:
print(f"[FBX2glTF转换] 转换失败: {result.stderr}")
return False
except subprocess.TimeoutExpired:
print(f"[FBX2glTF转换] 转换超时")
return False
except FileNotFoundError:
print(f"[FBX2glTF转换] FBX2glTF 未安装")
return False
except Exception as e:
print(f"[FBX2glTF转换] 转换过程出错: {e}")
return False
def _convertWithAssimp(self, input_path, output_path, progress=None):
"""使用 PyAssimp 进行转换"""
try:
import pyassimp
print(f"[PyAssimp转换] {input_path}{output_path}")
# 加载模型
scene = pyassimp.load(input_path)
if not scene:
print(f"[PyAssimp转换] 加载模型失败")
return False
if progress:
progress.setValue(30)
# 导出为GLB格式
pyassimp.export(scene, output_path, "glb2")
if progress:
progress.setValue(80)
# 释放资源
pyassimp.release(scene)
if os.path.exists(output_path):
print(f"[PyAssimp转换] 转换成功")
return True
else:
print(f"[PyAssimp转换] 转换失败: 输出文件未生成")
return False
except ImportError:
print(f"[PyAssimp转换] PyAssimp 未安装")
return False
except Exception as e:
print(f"[PyAssimp转换] 转换过程出错: {e}")
return False
def load_cesium_tileset(self, tileset_url, position=(0, 0, 0)):
try:
print(f"加载 Cesium 3D Tiles: {tileset_url}")
# 创建一个容器节点来管理tileset
node_name = f"cesium_tileset_{len(self.tilesets)}"
tileset_node = self.world.render.attachNewNode(node_name)
tileset_node.setPos(*position)
#添加标签以便场景树识别
tileset_node.setTag("is_scene_element","1")
tileset_node.setTag("element_type","cesium_tileset")
tileset_node.setTag("tileset_url",tileset_url)
tileset_node.setTag("file",f"tileset_{len(self.tilesets)}")
# 存储tileset信息
tileset_info = {
'url': tileset_url,
'node': tileset_node,
'position': position,
'tiles': {}
}
self.tilesets.append(tileset_info)
# 创建一个临时的可视化占位符,让用户能看到节点已添加
self._create_placeholder_geometry(tileset_node)
# 异步加载tileset数据
self._load_tileset_async(tileset_url, tileset_info)
# 更新场景树
self.updateSceneTree()
print(f"✓ Cesium 3D Tiles 加载请求已发送")
return tileset_node
except Exception as e:
print(f"❌ 加载 Cesium 3D Tiles 失败: {e}")
import traceback
traceback.print_exc()
return None
def _load_tileset_async(self, tileset_url, tileset_info):
"""异步加载 tileset 数据"""
async def load_tileset():
try:
async with aiohttp.ClientSession() as session:
async with session.get(tileset_url) as response:
if response.status == 200:
tileset_data = await response.json()
self._parse_tileset(tileset_data, tileset_info)
print(f"✓ Tileset 数据加载完成")
else:
print(f"✗ Tileset 加载失败: {response.status}")
except Exception as e:
print(f"✗ Tileset 加载出错: {e}")
# 在 Panda3D 的任务系统中运行异步任务
task = asyncio.ensure_future(load_tileset())
self._current_asyncio_task = task # 保存任务引用
self.world.taskMgr.add(self._check_async_task, "check_tileset_load", appendTask=True)
def _check_async_task(self, panda3d_task):
# 检查 asyncio 任务是否完成
if hasattr(self, '_current_asyncio_task'):
if self._current_asyncio_task.done():
try:
self._current_asyncio_task.result()
except Exception as e:
print(f"异步任务出错:{e}")
# 返回 Panda3D 任务管理器的完成状态
return panda3d_task.done # 注意是 done 而不是 DONE
# 返回 Panda3D 任务管理器的继续状态
return panda3d_task.cont # 注意是 cont 而不是 CONTINUE
def _parse_tileset(self,tileset_data,tileset_info):
try:
root = tileset_data.get('root',{})
self._parse_tile(root,tileset_info['node'],tileset_info)
print("✓ Tileset 解析完成")
except Exception as e:
print(f"✗ Tileset 解析出错: {e}")
def _parse_tile(self, tile_data, parent_node, tileset_info):
try:
# 获取tileID
tile_id = f"tile_{len(tileset_info['tiles'])}"
print(f"创建tile节点: {tile_id}")
# 创建tile节点
tile_node = parent_node.attachNewNode(tile_id)
tileset_info['tiles'][tile_id] = {
'node': tile_node,
'data': tile_data,
'loaded': False
}
# 如果有内容,创建占位几何体
if 'content' in tile_data:
print(f"为tile {tile_id} 创建几何体")
self._create_tile_geometry(tile_node)
# 递归解析子tiles
children = tile_data.get('children', [])
print(f"Tile {tile_id}{len(children)} 个子节点")
for child_data in children:
self._parse_tile(child_data, tile_node, tileset_info)
except Exception as e:
print(f"✗ Tile 解析出错: {e}")
import traceback
traceback.print_exc()
def _create_tile_geometry(self,parent_node):
"""为 tile 创建占位几何体"""
try:
# 创建一个简单的立方体作为占位符
from panda3d.core import GeomVertexFormat, GeomVertexData, GeomVertexWriter
from panda3d.core import Geom, GeomTriangles, GeomNode
format = GeomVertexFormat.getV3n3c4()
vdata = GeomVertexData('tile_cube', format, Geom.UHStatic)
vertex = GeomVertexWriter(vdata, 'vertex')
normal = GeomVertexWriter(vdata, 'normal')
color = GeomVertexWriter(vdata, 'color')
# 定义立方体顶点
vertices = [
(-0.5, -0.5, -0.5), (0.5, -0.5, -0.5), (0.5, 0.5, -0.5), (-0.5, 0.5, -0.5),
(-0.5, -0.5, 0.5), (0.5, -0.5, 0.5), (0.5, 0.5, 0.5), (-0.5, 0.5, 0.5)
]
for vert in vertices:
vertex.addData3f(*vert)
normal.addData3f(0, 0, 1)
color.addData4f(0.2, 0.6, 0.8, 1.0)
# 创建几何体
geom = Geom(vdata)
# 创建面
prim = GeomTriangles(Geom.UHStatic)
# 底面
prim.addVertices(0, 1, 2)
prim.addVertices(0, 2, 3)
# 顶面
prim.addVertices(4, 7, 6)
prim.addVertices(4, 6, 5)
# 前面
prim.addVertices(0, 4, 5)
prim.addVertices(0, 5, 1)
# 后面
prim.addVertices(2, 6, 7)
prim.addVertices(2, 7, 3)
# 左面
prim.addVertices(0, 3, 7)
prim.addVertices(0, 7, 4)
# 右面
prim.addVertices(1, 5, 6)
prim.addVertices(1, 6, 2)
prim.closePrimitive()
geom.addPrimitive(prim)
# 创建几何节点
geom_node = GeomNode('tile_geometry')
geom_node.addGeom(geom)
# 添加到场景
cube_node = parent_node.attachNewNode(geom_node)
cube_node.setScale(1000) # 放大以便观察
# 添加材质
material = Material()
material.setBaseColor((0.2, 0.6, 0.8, 1.0))
material.setSpecular((0.1, 0.1, 0.1, 1.0))
material.setShininess(10.0)
cube_node.setMaterial(material)
except Exception as e:
print(f"✗ 创建 tile 几何体出错: {e}")
def _create_placeholder_geometry(self, parent_node):
"""创建一个简单的占位符几何体,让用户能看到节点"""
try:
from panda3d.core import GeomVertexFormat, GeomVertexData, GeomVertexWriter
from panda3d.core import Geom, GeomTriangles, GeomNode
# 创建简单的立方体作为占位符
format = GeomVertexFormat.getV3n3c4()
vdata = GeomVertexData('placeholder_cube', format, Geom.UHStatic)
vertex = GeomVertexWriter(vdata, 'vertex')
normal = GeomVertexWriter(vdata, 'normal')
color = GeomVertexWriter(vdata, 'color')
# 定义立方体顶点
size = 1.0
vertices = [
# 前面 (Z+)
(-size, -size, size), (size, -size, size), (size, size, size), (-size, size, size),
# 后面 (Z-)
(-size, -size, -size), (-size, size, -size), (size, size, -size), (size, -size, -size),
# 左面 (X-)
(-size, -size, -size), (-size, -size, size), (-size, size, size), (-size, size, -size),
# 右面 (X+)
(size, -size, -size), (size, size, -size), (size, size, size), (size, -size, size),
# 上面 (Y+)
(-size, size, -size), (-size, size, size), (size, size, size), (size, size, -size),
# 下面 (Y-)
(-size, -size, -size), (size, -size, -size), (size, -size, size), (-size, -size, size)
]
normals = [
# 前面法线
(0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1),
# 后面法线
(0, 0, -1), (0, 0, -1), (0, 0, -1), (0, 0, -1),
# 左面法线
(-1, 0, 0), (-1, 0, 0), (-1, 0, 0), (-1, 0, 0),
# 右面法线
(1, 0, 0), (1, 0, 0), (1, 0, 0), (1, 0, 0),
# 上面法线
(0, 1, 0), (0, 1, 0), (0, 1, 0), (0, 1, 0),
# 下面法线
(0, -1, 0), (0, -1, 0), (0, -1, 0), (0, -1, 0)
]
# 青色
face_colors = [
(0.0, 1.0, 1.0, 1.0), (0.0, 1.0, 1.0, 1.0), (0.0, 1.0, 1.0, 1.0), (0.0, 1.0, 1.0, 1.0), # 前面 - 青色
(0.0, 0.8, 0.8, 1.0), (0.0, 0.8, 0.8, 1.0), (0.0, 0.8, 0.8, 1.0), (0.0, 0.8, 0.8, 1.0), # 后面 - 稍暗青色
(0.0, 0.9, 0.9, 1.0), (0.0, 0.9, 0.9, 1.0), (0.0, 0.9, 0.9, 1.0), (0.0, 0.9, 0.9, 1.0), # 左面 - 中等青色
(0.0, 0.7, 0.7, 1.0), (0.0, 0.7, 0.7, 1.0), (0.0, 0.7, 0.7, 1.0), (0.0, 0.7, 0.7, 1.0), # 右面 - 稍暗青色
(0.0, 1.0, 1.0, 1.0), (0.0, 1.0, 1.0, 1.0), (0.0, 1.0, 1.0, 1.0), (0.0, 1.0, 1.0, 1.0), # 上面 - 青色
(0.0, 0.6, 0.6, 1.0), (0.0, 0.6, 0.6, 1.0), (0.0, 0.6, 0.6, 1.0), (0.0, 0.6, 0.6, 1.0) # 下面 - 更暗青色
]
for i, vert in enumerate(vertices):
vertex.addData3f(*vert)
normal.addData3f(*normals[i])
color.addData4f(*face_colors[i])
# 创建几何体
geom = Geom(vdata)
# 创建面(每个面两个三角形)
prim = GeomTriangles(Geom.UHStatic)
# 每个面4个顶点生成2个三角形
for face in range(6): # 6个面
base_index = face * 4
# 第一个三角形
prim.addVertices(base_index, base_index + 1, base_index + 2)
# 第二个三角形
prim.addVertices(base_index, base_index + 2, base_index + 3)
prim.closePrimitive()
geom.addPrimitive(prim)
# 创建几何节点
geom_node = GeomNode('tileset_placeholder')
geom_node.addGeom(geom)
# 添加到场景
cube_node = parent_node.attachNewNode(geom_node)
cube_node.setScale(5) # 设置合适的大小
# 设置双面渲染
cube_node.setTwoSided(True)
# 添加材质
material = Material()
material.setBaseColor((0.0, 1.0, 1.0, 1.0)) # 青色
material.setSpecular((0.5, 0.5, 0.5, 1.0))
material.setShininess(32.0)
cube_node.setMaterial(material)
# 添加标识标签
cube_node.setTag("element_type", "cesium_placeholder")
print("✓ 占位符几何体创建完成")
return cube_node
except Exception as e:
print(f"✗ 创建占位符几何体出错: {e}")
import traceback
traceback.print_exc()
return None