Merge pull request 'addRender' (#15) from addRender into main
Reviewed-on: Hector/EG#15
This commit is contained in:
commit
86788ba88d
File diff suppressed because one or more lines are too long
BIN
core/TranslateArrowHandle.fbx
Executable file
BIN
core/TranslateArrowHandle.fbx
Executable file
Binary file not shown.
File diff suppressed because it is too large
Load Diff
6
main.py
6
main.py
@ -369,9 +369,6 @@ class MyWorld(CoreWorld):
|
||||
def updatePropertyPanel(self, item):
|
||||
"""更新属性面板显示 - 代理到property_panel"""
|
||||
return self.property_panel.updatePropertyPanel(item)
|
||||
|
||||
# def addAnimationPanel(self,originmodel,filepath):
|
||||
# return self.property_panel._addAnimationPanel(originmodel,filepath)
|
||||
|
||||
def updateGUIPropertyPanel(self, gui_element):
|
||||
"""更新GUI元素属性面板 - 代理到property_panel"""
|
||||
@ -380,6 +377,9 @@ class MyWorld(CoreWorld):
|
||||
def removeActorForModel(self,model):
|
||||
return self.property_panel.removeActorForModel( model)
|
||||
|
||||
def updateNodeVisibilityAfterDrag(self,item):
|
||||
return self.property_panel.updateNodeVisibilityAfterDrag(item)
|
||||
|
||||
# ==================== 工具管理代理 ====================
|
||||
|
||||
def setCurrentTool(self, tool):
|
||||
|
||||
@ -631,6 +631,7 @@ class SceneManager:
|
||||
|
||||
# 将碰撞节点附加到模型上
|
||||
cNodePath = model.attachNewNode(cNode)
|
||||
#cNodePath.hide()
|
||||
# cNodePath.show() # 取消注释可以显示碰撞体,用于调试
|
||||
|
||||
# ==================== 场景树管理 ====================
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
from PyQt5.QtWidgets import QTreeWidget, QTreeWidgetItem, QMenu
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.sip import delete
|
||||
from panda3d.core import GeomNode, ModelRoot
|
||||
|
||||
|
||||
@ -26,8 +27,11 @@ class InterfaceManager:
|
||||
def onTreeItemClicked(self, item, column):
|
||||
"""处理树形控件项目点击事件"""
|
||||
if not item:
|
||||
self.world.selection.updateSelection(None)
|
||||
return
|
||||
|
||||
self.world.property_panel.updatePropertyPanel(item)
|
||||
|
||||
# 获取节点对象
|
||||
nodePath = item.data(0, Qt.UserRole)
|
||||
if nodePath:
|
||||
@ -36,7 +40,7 @@ class InterfaceManager:
|
||||
self.world.selection.updateSelection(nodePath)
|
||||
|
||||
# 更新属性面板
|
||||
self.world.property_panel.updatePropertyPanel(item)
|
||||
#self.world.property_panel.updatePropertyPanel(item)
|
||||
|
||||
print(f"树形控件点击: {item.text(0)}")
|
||||
else:
|
||||
@ -77,6 +81,9 @@ class InterfaceManager:
|
||||
if self.isModelOrChild(item):
|
||||
deleteAction = menu.addAction("删除")
|
||||
deleteAction.triggered.connect(lambda: self.deleteNode(nodePath, item))
|
||||
else:
|
||||
deleteAction = menu.addAction("删除")
|
||||
deleteAction.triggered.connect(lambda: self.deleteNode(nodePath, item))
|
||||
|
||||
# 显示菜单
|
||||
menu.exec_(self.treeWidget.viewport().mapToGlobal(position))
|
||||
@ -94,12 +101,33 @@ class InterfaceManager:
|
||||
try:
|
||||
# 从场景中移除
|
||||
self.world.property_panel.removeActorForModel(nodePath)
|
||||
|
||||
if hasattr(nodePath,'getPythonTag'):
|
||||
light_object = nodePath.getPythonTag('rp_light_object')
|
||||
if light_object and hasattr(self.world,'render_pipeline'):
|
||||
self.world.render_pipeline.remove_light(light_object)
|
||||
|
||||
if hasattr(self.world,'selection'):
|
||||
if self.world.selection.selectedNode == nodePath:
|
||||
self.world.selection.clearSelectionBox()
|
||||
self.world.selection.clearGizmo()
|
||||
self.world.selection.selectedNode = None
|
||||
self.world.selection.selectedObject = None
|
||||
|
||||
if nodePath in self.world.Spotlight:
|
||||
self.world.Spotlight.remove(nodePath)
|
||||
if nodePath in self.world.Pointlight:
|
||||
self.world.Pointlight.remove(nodePath)
|
||||
|
||||
nodePath.removeNode()
|
||||
|
||||
if hasattr(self.world,'selection'):
|
||||
self.world.selection.checkAndClearIfTargetDeleted()
|
||||
|
||||
# 如果是模型根节点,从模型列表中移除
|
||||
if item.parent().text(0) == "模型":
|
||||
if nodePath in self.world.models:
|
||||
self.world.models.remove(nodePath)
|
||||
#if item.parent().text(0) == "模型":
|
||||
if nodePath in self.world.models:
|
||||
self.world.models.remove(nodePath)
|
||||
|
||||
# 从树形控件中移除
|
||||
parentItem = item.parent()
|
||||
@ -239,4 +267,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)
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
from collections import deque
|
||||
from traceback import print_exc
|
||||
from types import new_class
|
||||
from typing import Hashable
|
||||
@ -50,6 +51,16 @@ class PropertyPanelManager:
|
||||
if item.widget():
|
||||
item.widget().deleteLater()
|
||||
|
||||
def updateNodeVisibilityAfterDrag(self, item):
|
||||
"""拖拽结束后更新节点的可见性状态"""
|
||||
node = item.data(0, Qt.UserRole)
|
||||
if not node:
|
||||
return
|
||||
|
||||
# 当节点被拖拽后,需要根据新父节点的状态来更新可见性
|
||||
self._syncEffectiveVisibility(node)
|
||||
|
||||
|
||||
def updatePropertyPanel(self, item):
|
||||
"""更新属性面板显示"""
|
||||
if not self._propertyLayout or not self._propertyLayout.parent():
|
||||
@ -68,15 +79,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 +137,59 @@ class PropertyPanelManager:
|
||||
if propertyWidget:
|
||||
propertyWidget.update()
|
||||
|
||||
def _setUserVisible(self,node,visible):
|
||||
node.setPythonTag("user_visible",visible)
|
||||
self._syncEffectiveVisibility(node)
|
||||
|
||||
def _syncEffectiveVisibility(self, start_node):
|
||||
"""广度优先,确保父隐藏则子一定隐藏"""
|
||||
# 获取起始节点的父节点
|
||||
parent_node = start_node.getParent()
|
||||
|
||||
# 确定父节点的有效可见性
|
||||
parent_effective_visible = True
|
||||
if parent_node:
|
||||
parent_effective_visible = parent_node.getPythonTag("effective_visible")
|
||||
if parent_effective_visible is None:
|
||||
parent_effective_visible = True
|
||||
|
||||
q = deque([(start_node, parent_effective_visible)]) # (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 node.getName().startswith("modelCollision_"):
|
||||
node.hide()
|
||||
else:
|
||||
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)
|
||||
|
||||
collision_nodes = model.findAllMatches("**/modelCollision_*")
|
||||
for collision_node in collision_nodes:
|
||||
collision_node.hide()
|
||||
|
||||
except Exception as e:
|
||||
print(f"切换模型可见性失败: {str(e)}")
|
||||
|
||||
def refreshModelValues(self, nodePath):
|
||||
"""实时刷新模型所有属性数值(相对/世界位置、旋转、缩放)"""
|
||||
if not nodePath or self._propertyLayout is None:
|
||||
@ -185,16 +268,31 @@ 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())
|
||||
self.pos_z.setValue(relativePos.getZ())
|
||||
|
||||
# 连接位置变化事件
|
||||
self.pos_x.valueChanged.connect(lambda v: model.setX(parent, v) if parent else model.setX(v))
|
||||
self.pos_y.valueChanged.connect(lambda v: model.setY(parent, v) if parent else model.setY(v))
|
||||
self.pos_z.valueChanged.connect(lambda v: model.setZ(parent, v) if parent else model.setZ(v))
|
||||
# self.pos_x.valueChanged.connect(lambda v: model.setX(parent, v) if parent else model.setX(v))
|
||||
# self.pos_y.valueChanged.connect(lambda v: model.setY(parent, v) if parent else model.setY(v))
|
||||
# self.pos_z.valueChanged.connect(lambda v: model.setZ(parent, v) if parent else model.setZ(v))
|
||||
|
||||
def updateXPosition(value):
|
||||
model.setX(value)
|
||||
self.refreshModelValues(model)
|
||||
self.pos_x.valueChanged.connect(updateXPosition)
|
||||
|
||||
def updateYPosition(value):
|
||||
model.setY(value)
|
||||
self.refreshModelValues(model)
|
||||
self.pos_y.valueChanged.connect(updateYPosition)
|
||||
|
||||
def updateZPosition(value):
|
||||
model.setZ(value)
|
||||
self.refreshModelValues(model)
|
||||
self.pos_z.valueChanged.connect(updateZPosition)
|
||||
|
||||
# 创建并设置 X, Y, Z 标签居中
|
||||
x_label1 = QLabel("X")
|
||||
@ -219,7 +317,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())
|
||||
@ -238,7 +336,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())
|
||||
@ -4167,14 +4265,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("此模型无动画")
|
||||
@ -4846,434 +4936,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)
|
||||
@ -5308,20 +4971,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)
|
||||
|
||||
@ -339,6 +339,7 @@ class CustomTreeWidget(QTreeWidget):
|
||||
|
||||
# 获取节点引用
|
||||
dragged_node = dragged_item.data(0, Qt.UserRole)
|
||||
amtarget_node = target_item.data(0, Qt.UserRole)
|
||||
|
||||
# 如果目标是模型根节点,使用 render 作为新父节点
|
||||
if target_item.text(0) == "模型":
|
||||
@ -360,12 +361,17 @@ class CustomTreeWidget(QTreeWidget):
|
||||
|
||||
# 接受拖放事件,更新树形控件
|
||||
super().dropEvent(event)
|
||||
|
||||
|
||||
#self.world.property_panel.updateNodeVisibilityAfterDrag(dragged_item)
|
||||
# 更新属性面板
|
||||
self.world.updatePropertyPanel(dragged_item)
|
||||
self.world.property_panel._syncEffectiveVisibility(dragged_node)
|
||||
|
||||
|
||||
|
||||
else:
|
||||
event.ignore()
|
||||
|
||||
|
||||
def isValidParentChild(self, dragged_item, target_item):
|
||||
"""检查是否是有效的父子关系"""
|
||||
# 不能拖放到自己上
|
||||
|
||||
Loading…
Reference in New Issue
Block a user