节点显示隐藏逻辑补全,删除无用非骨骼动画逻辑

This commit is contained in:
Hector 2025-08-18 16:52:49 +08:00
parent ad59e573be
commit f81c4fd4cc
3 changed files with 91 additions and 455 deletions

View File

@ -630,7 +630,7 @@ class SceneManager:
cNode.addSolid(cSphere)
# 将碰撞节点附加到模型上
cNodePath = model.attachNewNode(cNode)
#cNodePath = model.attachNewNode(cNode)
# cNodePath.show() # 取消注释可以显示碰撞体,用于调试
# ==================== 场景树管理 ====================

View File

@ -22,6 +22,7 @@ class InterfaceManager:
# 更新场景树
self.world.scene_manager.updateSceneTree()
#self.syncVisibilityDownward()
def onTreeItemClicked(self, item, column):
"""处理树形控件项目点击事件"""
@ -239,4 +240,35 @@ class InterfaceManager:
item.setExpanded(True)
for i in range(item.childCount()):
self._restore_expanded(item.child(i),path)
self._restore_expanded(item.child(i),path)
def syncVisibilityDownward(self):
from collections import deque
if not self.world.models:
return
q = deque()
for root in self.world.models:
q.append(root)
while q:
node = q.popleft()
visible = node.getPythonTag("visible")
if visible is None:
visible = True
if not visible:
stack = [node]
while stack:
cur = stack.pop()
cur.hide()
for child in cur.getChildren():
stack.append(child)
continue
node.show()
for child in node.getChildren():
q.append(child)

View File

@ -1,3 +1,4 @@
from collections import deque
from traceback import print_exc
from types import new_class
from typing import Hashable
@ -68,15 +69,34 @@ class PropertyPanelManager:
self._propertyLayout.addWidget(tipLabel)
return
model = item.data(0, Qt.UserRole)
user_visible = True
if model:
user_visible = model.getPythonTag("user_visible")
if user_visible is None:
user_visible = True
model.setPythonTag("user_visible", True)
self.name_group = QGroupBox("物体名称")
name_layout = QHBoxLayout()
self.active_check = QCheckBox()
self.active_check.setChecked(True) # 默认激活
# 根据模型的实际可见性状态设置复选框
self.active_check.setChecked(user_visible)
self.name_input = QLineEdit(itemText)
name_layout.addWidget(self.active_check)
name_layout.addWidget(self.name_input)
self.name_group.setLayout(name_layout)
self._propertyLayout.addWidget(self.name_group)
if model:
try:
self.active_check.stateChanged.disconnect()
except TypeError:
pass
self.active_check.stateChanged.connect(
lambda state,m = model:self._setUserVisible(m,state == Qt.Checked)
)
# nameLabel = QLabel("名称:")
# nameEdit = QLineEdit(itemText)
# self._propertyLayout.addRow(nameLabel, nameEdit)
@ -107,6 +127,39 @@ class PropertyPanelManager:
if propertyWidget:
propertyWidget.update()
def _setUserVisible(self,node,visible):
node.setPythonTag("user_visible",visible)
self._syncEffectiveVisibility(node)
def _syncEffectiveVisibility(self, start_node):
"""广度优先,确保父隐藏则子一定隐藏"""
q = deque([(start_node, True)]) # (node, parent_effective_visible)
while q:
node, parent_eff = q.popleft()
user = node.getPythonTag("user_visible")
if user is None:
user = True
eff = parent_eff and user
node.setPythonTag("effective_visible", eff)
if eff:
node.show()
else:
node.hide()
for child in node.getChildren():
q.append((child, eff))
def _toggleModelVisibility(self, model, state):
"""切换模型可见性状态"""
try:
# 用我们自己维护的可见性接口,而不是直接 show/hide
visible = (state == Qt.Checked)
self._setUserVisible(model, visible)
except Exception as e:
print(f"切换模型可见性失败: {str(e)}")
def refreshModelValues(self, nodePath):
"""实时刷新模型所有属性数值(相对/世界位置、旋转、缩放)"""
if not nodePath or self._propertyLayout is None:
@ -185,7 +238,7 @@ class PropertyPanelManager:
# 设置位置控件属性
for pos_widget in [self.pos_x, self.pos_y, self.pos_z]:
pos_widget.setRange(-1000, 1000)
pos_widget.setRange(-1000000.0, 1000000.0)
self.pos_x.setValue(relativePos.getX())
self.pos_y.setValue(relativePos.getY())
@ -234,7 +287,7 @@ class PropertyPanelManager:
# 设置世界位置控件属性
for world_pos_widget in [self.world_pos_x, self.world_pos_y, self.world_pos_z]:
world_pos_widget.setRange(-1000, 1000)
world_pos_widget.setRange(-1000000.0, 1000000.0)
world_pos_widget.setReadOnly(True)
self.world_pos_x.setValue(worldPos.getX())
@ -253,7 +306,7 @@ class PropertyPanelManager:
# 设置旋转控件属性
for rot_widget in [self.rot_h, self.rot_p, self.rot_r]:
rot_widget.setRange(-180, 180)
rot_widget.setRange(-360, 360)
self.rot_h.setValue(model.getH())
self.rot_p.setValue(model.getP())
@ -4182,14 +4235,6 @@ class PropertyPanelManager:
# 忽略 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, animation_layout)
has_animation = True
print(f"[信息] 检测到非骨骼动画: {list(non_skeletal_anims.keys())}")
# 如果都没有动画
if not has_animation:
no_anim_label = QLabel("此模型无动画")
@ -4861,434 +4906,7 @@ except Exception as e:
origin_model.setPythonTag("anim_speed",speed)
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, layout):
"""构建非骨骼动画控制UI"""
from PyQt5.QtWidgets import QLabel, QComboBox, QHBoxLayout, QWidget, QPushButton, QDoubleSpinBox, QTabWidget
# 动画信息
info_text = f"非骨骼动画数量: {len(animations)}"
info_label = QLabel(info_text)
info_label.setStyleSheet("color:#888;font-size:10px;")
layout.addWidget(QLabel("信息:"), 0, 0)
layout.addWidget(info_label, 0, 1, 1, 3)
# 如果有多种类型的动画,使用标签页
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)
@ -5323,20 +4941,6 @@ except Exception as e:
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)