diff --git a/QPanda3D/Panda3DWorld.py b/QPanda3D/Panda3DWorld.py index 1dec236a..d6a4e3b8 100644 --- a/QPanda3D/Panda3DWorld.py +++ b/QPanda3D/Panda3DWorld.py @@ -135,9 +135,9 @@ class Panda3DWorld(ShowBase): #render_pipeline.set_camera(self.cam) #添加渲染效果�� - self.cam = self.render_pipeline._showbase.cam - self.camNode = self.cam.node() - self.camLens = self.camNode.get_lens() + #self.cam = self.render_pipeline._showbase.cam + #self.camNode = self.cam.node() + #self.camLens = self.camNode.get_lens() self.render_pipeline._showbase.camera = self.render_pipeline._showbase.cam #self.render_pipeline.daytime_mgr.update() diff --git a/RenderPipelineFile/config/daytime.yaml b/RenderPipelineFile/config/daytime.yaml index 864c1be0..633469d4 100644 --- a/RenderPipelineFile/config/daytime.yaml +++ b/RenderPipelineFile/config/daytime.yaml @@ -17,8 +17,8 @@ control_points: scattering: sun_intensity: [[[0.0000000000,0.0000000000],[0.0041666667,0.0000000000],[0.0083333333,0.0000000000],[0.0125000000,0.0000000000],[0.0166666667,0.0000000000],[0.0208333333,0.0000000000],[0.0250000000,0.0000000000],[0.0291666667,0.0000000000],[0.0333333333,0.0000000000],[0.0375000000,0.0000000000],[0.0416666667,0.0000000000],[0.0458333333,0.0000000000],[0.0500000000,0.0000000000],[0.0541666667,0.0000000000],[0.0583333333,0.0000000000],[0.0625000000,0.0000000000],[0.0666666667,0.0000000000],[0.0708333333,0.0000000000],[0.0750000000,0.0000000000],[0.0791666667,0.0000000000],[0.0833333333,0.0000000000],[0.0875000000,0.0000000000],[0.0916666667,0.0000000000],[0.0958333333,0.0000000000],[0.1000000000,0.0000000000],[0.1041666667,0.0000000000],[0.1083333333,0.0000000000],[0.1125000000,0.0000000000],[0.1166666667,0.0000000000],[0.1208333333,0.0000000000],[0.1250000000,0.0000000000],[0.1291666667,0.0000000000],[0.1333333333,0.0000000000],[0.1375000000,0.0000000000],[0.1416666667,0.0000000000],[0.1458333333,0.0000000000],[0.1500000000,0.0000000000],[0.1541666667,0.0000000000],[0.1583333333,0.0000028805],[0.1625000000,0.0003577724],[0.1666666667,0.0013331400],[0.1708333333,0.0029671803],[0.1750000000,0.0052963381],[0.1791666667,0.0083550556],[0.1833333333,0.0121755589],[0.1875000000,0.0167876159],[0.1916666667,0.0222183530],[0.1958333333,0.0284919947],[0.2000000000,0.0356297193],[0.2041666667,0.0436494349],[0.2083333333,0.0525656099],[0.2125000000,0.0623891610],[0.2166666667,0.0731272461],[0.2208333333,0.0847831708],[0.2250000000,0.0973563167],[0.2291666667,0.1108419698],[0.2333333333,0.1252313631],[0.2375000000,0.1405115250],[0.2416666667,0.1566653434],[0.2458333333,0.1736715009],[0.2500000000,0.1915046014],[0.2541666667,0.2101350464],[0.2583333333,0.2295292930],[0.2625000000,0.2496498145],[0.2666666667,0.2704552670],[0.2708333333,0.2919006662],[0.2750000000,0.3139375192],[0.2791666667,0.3365139497],[0.2833333333,0.3595750662],[0.2875000000,0.3830630359],[0.2916666667,0.4069173972],[0.2958333333,0.4310753462],[0.3000000000,0.4554720417],[0.3041666667,0.4800408236],[0.3083333333,0.5047136020],[0.3125000000,0.5294212108],[0.3166666667,0.5540936424],[0.3208333333,0.5786605298],[0.3250000000,0.6030514553],[0.3291666667,0.6271963182],[0.3333333333,0.6510256858],[0.3375000000,0.6744711982],[0.3416666667,0.6974659988],[0.3458333333,0.7199450163],[0.3500000000,0.7418453485],[0.3541666667,0.7631067095],[0.3583333333,0.7836717291],[0.3625000000,0.8034862953],[0.3666666667,0.8224999302],[0.3708333333,0.8406661079],[0.3750000000,0.8579425235],[0.3791666667,0.8742914270],[0.3833333333,0.8896799131],[0.3875000000,0.9040801386],[0.3916666667,0.9174695289],[0.3958333333,0.9298310650],[0.4000000000,0.9411533765],[0.4041666667,0.9514309312],[0.4083333333,0.9606641691],[0.4125000000,0.9688595571],[0.4166666667,0.9760296330],[0.4208333333,0.9821930708],[0.4250000000,0.9873746114],[0.4291666667,0.9916050060],[0.4333333333,0.9949209310],[0.4375000000,0.9973647924],[0.4416666667,0.9989845508],[0.4458333333,0.9998334497],[0.4500000000,0.9999696949],[0.4541666667,0.9994560801],[0.4583333333,0.9983595429],[0.4625000000,0.9967506613],[0.4666666667,0.9947030614],[0.4708333333,0.9922927758],[0.4750000000,0.9895975125],[0.4791666667,0.9866958610],[0.4833333333,0.9836664262],[0.4875000000,0.9805868867],[0.4916666667,0.9775330316],[0.4958333333,0.9745777179],[0.5000000000,0.9717898417],[0.5041666667,0.9692332877],[0.5083333333,0.9669658924],[0.5125000000,0.9650384806],[0.5089595376,0.9690650222],[0.5208333333,0.9623666659],[0.5250000000,0.9616814371],[0.5291666667,0.9614534423],[0.5333333333,0.9616877089],[0.5375000000,0.9623790807],[0.5416666667,0.9635123329],[0.5458333333,0.9650624244],[0.5500000000,0.9669949804],[0.5541666667,0.9692669864],[0.5583333333,0.9718275065],[0.5625000000,0.9746185969],[0.5666666667,0.9775762863],[0.5708333333,0.9806315864],[0.5750000000,0.9837115661],[0.5791666667,0.9867403433],[0.5833333333,0.9896401655],[0.5875000000,0.9923323562],[0.5916666667,0.9947382579],[0.5958333333,0.9967800977],[0.6000000000,0.9983817820],[0.6041666667,0.9994696263],[0.6083333333,0.9999730028],[0.6125000000,0.9998249266],[0.6166666667,0.9989625601],[0.6208333333,0.9973276624],[0.6250000000,0.9948669567],[0.6291666667,0.9915324664],[0.6333333333,0.9872817545],[0.6375000000,0.9820781426],[0.6416666667,0.9758908775],[0.6458333333,0.9686952146],[0.6500000000,0.9604725211],[0.6541666667,0.9512102537],[0.6583333333,0.9409019858],[0.6625000000,0.9295473441],[0.6666666667,0.9171518878],[0.6708333333,0.9037270619],[0.6750000000,0.8892899902],[0.6791666667,0.8738633008],[0.6833333333,0.8574749656],[0.6875000000,0.8401579787],[0.6916666667,0.8219502453],[0.6958333333,0.8028941798],[0.7000000000,0.7830364456],[0.7041666667,0.7624277344],[0.7083333333,0.7411222520],[0.7125000000,0.7191776044],[0.7166666667,0.6966542563],[0.7208333333,0.6736152714],[0.7250000000,0.6501259629],[0.7291666667,0.6262533880],[0.7333333333,0.6020661121],[0.7375000000,0.5776338043],[0.7416666667,0.5530267796],[0.7458333333,0.5283156992],[0.7500000000,0.5035711751],[0.7541666667,0.4788634341],[0.7583333333,0.4542618347],[0.7625000000,0.4298347613],[0.7666666667,0.4056490351],[0.7708333333,0.3817697830],[0.7750000000,0.3582600107],[0.7791666667,0.3351803495],[0.7833333333,0.3125888445],[0.7875000000,0.2905406366],[0.7916666667,0.2690876955],[0.7958333333,0.2482787388],[0.8000000000,0.2281588906],[0.8041666667,0.2087696425],[0.8083333333,0.1901486315],[0.8125000000,0.1723295359],[0.8166666667,0.1553419918],[0.8208333333,0.1392115328],[0.8250000000,0.1239595144],[0.8291666667,0.1096030703],[0.8333333333,0.0961551918],[0.8375000000,0.0836246599],[0.8416666667,0.0720161369],[0.8458333333,0.0613302273],[0.8500000000,0.0515635598],[0.8541666667,0.0427088803],[0.8583333333,0.0347551990],[0.8625000000,0.0276878920],[0.8666666667,0.0214889271],[0.8708333333,0.0161369711],[0.8750000000,0.0116076130],[0.8791666667,0.0078735477],[0.8833333333,0.0049047927],[0.8875000000,0.0026688977],[0.8916666667,0.0011311782],[0.8958333333,0.0002549473],[0.9000000000,0.0000000000],[0.9041666667,0.0000000000],[0.9083333333,0.0000000000],[0.9125000000,0.0000000000],[0.9166666667,0.0000000000],[0.9208333333,0.0000000000],[0.9250000000,0.0000000000],[0.9291666667,0.0000000000],[0.9333333333,0.0000000000],[0.9375000000,0.0000000000],[0.9416666667,0.0000000000],[0.9458333333,0.0000000000],[0.9500000000,0.0000000000],[0.9541666667,0.0000000000],[0.9583333333,0.0000000000],[0.9625000000,0.0000000000],[0.9666666667,0.0000000000],[0.9708333333,0.0000000000],[0.9750000000,0.0000000000],[0.9791666667,0.0000000000],[0.9833333333,0.0000000000],[0.9875000000,0.0000000000],[0.9916666667,0.0000000000],[0.9958333333,0.0000000000]]] sun_color: [[[0.5010435645,0.5818710306],[0.0433100000,0.8999700000],[0.8635787716,0.9130000000],[0.1785000000,0.8973600000],[0.8099800000,0.8651100000],[0.2360800000,0.7712700000],[0.6583432177,0.8485126184],[0.1266806142,0.9648102053],[0.9558541267,0.9090909091],[0.5568400771,0.7353760446]],[[0.5001318426,0.5160300000],[0.0572700000,0.6541600000],[0.2395000000,0.5976800000],[0.8104600000,0.6009000000],[0.6967400000,0.5483900000]],[[0.0862400000,0.4257800000],[0.4955600000,0.4033000000],[0.8234200000,0.4340200000]]] - sun_azimuth: [[[0.5000000000,0.1472222222]]] - sun_altitude: [[[0.5000000000,1.0000000000]]] + sun_azimuth: [[[0.5000000000,0.4055555556]]] + sun_altitude: [[[0.5000000000,0.7111111111]]] extinction: [[[0.4913294798,0.6378830084]]] volumetrics: fog_ramp_size: [[[0.5510597303,0.7409470752]]] diff --git a/RenderPipelineFile/effects/simple_transparent.yaml b/RenderPipelineFile/effects/simple_transparent.yaml new file mode 100644 index 00000000..d1fbc2fc --- /dev/null +++ b/RenderPipelineFile/effects/simple_transparent.yaml @@ -0,0 +1,26 @@ +# simple_transparent.yaml +vertex_shader: | + #version 330 + uniform mat4 p3d_ModelViewProjectionMatrix; + in vec4 p3d_Vertex; + in vec2 p3d_MultiTexCoord0; + out vec2 texcoord; + void main() { + gl_Position = p3d_ModelViewProjectionMatrix * p3d_Vertex; + texcoord = p3d_MultiTexCoord0; + } + +fragment_shader: | + #version 330 + uniform sampler2D p3d_Texture0; + uniform float material_opacity = 1.0; + in vec2 texcoord; + out vec4 o_color; + void main() { + vec4 c = texture(p3d_Texture0, texcoord); + o_color = vec4(c.rgb, c.a * material_opacity); + } + +render_states: + TransparencyAttrib: M_alpha + DepthWriteAttrib: 0 \ No newline at end of file diff --git a/core/selection.py b/core/selection.py index 3a6f2c63..dcf43051 100644 --- a/core/selection.py +++ b/core/selection.py @@ -398,7 +398,6 @@ class SelectionSystem: material.setRoughness(1.0) material.setMetallic(0.0) axis_nodes[axis].setMaterial(material) - print("自发光材质设置完成") except Exception as e: print(f"自发光材质设置失败: {str(e)}") @@ -419,8 +418,6 @@ class SelectionSystem: axis_node.setDepthWrite(False) axis_node.setLightOff() - print("✓ 坐标轴渲染设置完成") - except Exception as e: print(f"设置坐标轴渲染失败!!!!!!: {str(e)}") @@ -440,10 +437,8 @@ class SelectionSystem: # 第三步:强制设置每个轴的独立渲染 self._forceAxisIndependentRendering() - print("✅ 激进 RenderPipeline 兼容坐标轴设置完成") except Exception as e: - print(f"❌ 激进设置失败: {e}") # 使用最后的备用方案 self._setupUltimateGizmoFallback() @@ -488,8 +483,6 @@ class SelectionSystem: axis_node.setTag("unlit","1") axis_node.setColorScale(2.0,2.0,2.0,1.0) - print(" ✓ 坐标轴完全隔离") - except Exception as e: print(f" ❌ 隔离失败: {e}") @@ -514,8 +507,6 @@ class SelectionSystem: ) self.gizmo.setState(minimal_state, 10000) - print(" ✓ 最简渲染设置完成") - except Exception as e: print(f" ❌ 最简渲染设置失败: {e}") @@ -534,7 +525,6 @@ class SelectionSystem: # 每个轴都完全独立设置 self._setupSingleAxisRendering(axis_node, name, color, 0) - print(" ✓ 独立轴渲染设置完成") except Exception as e: print(f" ❌ 独立轴渲染设置失败: {e}") @@ -575,8 +565,6 @@ class SelectionSystem: def _setupUltimateGizmoFallback(self): """最后的备用方案 - 最简单的渲染""" try: - print("🚨 使用最后备用方案...") - # 最简单的设置 self.gizmo.setLightOff() self.gizmo.setFogOff() @@ -603,8 +591,6 @@ class SelectionSystem: self.gizmoZAxis.setBin("gui-popup", 20003) self.gizmoZAxis.setDepthTest(False) - print("✅ 最后备用方案设置完成") - except Exception as e: print(f"❌ 最后备用方案也失败: {e}") @@ -672,7 +658,6 @@ class SelectionSystem: self.gizmoTargetStartPos = None self.gizmoStartPos = None - print("清除了坐标轴") def setGizmoAxisColor(self, axis, color): """设置坐标轴颜色 - RenderPipeline 兼容版本""" @@ -772,7 +757,6 @@ class SelectionSystem: axis_node.setTag("gizmo_axis", axis_name) axis_node.setTag("pickable", "1") - print("✓ 坐标轴鼠标事件设置完成") except Exception as e: print(f"设置坐标轴鼠标事件失败: {e}") @@ -782,21 +766,15 @@ class SelectionSystem: def checkGizmoClick(self, mouseX, mouseY): """使用屏幕空间检测是否点击了坐标轴""" if not self.gizmo or not self.gizmoTarget: - print("坐标轴点击检测:坐标轴或目标不存在") return None # 基本参数验证 if not isinstance(mouseX, (int, float)) or not isinstance(mouseY, (int, float)): - print(f"坐标轴点击检测:无效的鼠标坐标 ({mouseX}, {mouseY})") return None try: - print(f"\n=== 坐标轴点击检测 ===") - print(f"鼠标位置: ({mouseX}, {mouseY})") - # 获取坐标轴中心的世界坐标 gizmo_world_pos = self.gizmo.getPos(self.world.render) - print(f"坐标轴世界位置: {gizmo_world_pos}") # 计算各轴端点的世界坐标 x_end = gizmo_world_pos + Vec3(self.axis_length, 0, 0) @@ -833,7 +811,6 @@ class SelectionSystem: # 如果无法获得屏幕坐标,使用备用方法 if not center_screen: - print("使用备用检测方法...") return self.checkGizmoClickFallback(mouseX, mouseY) # 计算点击阈值 @@ -872,7 +849,6 @@ class SelectionSystem: print(f"✓ 点击了{axis_label}") return axis_name - print("× 没有点击任何轴") return None except Exception as e: @@ -883,7 +859,6 @@ class SelectionSystem: def checkGizmoClickFallback(self, mouseX, mouseY): """备用检测方法:使用固定的屏幕区域""" - print("使用备用点击检测...") # 获取准确的窗口尺寸 win_width, win_height = self.world.getWindowSize() diff --git a/core/world.py b/core/world.py index 9fbef972..d987d256 100644 --- a/core/world.py +++ b/core/world.py @@ -37,27 +37,29 @@ class CoreWorld(Panda3DWorld): self._setupLighting() self._setupGround() self._loadFont() - #self.load_and_play_fbx_model() + #self.load_and_play_glb_model() - def load_and_play_fbx_model(self): + def load_and_play_glb_model(self): """加载 glTF 模型并播放动画""" try: from direct.actor.Actor import Actor # 使用 Actor 类加载 glTF 模型 - self.model = Actor("/home/tiger/Women.glb") + self.model = Actor("/home/tiger/cube.glb") print("模型加载成功!") self.model.reparentTo(self.render) self.model.setPos(0, 10, 0) self.model.setScale(10) - # 列出所有可用动画 - anims = self.model.getAnimNames() - print("可用动画:", anims) - - # 播放特定动画 - if anims: - self.model.loop('Armature|mixamo.com|Layer0') + # 找出所有 AnimBundleNode + print(f"开始寻找动画AnimationBundleNode...") + for np in self.model.findAllMatches("**/+AnimBundleNode"): + print(f"找到AnimBundleNode: {np.getName()}") + bundle = np.node().getBundle() + for i in range(bundle.getNumAnimations()): + anim_name = bundle.getAnimation(i).getName() + print("动画名:", anim_name) + # 这里不能直接 play,需要手动把 AnimControl 绑定到节点 except Exception as e: diff --git a/install_fbx2gltf.sh b/install_fbx2gltf.sh new file mode 100755 index 00000000..2feb3784 --- /dev/null +++ b/install_fbx2gltf.sh @@ -0,0 +1,38 @@ +#!/bin/bash + +# FBX2glTF 安装脚本 +echo "开始安装 FBX2glTF..." + +# 创建工具目录 +mkdir -p ~/tools + +# 方案1: 尝试下载 Godot 维护的版本 +echo "尝试下载 Godot 维护的 FBX2glTF..." +if wget -O ~/tools/FBX2glTF https://github.com/godotengine/FBX2glTF/releases/download/v0.13.1/FBX2glTF-linux-x64 2>/dev/null; then + chmod +x ~/tools/FBX2glTF + echo "✓ FBX2glTF 下载成功" +else + echo "× Godot版本下载失败,尝试原版..." + + # 方案2: 尝试下载 Facebook 原版 + if wget -O ~/tools/FBX2glTF https://github.com/facebookincubator/FBX2glTF/releases/download/v0.9.7/FBX2glTF-linux-x64 2>/dev/null; then + chmod +x ~/tools/FBX2glTF + echo "✓ FBX2glTF 原版下载成功" + else + echo "× 下载失败,请手动安装" + echo " 1. 访问: https://github.com/godotengine/FBX2glTF/releases" + echo " 2. 下载 FBX2glTF-linux-x64" + echo " 3. 移动到 ~/tools/FBX2glTF" + echo " 4. 运行: chmod +x ~/tools/FBX2glTF" + exit 1 + fi +fi + +# 添加到 PATH +if ! grep -q "~/tools" ~/.bashrc; then + echo 'export PATH="$HOME/tools:$PATH"' >> ~/.bashrc + echo "✓ 已添加到 PATH" +fi + +echo "✓ FBX2glTF 安装完成" +echo "请运行: source ~/.bashrc 或重启终端" \ No newline at end of file diff --git a/main.py b/main.py index b0235f8f..1a092946 100644 --- a/main.py +++ b/main.py @@ -370,13 +370,16 @@ class MyWorld(CoreWorld): """更新属性面板显示 - 代理到property_panel""" return self.property_panel.updatePropertyPanel(item) - def addAnimationPanel(self,originmodel,filepath): - return self.property_panel._addAnimationPanel(originmodel,filepath) + # def addAnimationPanel(self,originmodel,filepath): + # return self.property_panel._addAnimationPanel(originmodel,filepath) def updateGUIPropertyPanel(self, gui_element): """更新GUI元素属性面板 - 代理到property_panel""" return self.property_panel.updateGUIPropertyPanel(gui_element) + def removeActorForModel(self,model): + return self.property_panel.removeActorForModel( model) + # ==================== 工具管理代理 ==================== def setCurrentTool(self, tool): diff --git a/scene/scene_manager.py b/scene/scene_manager.py index f3239bb0..c15ca830 100644 --- a/scene/scene_manager.py +++ b/scene/scene_manager.py @@ -38,19 +38,40 @@ class SceneManager: # ==================== 模型导入和处理 ==================== - def importModel(self, filepath, apply_unit_conversion=False, normalize_scales=True): + 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=== 开始导入模型: {filepath} ===") print(f"单位转换: {'开启' if apply_unit_conversion 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"⚠️ 转换失败,使用原始文件") # 总是重新加载模型以确保材质信息完整 # 不使用ModelPool缓存,避免材质信息丢失问题 @@ -66,6 +87,12 @@ class SceneManager: # 将模型添加到场景 model.reparentTo(self.world.render) + # 保存原始路径和转换后的路径 + model.setTag("model_path", filepath) + model.setTag("original_path", original_filepath) + if filepath != original_filepath: + model.setTag("converted_from", os.path.splitext(original_filepath)[1]) + model.setTag("converted_to_glb", "true") # 可选的单位转换(主要针对FBX) if apply_unit_conversion and filepath.lower().endswith('.fbx'): @@ -952,4 +979,265 @@ class SceneManager: 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 diff --git a/ui/interface_manager.py b/ui/interface_manager.py index dba3c8c1..bb4386cd 100644 --- a/ui/interface_manager.py +++ b/ui/interface_manager.py @@ -91,6 +91,7 @@ class InterfaceManager: """删除节点""" try: # 从场景中移除 + self.world.property_panel.removeActorForModel(nodePath) nodePath.removeNode() # 如果是模型根节点,从模型列表中移除 diff --git a/ui/property_panel.py b/ui/property_panel.py index 27bd8632..166b70c2 100644 --- a/ui/property_panel.py +++ b/ui/property_panel.py @@ -3,11 +3,12 @@ from types import new_class from typing import Hashable from PyQt5.QtWidgets import (QLabel, QLineEdit, QDoubleSpinBox, QPushButton, - QTreeWidget, QTreeWidgetItem, QMenu, QCheckBox, QComboBox, QHBoxLayout, QWidget) + QTreeWidget, QTreeWidgetItem, QMenu, QCheckBox, QComboBox, QHBoxLayout, QWidget, + QVBoxLayout) from PyQt5.QtCore import Qt from deploy_libs.unicodedata import normalize from direct.actor.Actor import Actor -from panda3d.core import Vec3, Vec4, transpose, TransparencyAttrib +from panda3d.core import Vec3, Vec4, transpose, TransparencyAttrib, PartGroup class PropertyPanelManager: @@ -17,6 +18,7 @@ class PropertyPanelManager: """初始化属性面板管理器""" self.world = world self._propertyLayout = None + self._actor_cache={} def setPropertyLayout(self, layout): """设置属性面板布局引用""" @@ -177,7 +179,7 @@ class PropertyPanelManager: zScale.valueChanged.connect(lambda v: model.setScale(model.getScale().getX(), model.getScale().getY(), v)) self._propertyLayout.addRow("缩放 Z:", zScale) - self._addAnimationPanel(model,model) + self._addAnimationPanel(model) self._addSunAzimuthPanel() material_title = QLabel("材质属性") @@ -638,7 +640,7 @@ class PropertyPanelManager: def _updateModelMaterialPanel(self,model): """模型材质属性""" if model.is_empty(): - print("警告: 无法在空的 NodePath 上查找材质") + no_material_label = QLabel("无材质") no_material_label.setStyleSheet("color: gray;font-style:italic;") self._propertyLayout.addRow("材质:", no_material_label) @@ -668,12 +670,10 @@ class PropertyPanelManager: # 使用几何节点名称作为材质标题 geom_node_name = geom_node.getName() unique_name = f"{geom_node_name}({model_name})" - print(f"材质 {i}: 使用几何节点名称 '{geom_node_name}'") else: # 回退到原有的材质名称逻辑 material_name = material.get_name() if hasattr(material,'get_name') and material.get_name() else f"材质{i + 1}" unique_name = f"{material_name}({model_name})" - print(f"材质 {i}: 未找到几何节点,使用材质名称 '{material_name}'") # 处理重复名称 if unique_name in name_counter: @@ -705,7 +705,7 @@ class PropertyPanelManager: base_color = self._getOrCreateMaterialBaseColor(material) if base_color is not None: - print(f"材质基础颜色: {base_color}") + #print(f"材质基础颜色: {base_color}") #R分量 r_spinbox = QDoubleSpinBox() @@ -943,7 +943,6 @@ class PropertyPanelManager: print(f"直接属性设置失败: {e}") if success: - self._invalidateRenderState() print(f"材质基础颜色已更新: {new_color}") else: print(f"✗ 所有更新方法都失败了") @@ -964,7 +963,6 @@ class PropertyPanelManager: current_color = material.base_color material.base_color = Vec4(current_color.x,current_color.y,current_color.z,alpha_value) print(f"更新基础颜色透明度{alpha_value}") - self._invalidateRenderState() print(f"材质透明度已更新:{alpha_value}") except Exception as e: print(f"更新材质透明度失败: {e}") @@ -976,7 +974,6 @@ class PropertyPanelManager: print(f"材质不支持粗糙度属性或值为None,跳过更新") return material.set_roughness(value) - self._invalidateRenderState() except Exception as e: print(f"更新材质粗糙度失败: {e}") @@ -987,7 +984,6 @@ class PropertyPanelManager: print(f"材质不支持金属性属性或值为None,跳过更新") return material.set_metallic(value) - self._invalidateRenderState() except Exception as e: print(f"更新材质金属性失败: {e}") @@ -998,7 +994,6 @@ class PropertyPanelManager: print(f"材质不支持折射率属性或值为None,跳过更新") return material.set_refractive_index(value) - self._invalidateRenderState() except Exception as e: print(f"更新材质折射率失败: {e}") @@ -1030,13 +1025,6 @@ class PropertyPanelManager: print(f"检查材质状态时出错: {e}") return "未知材质类型(可尝试编辑)" - def _invalidateRenderState(self): - """使渲染状态失效以应用材质更改(无闪烁版本)""" - # 完全不做任何操作,避免闪烁 - # 现代渲染管线会自动检测纹理变化并更新 - print("材质更改已应用,无需手动刷新渲染状态") - - def _getTextureModeString(self, mode): """获取纹理模式的字符串表示""" @@ -1100,7 +1088,7 @@ class PropertyPanelManager: try: base_color = material.get_base_color() if base_color is not None: - print(f"✓ 通过get_base_color()获取: {base_color}") + #print(f"✓ 通过get_base_color()获取: {base_color}") return base_color except: pass @@ -1357,7 +1345,6 @@ class PropertyPanelManager: print(f"阶段 {i}: {stage.getName()}, Sort: {stage.getSort()}, 模式: {mode_name}, 纹理: {tex.getName() if tex else 'None'}") print("==========================================") - self._invalidateRenderState() print(f"漫反射贴图已成功应用:{texture_path}") else: print(f"未找到材质标题对应的材质或节点: {material_title}") @@ -1368,29 +1355,6 @@ class PropertyPanelManager: import traceback traceback.print_exc() - # def _applyNormalTexture(self, material, texture_path): - # """应用法线贴图""" - # try: - # from RenderPipelineFile.rpcore.loader import RPLoader - # from panda3d.core import TextureStage - # - # texture = RPLoader.load_texture(texture_path) - # if texture: - # node = self._findNodeWithMaterial(material) - # if node: - # # 创建法线贴图纹理阶段 - # normal_stage = TextureStage("normal") - # normal_stage.setSort(1) # 设置排序优先级 - # node.setTexture(normal_stage, texture) - # self._invalidateRenderState() - # print(f"法线贴图已应用:{texture_path}") - # else: - # print("未找到材质对应的节点") - # except Exception as e: - # print(f"应用法线贴图失败:{e}") - # import traceback - # traceback.print_exc() - def _applyNormalTexture(self, material, texture_path): """应用法线贴图 - Blender风格效果""" try: @@ -2060,7 +2024,6 @@ class PropertyPanelManager: node.setTexture(parallax_stage,texture) print("视差贴图已应用到p3d_Texture4槽") - self._invalidateRenderState() print(f"视差贴图已成功应用:{texture_path}") else: print("未找到材质对应节点") @@ -2389,7 +2352,6 @@ class PropertyPanelManager: material.set_emission(new_emission) print("材质着色模型已设置为自发光") - self._invalidateRenderState() print(f"自发光贴图已成功应用:{texture_path}") else: print("未找到材质对应的节点") @@ -2429,7 +2391,6 @@ class PropertyPanelManager: print("AO贴图已应用到p3d_Texture7槽") print("注意:AO贴图需要自定义shader支持才能正确显示") - self._invalidateRenderState() print(f"AO贴图已成功应用:{texture_path}") else: print("未找到材质对应的节点") @@ -2479,7 +2440,6 @@ class PropertyPanelManager: material.set_emission(new_emission) print("材质着色模型已设置为透明") - self._invalidateRenderState() print(f"透明度贴图已成功应用:{texture_path}") else: print("未找到材质对应的节点") @@ -2519,7 +2479,6 @@ class PropertyPanelManager: print("细节贴图已应用到p3d_Texture9槽") print("注意:细节贴图需要自定义shader支持才能正确显示") - self._invalidateRenderState() print(f"细节贴图已成功应用:{texture_path}") else: print("未找到材质对应的节点") @@ -2559,7 +2518,6 @@ class PropertyPanelManager: print("光泽贴图已应用到p3d_Texture10槽") print("注意:光泽贴图需要自定义shader支持才能正确显示") - self._invalidateRenderState() print(f"光泽贴图已成功应用:{texture_path}") else: print("未找到材质对应的节点") @@ -2623,7 +2581,6 @@ class PropertyPanelManager: def _findMaterialAndNodeByTitle(self, material_title): """根据材质标题查找对应的材质和节点""" - print(f"正在查找材质标题: '{material_title}'") current_item = self.world.treeWidget.currentItem() if not current_item: @@ -2671,17 +2628,17 @@ class PropertyPanelManager: """查找使用指定材质的具体几何节点""" from panda3d.core import MaterialAttrib, GeomNode - print(f"查找材质: {target_material.get_name() if hasattr(target_material, 'get_name') else 'unnamed'}") + #print(f"查找材质: {target_material.get_name() if hasattr(target_material, 'get_name') else 'unnamed'}") # 首先尝试查找GeomNode geom_nodes = model.find_all_matches("**/+GeomNode") - print(f"找到 {len(geom_nodes)} 个几何节点") + #print(f"找到 {len(geom_nodes)} 个几何节点") # 如果没有找到GeomNode,尝试查找所有子节点 if len(geom_nodes) == 0: - print("未找到GeomNode,尝试查找所有子节点...") + #print("未找到GeomNode,尝试查找所有子节点...") all_nodes = model.find_all_matches("**") - print(f"找到 {len(all_nodes)} 个子节点") + #print(f"找到 {len(all_nodes)} 个子节点") for node_np in all_nodes: node = node_np.node() @@ -2692,14 +2649,14 @@ class PropertyPanelManager: for geom_np in geom_nodes: geom_node = geom_np.node() geom_count = geom_node.get_num_geoms() - print(f"检查几何节点 {geom_node.get_name()}: {geom_count} 个几何体") + #rint(f"检查几何节点 {geom_node.get_name()}: {geom_count} 个几何体") for i in range(geom_count): state = geom_node.get_geom_state(i) if state.has_attrib(MaterialAttrib): material = state.get_attrib(MaterialAttrib).get_material() if material == target_material: - print(f"找到匹配的几何节点: {geom_np.get_name()}") + #print(f"找到匹配的几何节点: {geom_np.get_name()}") return geom_np print("未找到匹配的几何节点") @@ -2726,14 +2683,14 @@ class PropertyPanelManager: # 使用现有的精确查找方法 geom_node = self._findSpecificGeomNodeWithMaterial(current_model, target_material) if geom_node: - print(f"✓ 找到特定几何节点: {geom_node.getName()}") + #print(f"✓ 找到特定几何节点: {geom_node.getName()}") # 存储映射以供后续使用 if not hasattr(self, '_material_geom_mapping'): self._material_geom_mapping = {} self._material_geom_mapping[material_id] = geom_node return geom_node else: - print("⚠️ 未找到特定几何节点,使用模型节点(可能影响所有材质)") + #print("⚠️ 未找到特定几何节点,使用模型节点(可能影响所有材质)") return current_model print("❌ 未找到当前选中的模型") @@ -2743,23 +2700,23 @@ class PropertyPanelManager: """查找使用指定材质的具体几何节点""" from panda3d.core import MaterialAttrib - print(f"查找材质: {target_material.get_name() if hasattr(target_material, 'get_name') else 'unnamed'}") + #print(f"查找材质: {target_material.get_name() if hasattr(target_material, 'get_name') else 'unnamed'}") # 遍历模型下的所有几何节点 geom_nodes = model.find_all_matches("**/+GeomNode") - print(f"找到 {len(geom_nodes)} 个几何节点") + #print(f"找到 {len(geom_nodes)} 个几何节点") for geom_np in geom_nodes: geom_node = geom_np.node() geom_count = geom_node.get_num_geoms() - print(f"几何节点 {geom_node.get_name()}: {geom_count} 个几何体") + #print(f"几何节点 {geom_node.get_name()}: {geom_count} 个几何体") for i in range(geom_count): state = geom_node.get_geom_state(i) if state.has_attrib(MaterialAttrib): material = state.get_attrib(MaterialAttrib).get_material() if material == target_material: - print(f"找到匹配的几何节点: {geom_np.get_name()}") + #print(f"找到匹配的几何节点: {geom_np.get_name()}") return geom_np else: print(f"几何体 {i} 没有材质属性") @@ -2791,7 +2748,6 @@ class PropertyPanelManager: material.set_metallic(value) elif property_name == "ior": material.set_refractive_index(value) - self._invalidateRenderState() def _addShadingModelPanel(self, material): """添加着色模型选择面板""" @@ -2885,7 +2841,6 @@ class PropertyPanelManager: new_emission = Vec4(float(model_index), current_emission.y, current_emission.z, current_emission.w) material.set_emission(new_emission) - self._invalidateRenderState() # 刷新UI以更新相关控件的值 if model_index in [1, 3]: # 自发光或透明模式 @@ -2928,36 +2883,44 @@ class PropertyPanelManager: # 更新基础颜色的Alpha通道 self._updateMaterialAlphaForTransparency(material, opacity_value) - self._invalidateRenderState() - print(f"透明度已更新:") - print(f" - emission.y = {opacity_value}") - print(f" - base_color.w = {opacity_value}") - print(f" - 视觉透明度 = {1.0 - opacity_value:.2f}") - except Exception as e: print(f"更新透明度失败: {e}") - def _updateMaterialAlphaForTransparency(self, material, transparency_value): - """更新材质基础颜色的Alpha通道以匹配透明度""" - try: - # 获取当前基础颜色 - current_color = self._getOrCreateMaterialBaseColor(material) - if current_color is not None: - from panda3d.core import Vec4 - # 设置Alpha通道为透明度值 - new_color = Vec4(current_color.x, current_color.y, current_color.z, transparency_value) + def _updateMaterialAlphaForTransparency(self, material, opacity_slider): + """ + opacity_slider: 0=全透 1=不透 + shader 需要 1-opacity_slider 作为真正的 alpha + """ + from panda3d.core import Vec4, TransparencyAttrib - # 尝试多种方式设置颜色 - if hasattr(material, 'set_base_color'): - material.set_base_color(new_color) - elif hasattr(material, 'setDiffuse'): - material.setDiffuse(new_color) + current_item = self.world.treeWidget.currentItem() + if not current_item: + return + model = current_item.data(0, Qt.UserRole) + if model.isEmpty(): + return - self._applyTransparentRenderingEffect() + model.setTransparency(TransparencyAttrib.MAlpha) + model.setTwoSided(True) + model.setDepthWrite(False) + + alpha = opacity_slider # 反转 + color = self._getOrCreateMaterialBaseColor(material) or Vec4(1, 1, 1, 1) + material.base_color=Vec4(color.x, color.y, color.z, alpha) + material.base_color=Vec4(color.x, color.y, color.z, alpha) + + em = material.emission or Vec4(0, 0, 0, 0) + material.set_emission(Vec4(3.0,alpha,em.z,em.w)) + + self.world.render_pipeline.set_effect( + model, + "effects/simple_transparent.yaml", + options={}, + sort=200 + ) + self.world.render_pipeline.prepare_scene(model) + print(f"[透明] 不透明度={opacity_slider:.2f} 已同步") - print(f"材质基础颜色Alpha已更新为: {transparency_value}") - except Exception as e: - print(f"更新材质Alpha通道失败: {e}") def _applyTransparentRenderingEffect(self): from panda3d.core import TransparencyAttrib @@ -3044,7 +3007,6 @@ class PropertyPanelManager: ) material.set_base_color(emissive_color) - self._invalidateRenderState() print(f"自发光强度已更新为: {strength}") def _addMaterialPresetPanel(self, material): @@ -3180,7 +3142,6 @@ class PropertyPanelManager: if preset["shading_model"]==3: self._apply_transparent_effect() - self._invalidateRenderState() #material._applied_preset = preset_name self._refreshMaterialUI() print(f"已应用材质预设: {preset_name}") @@ -3597,7 +3558,6 @@ class PropertyPanelManager: self._propertyLayout.addRow("快速设置:", preset_widget) - print("✅ 太阳控制面板已添加") except Exception as e: print(f"❌ 添加太阳方位角控制面板失败: {e}") @@ -3912,194 +3872,1166 @@ class PropertyPanelManager: print(f"⚠️ 文件通信失败: {e}") return False - def _addAnimationPanel(self,originmodel,filepath): + def _addAnimationPanel(self, origin_model): try: - model = Actor(filepath) - model.reparentTo(self.world.render) - - - model.setPos(0,0,0.3) - - model.hide() - animations = model.getAnimNames() - - if animations: - animation_title=QLabel("动画控制") - animation_title.setStyleSheet("color:#6B6BFF;font-weight:bold;font-size:14px;margin-top:10px;") - self._propertyLayout.addRow(animation_title) - - self.animation_combo = QComboBox() - self.animation_combo.addItems(animations) - self._propertyLayout.addRow("动画名称:", self.animation_combo) - - #播放控制按钮 - button_layout = QHBoxLayout() - - self.play_button = QPushButton("播放") - self.play_button.clicked.connect(lambda:self._playAnimation(model,originmodel)) - button_layout.addWidget(self.play_button) - - self.pause_button = QPushButton("暂停") - self.pause_button.clicked.connect(lambda:self._pauseAnimation(model,originmodel)) - button_layout.addWidget(self.pause_button) - - self.stop_button = QPushButton("停止") - self.stop_button.clicked.connect(lambda:self._stopAnimation(model,originmodel)) - button_layout.addWidget(self.stop_button) - - self.loop_button = QPushButton("循环") - self.loop_button.clicked.connect(lambda:self._loopAnimation(model,originmodel)) - button_layout.addWidget(self.loop_button) - - button_widget = QWidget() - button_widget.setLayout(button_layout) - self._propertyLayout.addRow("控制:", button_widget) - - self.speed_spinbox = QDoubleSpinBox() - self.speed_spinbox.setRange(0.1,5.0) - self.speed_spinbox.setValue(1.0) - self.speed_spinbox.setSingleStep(0.1) - self.speed_spinbox.valueChanged.connect(lambda v:self._setAnimationSpeed(model,v)) - self._propertyLayout.addRow("播放速度:", self.speed_spinbox) - else: + has_animation = False + + # 首先检测骨骼动画 + has_skeletal_anim = False + try: + actor = self._getActor(origin_model) + if actor and actor.getAnimNames(): + self._buildSkeletalUI(origin_model, actor) + has_animation = True + has_skeletal_anim = True + print(f"[信息] 检测到骨骼动画: {actor.getAnimNames()}") + except Exception as actor_error: + # 忽略 Actor 加载错误,很多模型都不是角色动画 + print(f"[信息] 该模型不包含骨骼动画: {actor_error}") + + # 只有在没有骨骼动画时才检测非骨骼动画 + if not has_skeletal_anim: + non_skeletal_anims = self._detectNonSkeletalAnimations(origin_model) + if non_skeletal_anims and self._validateNonSkeletalAnimations(origin_model, non_skeletal_anims): + self._buildNonSkeletalUI(origin_model, non_skeletal_anims) + has_animation = True + print(f"[信息] 检测到非骨骼动画: {list(non_skeletal_anims.keys())}") + + # 如果都没有动画 + if not has_animation: no_anim_label = QLabel("此模型无动画") - no_anim_label.setStyleSheet("color:#888888;font-style:italic;") - self._propertyLayout.addRow("动画:",no_anim_label) - except Exception as e: - print(f"添加动画面板失败: {e}") - - def _detectAnimations(self, model): - """检测模型中的动画""" - try: - from direct.actor.Actor import Actor - - if not isinstance(model, Actor): - model = self._convertToActor(model) - if not model: - print("无法转换为Actor") - return [] - print(f"模型已转换为Actor:{type(model)}") - animations = [] - - animations = model.getAnimNames() - - print(f"检测到的动画: {animations}") - return animations + no_anim_label.setStyleSheet("color:#888;font-style:italic;") + self._propertyLayout.addRow("动画:", no_anim_label) except Exception as e: - print(f"检测动画失败: {e}") - return [] - - def _convertToActor(self, model): - """将NodePath转换为Actor,自动获取模型路径""" - try: - from direct.actor.Actor import Actor - import os - - model_path=model.filepath - if model_path: - actor = Actor(model_path) - actor.reparentTo(self.world.render) - print(f"{model_path}") - print("转化actor成功") - return actor - - except Exception as e: - print(f"转换为Actor失败: {e}") - return None - - def _getModelPath(self,model): - import os - model_root = model.find("**/+ModelRoot") - if not model_root.isEmpty(): - model_root_node = model_root.node() - if hasattr(model_root_node,'get_fullpath'): - fullpath = model_root_node.get_fullpath() - if fullpath and not fullpath.empty(): - return str(fullpath) - tag_path = model.getTag("original_path") - if tag_path: - return tag_path - - return None - - def _playAnimation(self, model,originmodel): - """播放动画""" - try: - print(f"=== 动画播放调试信息 ===") - print(f"模型类型: {type(model)}") - orginPos = originmodel.getPos() - model.setPos(orginPos) - model.show() - originmodel.hide() - - if hasattr(self, 'animation_combo'): - anim_name = self.animation_combo.currentText() - print(f"播放动画: {anim_name}") - self._initActorModel(model,originmodel) - - # 使用Actor的标准方法播放动画 - model.play(anim_name) - - # 验证播放状态 - # control = model.getAnimControl(anim_name) - # if control and control.isPlaying(): - # print(f"✓ 动画播放成功: {anim_name}") - # else: - # print(f"❌ 动画播放失败: {anim_name}") - - except Exception as e: - print(f"播放动画失败: {e}") + print("添加动画面板失败:", e) import traceback traceback.print_exc() - def _pauseAnimation(self,model,originmodel): + def _buildSkeletalUI(self,origin_model,actor): + from PyQt5.QtWidgets import QLabel,QComboBox,QHBoxLayout,QWidget,QPushButton,QDoubleSpinBox + + actor.hide() + origin_model.show() + + animation_title = QLabel("骨骼动画控制") + animation_title.setStyleSheet("color:#6B6BFF;font-weight:bold;font-size:14px;margin-top:10px;") + self._propertyLayout.addRow(animation_title) + + # 获取和分析动画名称 + anim_names = actor.getAnimNames() + processed_names = self._processAnimationNames(origin_model, anim_names) + + # 显示动画信息 + format_info = self._getModelFormat(origin_model) + animation_info = self._analyzeAnimationQuality(actor, anim_names, format_info) + info_text = f"格式: {format_info} | 动画数量: {len(processed_names)}" + if animation_info: + info_text += f" | {animation_info}" + info_label = QLabel(info_text) + info_label.setStyleSheet("color:#888;font-size:10px;") + self._propertyLayout.addRow("信息:", info_label) + + # 如果是 FBX 且动画有问题,添加转换按钮 + if format_info == "FBX" and "⚠️" in animation_info: + convert_btn = QPushButton("🔄 转换FBX动画") + convert_btn.setStyleSheet("background-color:#FFA500;color:white;font-weight:bold;") + convert_btn.clicked.connect(lambda: self._convertFBXManually(origin_model)) + self._propertyLayout.addRow("修复:", convert_btn) + + self.animation_combo = QComboBox() + # 使用处理后的名称,但保留原始名称用于播放 + for display_name, original_name in processed_names: + self.animation_combo.addItem(display_name, original_name) + self._propertyLayout.addRow("动画名称:",self.animation_combo) + + btn_box = QWidget() + btn_lay = QHBoxLayout(btn_box) + for txt,slot in (("播放",self._playAnimation), + ("暂停",self._pauseAnimation), + ("停止",self._stopAnimation), + ("循环",self._loopAnimation)): + btn = QPushButton(txt) + btn.clicked.connect(lambda _,f=slot:f(origin_model)) + btn_lay.addWidget(btn) + self._propertyLayout.addRow("控制:",btn_box) + + self.speed_spinbox = QDoubleSpinBox() + self.speed_spinbox.setRange(0.1,5.0) + self.speed_spinbox.setSingleStep(0.1) + self.speed_spinbox.setValue(1.0) + self.speed_spinbox.valueChanged.connect(lambda v:self._setAnimationSpeed(origin_model,v)) + self._propertyLayout.addRow("播放速度:",self.speed_spinbox) + + def _getModelFormat(self, origin_model): + """获取模型格式信息""" + filepath = origin_model.getTag("model_path") + original_path = origin_model.getTag("original_path") + converted_from = origin_model.getTag("converted_from") + + if filepath: + ext = filepath.lower().split('.')[-1] + format_name = ext.upper() + + # 如果是转换后的文件,显示转换信息 + if converted_from and original_path: + original_ext = converted_from.upper() + format_name = f"{format_name} (从{original_ext}转换)" + + return format_name + return "未知" + + def _processAnimationNames(self, origin_model, anim_names): + """处理和分析动画名称,返回 [(显示名称, 原始名称), ...]""" + format_info = self._getModelFormat(origin_model) + processed = [] + + print(f"[动画分析] 格式: {format_info}, 原始动画名称: {anim_names}") + + for name in anim_names: + display_name = name + original_name = name + + if format_info == "GLB": + # GLB 格式通常有真实的动画名称 + if "|" in name: + # 处理类似 'Armature|mixamo.com|Layer0' 的名称 + parts = name.split("|") + if "mixamo" in name.lower(): + # Mixamo 动画 + display_name = f"Mixamo_{parts[-1]}" if len(parts) > 1 else name + elif len(parts) > 2: + # 其他复杂命名 + display_name = f"{parts[0]}_{parts[-1]}" + else: + display_name = parts[-1] + + elif format_info == "FBX": + # FBX 格式可能需要特殊处理 + if self._isLikelyBoneGroup(name): + # 检查是否是骨骼组而非动画 + print(f"[警告] '{name}' 可能不是真正的动画序列,而是骨骼组") + display_name = f"⚠️ {name} (可能非动画)" + else: + display_name = name + + elif format_info in ["EGG", "BAM"]: + # 原生格式通常命名规范 + display_name = name + + processed.append((display_name, original_name)) + print(f"[动画分析] {original_name} → {display_name}") + + return processed + + def _isLikelyBoneGroup(self, name): + """判断动画名称是否更像骨骼组而不是动画序列""" + bone_indicators = ['joints', 'bones', 'skeleton', 'surface', 'mesh', 'beta', 'rig'] + name_lower = name.lower() + + # 如果包含这些关键词,可能是骨骼组 + for indicator in bone_indicators: + if indicator in name_lower: + return True + + # 如果名称太简单(少于3个字符),可能不是动画 + if len(name) < 3: + return True + + return False + + def _analyzeAnimationQuality(self, actor, anim_names, format_info): + """分析动画质量和类型""" try: - if hasattr(model,'stop'): - self._initActorModel(model, originmodel) - originmodel.hide() - model.show() - model.stop() - print("动画已暂停") + total_frames = 0 + valid_anims = 0 + + for anim_name in anim_names: + try: + control = actor.getAnimControl(anim_name) + if control: + frames = control.getNumFrames() + if frames > 1: + valid_anims += 1 + total_frames += frames + print(f"[动画分析] '{anim_name}': {frames} 帧") + else: + print(f"[动画分析] '{anim_name}': 无有效帧数 ({frames})") + except Exception as e: + print(f"[动画分析] '{anim_name}' 分析失败: {e}") + + if valid_anims == 0: + return "⚠️ 无有效动画序列" + elif valid_anims < len(anim_names): + return f"⚠️ {valid_anims}/{len(anim_names)} 个有效" + else: + avg_frames = total_frames // valid_anims + return f"✓ 平均 {avg_frames} 帧" + except Exception as e: - print(f"暂停动画失败:{e}") + print(f"[动画分析] 分析失败: {e}") + return "分析失败" - def _stopAnimation(self,model,originmodel): + def _getActor(self,origin_model): + if origin_model in self._actor_cache: + return self._actor_cache[origin_model] + filepath = origin_model.getTag("model_path") + if not filepath: + return None + + print(f"[Actor加载] 尝试加载: {filepath}") + + # 检查是否是 FBX 文件,如果是,使用专门的 FBX 动画加载器 + if filepath.lower().endswith('.fbx'): + return self._createFBXActor(origin_model, filepath) + + # 其他格式使用标准 Actor 加载 try: - if hasattr(model,'stop'): - self._initActorModel(model, originmodel) - originmodel.show() - model.hide() - model.stop() - print("动画已停止") + test_actor=Actor(filepath) + anims = test_actor.getAnimNames() + print(f"[Actor加载] 标准加载检测到动画: {anims}") + if not anims: + test_actor.cleanup() + test_actor.removeNode() + return None + actor = Actor(filepath) + actor.reparentTo(self.world.render) + self._actor_cache[origin_model] = actor + return actor except Exception as e: - print(f"停止动画失败:{e}") - - def _loopAnimation(self,model,originmodel): + print(f"创建Actor失败: {e}") + return None + + def _createFBXActor(self, origin_model, filepath): + """专门为 FBX 文件创建 Actor,使用转换方式获取真实动画""" try: - if hasattr(self,'animation_combo'): - self._initActorModel(model, originmodel) - anim_name = self.animation_combo.currentText() - if anim_name and hasattr(model,'loop'): - model.show() - originmodel.hide() - model.loop(anim_name) - print(f"循环播放动画:{anim_name}") + print(f"[FBX动画] 开始处理 FBX 动画: {filepath}") + + # 方法1: 尝试转换 FBX 为包含动画的格式 + converted_actor = self._convertFBXToActor(filepath) + if converted_actor: + converted_actor.reparentTo(self.world.render) + self._actor_cache[origin_model] = converted_actor + print(f"[FBX动画] 转换成功,动画: {converted_actor.getAnimNames()}") + return converted_actor + + # 方法2: 直接加载但进行动画数据修复 + actor = Actor(filepath) + if actor: + fixed_actor = self._fixFBXAnimations(actor, filepath) + if fixed_actor and fixed_actor.getAnimNames(): + fixed_actor.reparentTo(self.world.render) + self._actor_cache[origin_model] = fixed_actor + print(f"[FBX动画] 修复成功,动画: {fixed_actor.getAnimNames()}") + return fixed_actor + + print(f"[FBX动画] 无法获取有效动画数据") + return None + except Exception as e: - print(f"循环播放动画失败:{e}") - - def _setAnimationSpeed(self,model,speed): + print(f"[FBX动画] 处理失败: {e}") + return None + + def _convertFBXToActor(self, fbx_path): + """将 FBX 转换为可用的 Actor""" try: - if hasattr(self,'animation_combo'): - anim_name = self.animation_combo.currentText() - if anim_name and hasattr(model,'setPlayRate'): - model.setPlayRate(speed,anim_name) - print(f"设置动画速度:{speed}") + import tempfile + import os + + # 创建临时文件用于转换 + temp_dir = tempfile.mkdtemp() + egg_path = os.path.join(temp_dir, "converted.egg") + + print(f"[FBX转换] 转换 {fbx_path} 到 {egg_path}") + + # 使用 Panda3D 转换工具链 + # FBX -> Collada -> EGG + try: + # 检查是否有可用的转换工具 + import subprocess + + # 方法1: 尝试直接使用 assimp (如果安装了) + result = subprocess.run([ + 'assimp', 'export', fbx_path, egg_path + ], capture_output=True, text=True, timeout=30) + + if result.returncode == 0 and os.path.exists(egg_path): + actor = Actor(egg_path) + if actor.getAnimNames(): + print(f"[FBX转换] Assimp 转换成功") + return actor + + except (subprocess.TimeoutExpired, subprocess.CalledProcessError, FileNotFoundError): + print(f"[FBX转换] Assimp 转换失败,尝试其他方法") + + # 方法2: 使用 Blender 脚本转换(如果安装了 Blender) + try: + blender_script = f''' +import bpy +import os +bpy.ops.wm.read_factory_settings(use_empty=True) +bpy.ops.import_scene.fbx(filepath="{fbx_path}") +bpy.ops.export_scene.gltf(filepath="{egg_path.replace('.egg', '.gltf')}", export_animations=True) +''' + + script_path = os.path.join(temp_dir, "convert.py") + with open(script_path, 'w') as f: + f.write(blender_script) + + result = subprocess.run([ + 'blender', '--background', '--python', script_path + ], capture_output=True, text=True, timeout=60) + + gltf_path = egg_path.replace('.egg', '.gltf') + if os.path.exists(gltf_path): + # 使用 gltf2bam 转换为 BAM + subprocess.run(['gltf2bam', gltf_path, egg_path.replace('.egg', '.bam')]) + bam_path = egg_path.replace('.egg', '.bam') + if os.path.exists(bam_path): + actor = Actor(bam_path) + if actor.getAnimNames(): + print(f"[FBX转换] Blender 转换成功") + return actor + + except (subprocess.TimeoutExpired, subprocess.CalledProcessError, FileNotFoundError): + print(f"[FBX转换] Blender 转换失败") + + return None + except Exception as e: - print(f"设置动画速度失败:{e}") + print(f"[FBX转换] 转换过程出错: {e}") + return None + + def _fixFBXAnimations(self, actor, fbx_path): + """修复 FBX Actor 的动画数据""" + try: + print(f"[FBX修复] 尝试修复动画数据") + + # 获取所有动画名称 + anim_names = actor.getAnimNames() + print(f"[FBX修复] 原始动画名称: {anim_names}") + + # 检查每个动画是否真的有动画数据 + valid_anims = [] + for anim_name in anim_names: + try: + control = actor.getAnimControl(anim_name) + if control and control.getNumFrames() > 1: + valid_anims.append(anim_name) + print(f"[FBX修复] 有效动画: {anim_name} ({control.getNumFrames()} 帧)") + else: + print(f"[FBX修复] 无效动画: {anim_name} (帧数: {control.getNumFrames() if control else 0})") + except: + print(f"[FBX修复] 无法获取动画控制: {anim_name}") + + if valid_anims: + print(f"[FBX修复] 找到 {len(valid_anims)} 个有效动画") + return actor + else: + print(f"[FBX修复] 没有找到有效动画,尝试重新解析") + + # 尝试重新加载和分析 FBX 文件结构 + return self._deepAnalyzeFBX(fbx_path) + + except Exception as e: + print(f"[FBX修复] 修复失败: {e}") + return None + + def _deepAnalyzeFBX(self, fbx_path): + """深度分析 FBX 文件并尝试提取动画""" + try: + print(f"[FBX深度分析] 分析文件: {fbx_path}") + + # 尝试直接加载为模型,然后手动查找动画节点 + from panda3d.core import Loader + + loader = Loader.getGlobalPtr() + model = loader.loadSync(fbx_path) + + if model: + print(f"[FBX深度分析] 成功加载模型") + + # 查找动画相关节点 + anim_nodes = model.findAllMatches("**/+AnimBundleNode") + char_nodes = model.findAllMatches("**/+CharacterNode") + + print(f"[FBX深度分析] AnimBundleNode: {anim_nodes.getNumPaths()}") + print(f"[FBX深度分析] CharacterNode: {char_nodes.getNumPaths()}") + + if not char_nodes.isEmpty(): + # 尝试基于 CharacterNode 创建 Actor + char_node = char_nodes.getPath(0) + character = char_node.node().getCharacter() + + if character: + # 创建新的 Actor 实例并绑定角色 + actor = Actor() + actor.instance(model, "character") + + # 检查是否有动画 + if actor.getAnimNames(): + print(f"[FBX深度分析] 成功提取动画: {actor.getAnimNames()}") + return actor + + return None + + except Exception as e: + print(f"[FBX深度分析] 分析失败: {e}") + return None + + def _convertFBXManually(self, origin_model): + """手动转换 FBX 动画""" + from PyQt5.QtWidgets import QMessageBox, QProgressDialog + from PyQt5.QtCore import QTimer + + try: + filepath = origin_model.getTag("model_path") + if not filepath or not filepath.lower().endswith('.fbx'): + return + + # 显示进度对话框 + progress = QProgressDialog("正在转换FBX动画...", "取消", 0, 100) + progress.setWindowTitle("FBX动画转换") + progress.show() + + print(f"[手动转换] 开始转换: {filepath}") + + # 尝试使用系统转换工具 + converted_path = self._systemConvertFBX(filepath, progress) + + if converted_path: + # 重新加载转换后的模型 + progress.setLabelText("重新加载模型...") + progress.setValue(80) + + # 清除缓存 + if origin_model in self._actor_cache: + del self._actor_cache[origin_model] + + # 更新模型路径标签 + origin_model.setTag("model_path", converted_path) + + progress.setValue(100) + progress.hide() + + # 显示成功消息 + QMessageBox.information(None, "转换成功", + f"FBX动画转换成功!\n请重新选择模型查看动画。") + + print(f"[手动转换] 转换完成: {converted_path}") + + else: + progress.hide() + + # 显示转换选项 + msg = QMessageBox() + msg.setWindowTitle("转换建议") + msg.setText("自动转换失败,建议使用以下方法:") + msg.setDetailedText(""" +1. 使用 Blender 转换: + - 打开 Blender + - 导入 FBX 文件 + - 导出为 glTF (.gltf) 格式,确保选择"包含动画" + +2. 使用命令行工具: + - gltf2bam your_file.gltf your_file.bam + +3. 检查原始 FBX 文件: + - 确保 FBX 文件确实包含动画数据 + - 尝试在其他软件中验证动画 + """) + msg.exec_() + + except Exception as e: + print(f"[手动转换] 转换失败: {e}") + QMessageBox.warning(None, "转换失败", f"转换过程中出现错误: {e}") + + def _systemConvertFBX(self, fbx_path, progress=None): + """使用系统工具转换 FBX""" + import os + import subprocess + import tempfile + + try: + # 准备输出路径 + base_name = os.path.splitext(os.path.basename(fbx_path))[0] + output_dir = os.path.dirname(fbx_path) + gltf_path = os.path.join(output_dir, f"{base_name}_converted.gltf") + bam_path = os.path.join(output_dir, f"{base_name}_converted.bam") + + if progress: + progress.setValue(20) + progress.setLabelText("检查转换工具...") + + # 方法1: 使用 gltf2bam 的逆向功能(如果支持) + try: + # 首先尝试看看是否有直接的 FBX 支持 + result = subprocess.run(['gltf2bam', '--help'], + capture_output=True, text=True, timeout=10) + print(f"[系统转换] gltf2bam 可用") + except: + print(f"[系统转换] gltf2bam 不可用") + + if progress: + progress.setValue(40) + progress.setLabelText("尝试 Blender 转换...") + + # 方法2: 使用 Blender 无头模式转换 + try: + # 创建 Blender 转换脚本 + script_content = f''' +import bpy +import sys +import os - def _initActorModel(self,model,originmodel): - model.setPos(originmodel.getPos()) - model.setScale(originmodel.getScale()) - model.setHpr(originmodel.getHpr()) +# 清理默认场景 +bpy.ops.object.select_all(action='SELECT') +bpy.ops.object.delete(use_global=False) +# 导入 FBX +try: + bpy.ops.import_scene.fbx(filepath="{fbx_path}") + print("FBX导入成功") + + # 导出为 glTF + bpy.ops.export_scene.gltf( + filepath="{gltf_path}", + export_animations=True, + export_force_sampling=True, + export_frame_range=True + ) + print("glTF导出成功") + +except Exception as e: + print(f"转换失败: {{e}}") + sys.exit(1) +''' + + temp_script = tempfile.mktemp(suffix='.py') + with open(temp_script, 'w') as f: + f.write(script_content) + + if progress: + progress.setValue(60) + progress.setLabelText("执行 Blender 转换...") + + # 执行 Blender 转换 + result = subprocess.run([ + 'blender', '--background', '--python', temp_script + ], capture_output=True, text=True, timeout=120) + + # 清理临时文件 + if os.path.exists(temp_script): + os.remove(temp_script) + + if result.returncode == 0 and os.path.exists(gltf_path): + if progress: + progress.setValue(80) + progress.setLabelText("转换为 BAM 格式...") + + # 转换 glTF 为 BAM + result2 = subprocess.run(['gltf2bam', gltf_path, bam_path], + capture_output=True, text=True, timeout=60) + + if result2.returncode == 0 and os.path.exists(bam_path): + print(f"[系统转换] 成功转换为: {bam_path}") + return bam_path + elif os.path.exists(gltf_path): + print(f"[系统转换] 成功转换为: {gltf_path}") + return gltf_path + + except subprocess.TimeoutExpired: + print(f"[系统转换] Blender 转换超时") + except FileNotFoundError: + print(f"[系统转换] Blender 未安装") + except Exception as e: + print(f"[系统转换] Blender 转换出错: {e}") + + return None + + except Exception as e: + print(f"[系统转换] 系统转换失败: {e}") + return None + + def _playAnimation(self,origin_model): + actor=self._getActor(origin_model) + if not actor: + return + actor.setPos(origin_model.getPos()) + actor.setHpr(origin_model.getHpr()) + actor.setScale(origin_model.getScale()) + origin_model.hide() + actor.show() + + if hasattr(self,'animation_combo'): + # 获取原始动画名称(存储在 userData 中) + current_index = self.animation_combo.currentIndex() + if current_index >= 0: + original_name = self.animation_combo.itemData(current_index) + display_name = self.animation_combo.currentText() + if original_name: + actor.play(original_name) + print(f"『动画播放』:{display_name} (原始名称: {original_name})") + else: + # 兜底:使用显示名称 + actor.play(display_name) + print(f"『动画播放』:{display_name}") + + + def _pauseAnimation(self,origin_model): + actor = self._getActor(origin_model) + if not actor: + return + actor.setPos(origin_model.getPos()) + actor.setHpr(origin_model.getHpr()) + actor.setScale(origin_model.getScale()) + + origin_model.hide() + actor.show() + actor.stop() + print("『动画』暂停") + + def _stopAnimation(self,origin_model): + actor = self._getActor(origin_model) + if not actor: + return + actor.stop() + + # 获取原始动画名称 + current_index = self.animation_combo.currentIndex() + if current_index >= 0: + original_name = self.animation_combo.itemData(current_index) + display_name = self.animation_combo.currentText() + anim_name = original_name if original_name else display_name + + if anim_name and actor.getAnimControl(anim_name): + actor.getAnimControl(anim_name).pose(0) + actor.hide() + origin_model.show() + print("『动画』停止切换至原始模型") + + + def _loopAnimation(self,origin_model): + actor = self._getActor(origin_model) + if not actor: + return + + actor.setPos(origin_model.getPos()) + actor.setHpr(origin_model.getHpr()) + actor.setScale(origin_model.getScale()) + + origin_model.hide() + actor.show() + + # 获取原始动画名称 + current_index = self.animation_combo.currentIndex() + if current_index >= 0: + original_name = self.animation_combo.itemData(current_index) + display_name = self.animation_combo.currentText() + anim_name = original_name if original_name else display_name + + if anim_name: + actor.loop(anim_name) + print(f"[动画] 循环: {display_name} (原始名称: {anim_name})") + + def _setAnimationSpeed(self, origin_model, speed): + """ + 设置当前动画的播放倍速。 + """ + actor = self._getActor(origin_model) + if not actor: + return + + # 获取原始动画名称 + current_index = self.animation_combo.currentIndex() + + if current_index >= 0: + original_name = self.animation_combo.itemData(current_index) + display_name = self.animation_combo.currentText() + anim_name = original_name if original_name else display_name + + if anim_name: + actor.setPlayRate(speed, anim_name) + print(f"[动画] 速度设为: {speed} ({display_name})") + + def _detectNonSkeletalAnimations(self, origin_model): + """检测模型中的非骨骼动画""" + animations = {} + + try: + print(f"[调试] 开始检测非骨骼动画: {origin_model}") + + # 1. 精确检测 AnimBundle (非骨骼动画) + bundle_nodes = origin_model.findAllMatches("**/+AnimBundleNode") + print(f"[调试] 找到 AnimBundleNode 数量: {bundle_nodes.getNumPaths()}") + + if not bundle_nodes.isEmpty(): + for i, bundle_node in enumerate(bundle_nodes): + try: + bundle = bundle_node.node().getBundle() + print(f"[调试] AnimBundle #{i}: {bundle}") + + if bundle and hasattr(bundle, 'getNumFrames'): + num_frames = bundle.getNumFrames() + print(f"[调试] Bundle 帧数: {num_frames}") + + # 只有真正有多帧的才认为是动画 + if num_frames > 1: + anim_names = [] + + # 尝试获取动画名称 + try: + if hasattr(bundle, 'getName') and bundle.getName(): + anim_names.append(bundle.getName()) + else: + anim_names.append(f"Animation_{i}") + print(f"[调试] 检测到有效帧动画: {anim_names}, {num_frames} 帧") + except Exception as name_error: + anim_names.append(f"Animation_{i}") + print(f"[调试] 使用默认动画名: Animation_{i}") + + animations['transform'] = { + 'bundle': bundle, + 'names': anim_names, + 'node': bundle_node, + 'frames': num_frames + } + else: + print(f"[调试] Bundle 只有 {num_frames} 帧,不认为是动画") + + except Exception as bundle_error: + print(f"[调试] 处理 bundle 失败: {bundle_error}") + + # 2. 仅对 GLB 文件进行特殊检测 + filepath = origin_model.getTag("model_path") + if filepath and filepath.lower().endswith('.glb') and not animations: + print(f"[调试] GLB 文件特殊检测: {filepath}") + + # 检查是否有任何动画相关的节点名称 + all_nodes = origin_model.findAllMatches("**") + anim_indicators = [] + + for node in all_nodes: + node_name = node.getName().lower() + # 查找典型的动画节点命名模式 + if any(keyword in node_name for keyword in ['anim', 'key', 'frame', 'action', 'timeline']): + anim_indicators.append(node.getName()) + print(f"[调试] 发现可能的动画节点: {node.getName()}") + + # 只有在明确找到动画指示器时才创建动画条目 + if anim_indicators: + animations['glb_keyframe'] = { + 'bundle': None, + 'names': ['GLB_Animation'], + 'node': origin_model, + 'type': 'glb_manual', + 'indicators': anim_indicators + } + print(f"[调试] 创建 GLB 动画条目,基于指示器: {anim_indicators}") + + except Exception as e: + print(f"检测非骨骼动画失败: {e}") + + print(f"[调试] 最终检测结果: {animations}") + return animations if animations else None + + def _validateNonSkeletalAnimations(self, origin_model, animations): + """验证检测到的非骨骼动画是否真的可以播放""" + try: + print(f"[验证] 开始验证非骨骼动画: {list(animations.keys())}") + + for anim_type, anim_data in animations.items(): + if anim_type == 'transform': + # 验证变换动画 + bundle = anim_data.get('bundle') + if bundle: + # 检查是否真的有可播放的动画数据 + if hasattr(bundle, 'getNumFrames'): + frames = bundle.getNumFrames() + if frames <= 1: + print(f"[验证] 变换动画帧数不足: {frames}") + return False + + # 检查是否有有效的动画通道 + try: + if hasattr(bundle, 'getNumChannels'): + channels = bundle.getNumChannels() + if channels == 0: + print(f"[验证] 无有效动画通道") + return False + except: + pass + + elif anim_type == 'glb_keyframe': + # 验证 GLB 动画指示器 + indicators = anim_data.get('indicators', []) + if not indicators: + print(f"[验证] GLB 动画无有效指示器") + return False + + print(f"[验证] 动画验证通过") + return True + + except Exception as e: + print(f"[验证] 动画验证失败: {e}") + return False + + def _buildNonSkeletalUI(self, origin_model, animations): + """构建非骨骼动画控制UI""" + from PyQt5.QtWidgets import QLabel, QComboBox, QHBoxLayout, QWidget, QPushButton, QDoubleSpinBox, QTabWidget + + # 标题 + non_skeletal_title = QLabel("非骨骼动画控制") + non_skeletal_title.setStyleSheet("color:#FF6B6B;font-weight:bold;font-size:14px;margin-top:10px;") + self._propertyLayout.addRow(non_skeletal_title) + + # 如果有多种类型的动画,使用标签页 + if len(animations) > 1: + tab_widget = QTabWidget() + + for anim_type, anim_data in animations.items(): + tab = QWidget() + tab_layout = QVBoxLayout(tab) + self._buildAnimationTypeUI(tab_layout, origin_model, anim_type, anim_data) + tab_widget.addTab(tab, self._getAnimTypeDisplayName(anim_type)) + + self._propertyLayout.addRow("动画类型:", tab_widget) + else: + # 只有一种类型,直接显示 + anim_type, anim_data = next(iter(animations.items())) + self._buildAnimationTypeUI(self._propertyLayout, origin_model, anim_type, anim_data) + + # 存储动画信息供控制使用 + if not hasattr(self, '_non_skeletal_cache'): + self._non_skeletal_cache = {} + self._non_skeletal_cache[origin_model] = animations + + def _buildAnimationTypeUI(self, layout, origin_model, anim_type, anim_data): + """为特定动画类型构建UI""" + from PyQt5.QtWidgets import QLabel, QComboBox, QHBoxLayout, QWidget, QPushButton, QDoubleSpinBox + + if anim_type == 'transform': + # 变换动画控制 + self.ns_transform_combo = QComboBox() + self.ns_transform_combo.addItems(anim_data['names']) + layout.addRow("变换动画:", self.ns_transform_combo) + + elif anim_type == 'texture': + # 纹理动画控制 + self.ns_texture_combo = QComboBox() + self.ns_texture_combo.addItems(anim_data['stages']) + layout.addRow("纹理动画:", self.ns_texture_combo) + + elif anim_type == 'material': + # 材质动画控制 + self.ns_material_combo = QComboBox() + self.ns_material_combo.addItems(anim_data['properties']) + layout.addRow("材质动画:", self.ns_material_combo) + + elif anim_type == 'lerp': + # Lerp动画控制 + self.ns_lerp_combo = QComboBox() + self.ns_lerp_combo.addItems(anim_data['intervals']) + layout.addRow("Lerp动画:", self.ns_lerp_combo) + + # 通用控制按钮 + btn_box = QWidget() + btn_lay = QHBoxLayout(btn_box) + for txt, cmd in (("播放", "play"), ("暂停", "pause"), ("停止", "stop"), ("循环", "loop")): + btn = QPushButton(txt) + btn.clicked.connect(lambda _, c=cmd, t=anim_type: self._controlNonSkeletalAnimation(origin_model, t, c)) + btn_lay.addWidget(btn) + layout.addRow("控制:", btn_box) + + # 播放速度 + speed_spinbox = QDoubleSpinBox() + speed_spinbox.setRange(0.1, 5.0) + speed_spinbox.setSingleStep(0.1) + speed_spinbox.setValue(1.0) + speed_spinbox.valueChanged.connect(lambda v, t=anim_type: self._setNonSkeletalAnimationSpeed(origin_model, t, v)) + layout.addRow("播放速度:", speed_spinbox) + + def _getAnimTypeDisplayName(self, anim_type): + """获取动画类型的显示名称""" + names = { + 'transform': '变换动画', + 'glb_keyframe': 'GLB关键帧动画', + 'texture': '纹理动画', + 'material': '材质动画', + 'lerp': 'Lerp动画' + } + return names.get(anim_type, anim_type) + + def _controlNonSkeletalAnimation(self, origin_model, anim_type, command): + """控制非骨骼动画播放""" + try: + if not hasattr(self, '_non_skeletal_cache') or origin_model not in self._non_skeletal_cache: + return + + animations = self._non_skeletal_cache[origin_model] + if anim_type not in animations: + return + + anim_data = animations[anim_type] + + if anim_type == 'transform': + self._controlTransformAnimation(origin_model, anim_data, command) + elif anim_type == 'glb_keyframe': + self._controlGLBKeyframeAnimation(origin_model, anim_data, command) + elif anim_type == 'texture': + self._controlTextureAnimation(origin_model, anim_data, command) + elif anim_type == 'material': + self._controlMaterialAnimation(origin_model, anim_data, command) + elif anim_type == 'lerp': + self._controlLerpAnimation(origin_model, anim_data, command) + + print(f"[非骨骼动画] {anim_type} {command}") + + except Exception as e: + print(f"控制非骨骼动画失败: {e}") + + def _controlTransformAnimation(self, origin_model, anim_data, command): + """控制变换动画""" + try: + bundle = anim_data['bundle'] + bundle_node = anim_data['node'] + + print(f"[调试] 控制变换动画: {command}, Bundle: {bundle}") + + if command == 'play': + # 方法1: 通过 AnimBundle 直接播放 + if hasattr(bundle, 'play'): + bundle.play() + print(f"[动画] 通过 bundle.play() 播放") + # 方法2: 通过 AnimControl 播放 + elif hasattr(bundle_node.node(), 'getAnimControl'): + controls = bundle_node.node().getAnimControls() + if controls: + controls[0].play() + print(f"[动画] 通过 AnimControl 播放") + # 方法3: 通过启用动画节点 + else: + bundle_node.node().setPlayRate(1.0) + print(f"[动画] 设置播放速率为 1.0") + + elif command == 'pause': + if hasattr(bundle, 'pause'): + bundle.pause() + elif hasattr(bundle_node.node(), 'getAnimControl'): + controls = bundle_node.node().getAnimControls() + if controls: + controls[0].pause() + else: + bundle_node.node().setPlayRate(0.0) + + elif command == 'stop': + if hasattr(bundle, 'stop'): + bundle.stop() + elif hasattr(bundle_node.node(), 'getAnimControl'): + controls = bundle_node.node().getAnimControls() + if controls: + controls[0].stop() + else: + bundle_node.node().setPlayRate(0.0) + # 重置到第一帧 + if hasattr(bundle, 'setFrame'): + bundle.setFrame(0) + + elif command == 'loop': + if hasattr(bundle, 'loop'): + bundle.loop() + elif hasattr(bundle_node.node(), 'getAnimControl'): + controls = bundle_node.node().getAnimControls() + if controls: + controls[0].loop(True) + controls[0].play() + else: + bundle_node.node().setPlayRate(1.0) + + except Exception as e: + print(f"[错误] 控制变换动画失败: {e}") + import traceback + traceback.print_exc() + + def _controlGLBKeyframeAnimation(self, origin_model, anim_data, command): + """控制 GLB 关键帧动画""" + try: + print(f"[调试] 控制 GLB 动画: {command}") + + # 尝试通过 AnimControlCollection 控制 + from panda3d.core import AnimControlCollection + + # 方法1: 查找 AnimControlCollection + anim_collection = AnimControlCollection() + origin_model.getAnimControls(anim_collection) + + if anim_collection.getNumAnims() > 0: + for i in range(anim_collection.getNumAnims()): + anim_control = anim_collection.getAnim(i) + print(f"[调试] 找到动画控制: {anim_control.getName()}") + + if command == 'play': + anim_control.setPlayRate(1.0) + print(f"[GLB动画] 播放: {anim_control.getName()}") + elif command == 'pause': + anim_control.setPlayRate(0.0) + print(f"[GLB动画] 暂停: {anim_control.getName()}") + elif command == 'stop': + anim_control.setPlayRate(0.0) + anim_control.setFrame(0) + print(f"[GLB动画] 停止: {anim_control.getName()}") + elif command == 'loop': + anim_control.setPlayRate(1.0) + anim_control.loop(True) + print(f"[GLB动画] 循环: {anim_control.getName()}") + return + + # 方法2: 通过自动播放任务 + if command == 'play': + origin_model.setPlayRate(1.0) + print(f"[GLB动画] 设置播放速率为 1.0") + elif command == 'pause': + origin_model.setPlayRate(0.0) + print(f"[GLB动画] 暂停播放") + elif command == 'stop': + origin_model.setPlayRate(0.0) + print(f"[GLB动画] 停止播放") + elif command == 'loop': + origin_model.setPlayRate(1.0) + print(f"[GLB动画] 循环播放") + + except Exception as e: + print(f"[错误] 控制 GLB 动画失败: {e}") + + def _controlTextureAnimation(self, origin_model, anim_data, command): + """控制纹理动画""" + # 纹理动画通常通过 LerpInterval 实现 + print(f"[纹理动画] {command} - 功能待实现") + + def _controlMaterialAnimation(self, origin_model, anim_data, command): + """控制材质动画""" + # 材质动画通常通过修改材质属性实现 + print(f"[材质动画] {command} - 功能待实现") + + def _controlLerpAnimation(self, origin_model, anim_data, command): + """控制Lerp动画""" + # 通过 LerpInterval 控制 + print(f"[Lerp动画] {command} - 功能待实现") + + def _setNonSkeletalAnimationSpeed(self, origin_model, anim_type, speed): + """设置非骨骼动画播放速度""" + try: + if not hasattr(self, '_non_skeletal_cache') or origin_model not in self._non_skeletal_cache: + return + + animations = self._non_skeletal_cache[origin_model] + if anim_type not in animations: + return + + anim_data = animations[anim_type] + + if anim_type == 'transform': + bundle = anim_data['bundle'] + if hasattr(bundle, 'setPlayRate'): + bundle.setPlayRate(speed) + + print(f"[非骨骼动画] {anim_type} 速度设为: {speed}") + + except Exception as e: + print(f"设置非骨骼动画速度失败: {e}") + + def _detectPlayer(self,origin_model): + filepath = origin_model.getTag("model_path") + if filepath: + try: + actor = Actor(filepath) + if actor.getAnimNames(): + actor.cleanup();actor.removeNode() + return ("actor",None) + except: + pass + bundle_np = origin_model.find("**/+AnimBundleNode") + if not bundle_np.isEmpty(): + ctrl = bundle_np.node().getBundle().bind(origin_model.node(),PartGroup.PART_Whole) + return ("bundle",ctrl) + return None + + def _buildBundleUI(self,origin_model,ctrl): + from PyQt5.QtWidgets import QLabel,QPushButton,QHBoxLayout,QWidget,QSlider + + title = QLabel("骨骼动画控制") + title.setStyleSheet("color:#FF8C00;font-weight:bold;font-size:14px;margin-top:10px;") + self._propertyLayout.addRow(title) + + btn_box = QWidget() + lay = QHBoxLayout(btn_box) + for txt , fn in (("播放",ctrl.play), + ("暂停",ctrl.stop), + ("重置",lambda:ctrl.pose(0))): + btn = QPushButton(txt) + btn.clicked.connect(fn) + lay.addWidget(btn) + self._propertyLayout.addRow("控制:",btn_box) + + slider = QSlider() + slider.setOrientation(1) + slider.setRange(0,int(ctrl.getNumFrames())) + slider.valueChanged.connect(ctrl.pose) + self._propertyLayout.addRow("帧:",slider) + + self._actor_cache[origin_model] = ("bundle",ctrl) + + def _dispatchAnimCommand(self,origin_model,cmd): + cache = self._actor_cache.get(origin_model) + if not cache: + return + kind,player = cache + + if kind == "actor": + actor=player + anim_name = self.animation_combo.currentText() + actor.setPos(origin_model.getPos()) + actor.setHpr(origin_model.getHpr()) + actor.setScale(origin_model.getScale()) + + if cmd == "play": + origin_model.hide() + actor.show() + actor.play(anim_name) + elif cmd == "pause": + origin_model.hide() + actor.show() + actor.stop() + elif cmd == "stop": + actor.stop() + if anim_name and actor.getAnimControl(anim_name): + actor.getAnimControl(anim_name).pose(0) + actor.hide();origin_model.show() + elif cmd == "loop": + origin_model.hide() + actor.show() + actor.loop(anim_name) + elif isinstance(cmd,tuple) and cmd[0] == "speed": + actor.setPlayRate(cmd[1], anim_name) + + elif kind == "bundle": + ctrl = player + if cmd == "play": + ctrl.play() + elif cmd == "pause": + ctrl.stop() + elif cmd == "stop": + ctrl.stop() + ctrl.pose(0) + elif cmd == "loop": + ctrl.loop(True) + elif isinstance(cmd,tuple) and cmd[0] =="speed": + ctrl.setPlayRate(cmd[1]) + + def removeActorForModel(self, model): + """删除 model 对应的 Actor(如果存在)""" + actor = self._actor_cache.pop(model, None) + if actor: + actor.stop() + actor.cleanup() + actor.removeNode() \ No newline at end of file diff --git a/ui/widgets.py b/ui/widgets.py index 70347ec1..61251287 100644 --- a/ui/widgets.py +++ b/ui/widgets.py @@ -150,7 +150,7 @@ class CustomPanda3DWidget(QPanda3DWidget): filepath = url.toLocalFile() if filepath.lower().endswith(('.egg', '.bam', '.obj', '.fbx', '.gltf', '.glb')): self.world.importModel(filepath) - self.world.addAnimationPanel(None,filepath) + #self.world.addAnimationPanel(None,filepath) event.acceptProposedAction() else: event.ignore()