灯光移动问题修复,模型动画可以正常播放

This commit is contained in:
Hector 2026-02-26 11:52:42 +08:00
parent 69d83c6ab5
commit 405e8a9ad3
8 changed files with 1739 additions and 270 deletions

View File

@ -373,6 +373,7 @@ class TransformGizmo(DirectObject):
old_scale = action.get("old_scale") old_scale = action.get("old_scale")
if old_scale is not None: if old_scale is not None:
node.setScale(old_scale) node.setScale(old_scale)
self._sync_light_position_if_needed(node)
self._redo_history.append(action) self._redo_history.append(action)
return True return True
@ -402,6 +403,7 @@ class TransformGizmo(DirectObject):
if new_scale is not None: if new_scale is not None:
node.setScale(new_scale) node.setScale(new_scale)
self._sync_light_position_if_needed(node)
self._history.append(action) self._history.append(action)
return True return True
@ -464,6 +466,28 @@ class TransformGizmo(DirectObject):
# New user action invalidates redo chain. # New user action invalidates redo chain.
self._redo_history.clear() self._redo_history.clear()
def _sync_light_position_if_needed(self, node: Optional[NodePath]) -> None:
"""When target node wraps an RP light, keep RP light position in sync."""
try:
if node is None or node.isEmpty() or (not node.hasPythonTag("rp_light_object")):
return
light_obj = node.getPythonTag("rp_light_object")
if not light_obj:
return
world_pos = node.getPos(self.world.render)
try:
light_obj.setPos(world_pos)
except Exception:
try:
light_obj.setPos(world_pos.x, world_pos.y, world_pos.z)
except Exception:
try:
light_obj.pos = world_pos
except Exception:
pass
except Exception:
pass
# ------------------------------------------------------------------ # # ------------------------------------------------------------------ #
# Input helpers (hotkeys / mouse states) # Input helpers (hotkeys / mouse states)
# ------------------------------------------------------------------ # # ------------------------------------------------------------------ #

View File

@ -616,24 +616,40 @@ class CompositeCommand(Command):
for command in self.commands: for command in self.commands:
command.redo() command.redo()
class MoveLightCommand(Command): class MoveLightCommand(Command):
def __init__(self, node, old_pos, new_pos, light_object=None): def __init__(self, node, old_pos, new_pos, light_object=None):
self.node = node self.node = node
self.old_pos = Point3(old_pos) self.old_pos = Point3(old_pos)
self.new_pos = Point3(new_pos) self.new_pos = Point3(new_pos)
self.light_object = light_object self.light_object = light_object
def execute(self): # 将原来的 do() 改为 execute() def _apply_light_position(self, pos):
if self.light_object: if not self.light_object:
self.light_object.pos = self.new_pos return
if self.node: try:
self.node.setPos(self.new_pos) self.light_object.setPos(pos)
return
def undo(self): except Exception:
if self.light_object: pass
self.light_object.pos = self.old_pos try:
if self.node: self.light_object.setPos(pos.x, pos.y, pos.z)
self.node.setPos(self.old_pos) return
except Exception:
pass
try:
self.light_object.pos = Point3(pos)
except Exception:
pass
def execute(self): # 将原来的 do() 改为 execute()
self._apply_light_position(self.new_pos)
if self.node:
self.node.setPos(self.new_pos)
def undo(self):
self._apply_light_position(self.old_pos)
if self.node:
self.node.setPos(self.old_pos)
def redo(self): def redo(self):
self.execute() # 调用 execute() 而不是 do() self.execute() # 调用 execute() 而不是 do()

View File

@ -122,6 +122,46 @@ class SelectionSystem:
tg = getattr(self.world, "newTransform", None) tg = getattr(self.world, "newTransform", None)
return tg is not None return tg is not None
def _sync_rp_light_position(self, light_node, light_object=None):
"""同步灯光包装节点与 RenderPipeline 灯光对象位置。"""
try:
if not light_node or light_node.isEmpty():
return False
if light_object is None:
light_object = light_node.getPythonTag("rp_light_object")
if not light_object and light_node.hasTag("light_type"):
# 兼容旧数据:节点存在 light_type 但未绑定 rp_light_object 时尝试重建绑定
scene_manager = getattr(self.world, "scene_manager", None)
if scene_manager:
try:
light_type = light_node.getTag("light_type")
if light_type == "spot_light" and hasattr(scene_manager, "_recreateSpotLight"):
scene_manager._recreateSpotLight(light_node)
elif light_type == "point_light" and hasattr(scene_manager, "_recreatePointLight"):
scene_manager._recreatePointLight(light_node)
light_object = light_node.getPythonTag("rp_light_object")
except Exception:
pass
if not light_object:
return False
world_pos = light_node.getPos(self.world.render)
# 优先使用 RP Light 的 setPos 接口
try:
light_object.setPos(world_pos)
except Exception:
try:
light_object.setPos(world_pos.x, world_pos.y, world_pos.z)
except Exception:
try:
light_object.pos = Point3(world_pos)
except Exception:
return False
return True
except Exception:
return False
def sync_transform_gizmo_mode(self): def sync_transform_gizmo_mode(self):
"""Sync TransformGizmo mode with current tool.""" """Sync TransformGizmo mode with current tool."""
if not self._has_new_transform_gizmo(): if not self._has_new_transform_gizmo():
@ -738,9 +778,9 @@ class SelectionSystem:
light_object = self.gizmoTarget.getPythonTag("rp_light_object") light_object = self.gizmoTarget.getPythonTag("rp_light_object")
if light_object: if light_object:
light_pos = light_object.pos # 以节点位置为真值并回写 RP 灯光,避免“手柄能动但灯光不动”
self.gizmo.setPos(light_object.pos) self._sync_rp_light_position(self.gizmoTarget, light_object)
self.gizmoTarget.setPos(light_pos) self.gizmo.setPos(self.gizmoTarget.getPos(self.world.render))
else: else:
# 只在必要时更新位置和朝向 # 只在必要时更新位置和朝向
self._updateGizmoPositionAndOrientation() self._updateGizmoPositionAndOrientation()
@ -1530,7 +1570,8 @@ class SelectionSystem:
light_object = self.gizmoTarget.getPythonTag("rp_light_object") light_object = self.gizmoTarget.getPythonTag("rp_light_object")
if light_object: if light_object:
self.gizmoTargetStartPos = Point3(light_object.pos) # 起始位置统一使用节点世界坐标,避免依赖 light_object.pos 的陈旧值
self.gizmoTargetStartPos = Point3(self.gizmoTarget.getPos(self.world.render))
else: else:
self.gizmoTargetStartPos = self.gizmoTarget.getPos() self.gizmoTargetStartPos = self.gizmoTarget.getPos()
@ -1818,8 +1859,8 @@ class SelectionSystem:
# 应用新位置到目标节点 # 应用新位置到目标节点
light_object = self.gizmoTarget.getPythonTag("rp_light_object") light_object = self.gizmoTarget.getPythonTag("rp_light_object")
if light_object: if light_object:
light_object.pos = newPos
self.gizmoTarget.setPos(newPos) self.gizmoTarget.setPos(newPos)
self._sync_rp_light_position(self.gizmoTarget, light_object)
print(f"🔄 光源拖拽移动: {currentPos} -> {newPos}") print(f"🔄 光源拖拽移动: {currentPos} -> {newPos}")
else: else:
self.gizmoTarget.setPos(newPos) self.gizmoTarget.setPos(newPos)

71
main.py
View File

@ -154,6 +154,7 @@ class MyWorld(CoreWorld):
# 新的坐标系 # 新的坐标系
self.newTransform = TransformGizmo(self) self.newTransform = TransformGizmo(self)
self._setup_transform_gizmo_light_sync()
# 初始化视频管理 # 初始化视频管理
if VideoManager is not None: if VideoManager is not None:
@ -816,6 +817,76 @@ class MyWorld(CoreWorld):
# 顶部工具栏 # 顶部工具栏
if self.showToolbar: if self.showToolbar:
self._draw_toolbar() self._draw_toolbar()
def _sync_rp_light_from_node(self, node):
"""将灯光包装节点的位置同步到 RenderPipeline 灯光对象。"""
try:
if not node or node.isEmpty() or (not node.hasPythonTag("rp_light_object")):
# 兼容旧场景:仅有 light_type 标签时尝试补绑 rp_light_object
if not node or node.isEmpty() or (not node.hasTag("light_type")):
return False
scene_manager = getattr(self, "scene_manager", None)
if scene_manager:
try:
light_type = node.getTag("light_type")
if light_type == "spot_light" and hasattr(scene_manager, "_recreateSpotLight"):
scene_manager._recreateSpotLight(node)
elif light_type == "point_light" and hasattr(scene_manager, "_recreatePointLight"):
scene_manager._recreatePointLight(node)
except Exception:
pass
light_obj = node.getPythonTag("rp_light_object") if node.hasPythonTag("rp_light_object") else None
if not light_obj:
return False
world_pos = node.getPos(self.render)
try:
light_obj.setPos(world_pos)
except Exception:
try:
light_obj.setPos(world_pos.x, world_pos.y, world_pos.z)
except Exception:
try:
light_obj.pos = Point3(world_pos)
except Exception:
return False
return True
except Exception:
return False
def _on_transform_gizmo_drag_event(self, payload):
"""TransformGizmo 拖拽事件回调:实时同步灯光位置。"""
try:
node = payload.get("target") if isinstance(payload, dict) else None
if node and (not node.isEmpty()):
self._sync_rp_light_from_node(node)
except Exception:
pass
def _setup_transform_gizmo_light_sync(self):
"""为 newTransform 注册灯光同步事件钩子。"""
tg = getattr(self, "newTransform", None)
if not tg:
return
try:
from TransformGizmo.events import GizmoEvent
hooks = {
"move": {
GizmoEvent.DRAG_MOVE: [self._on_transform_gizmo_drag_event],
GizmoEvent.DRAG_END: [self._on_transform_gizmo_drag_event],
},
"rotate": {
GizmoEvent.DRAG_MOVE: [self._on_transform_gizmo_drag_event],
GizmoEvent.DRAG_END: [self._on_transform_gizmo_drag_event],
},
"scale": {
GizmoEvent.DRAG_MOVE: [self._on_transform_gizmo_drag_event],
GizmoEvent.DRAG_END: [self._on_transform_gizmo_drag_event],
},
}
tg.set_event_hooks(hooks, replace=False)
except Exception as e:
print(f"绑定 TransformGizmo 灯光同步事件失败: {e}")
def _draw_menu_bar(self): def _draw_menu_bar(self):
self.editor_panels.draw_menu_bar() self.editor_panels.draw_menu_bar()

View File

@ -227,6 +227,12 @@ class SceneManager:
if normalize_scales: if normalize_scales:
model.setTag("scale_normalization_applied", "true") model.setTag("scale_normalization_applied", "true")
# 初始化动画标签,避免属性面板首次读取时误判“无动画”
try:
self._processModelAnimations(model)
except Exception as e:
print(f"初始化模型动画标签失败: {e}")
# 添加到模型列表 # 添加到模型列表
self.models.append(model) self.models.append(model)
@ -2086,10 +2092,15 @@ class SceneManager:
def _processModelAnimations(self, model_node): def _processModelAnimations(self, model_node):
"""处理模型动画,确保在场景加载时正确识别动画信息""" """处理模型动画,确保在场景加载时正确识别动画信息"""
try: try:
# 检查模型是否已经有动画信息标签 # 已检测过则直接复用,避免重复开销
if model_node.hasTag("has_animations_checked"):
return model_node.hasTag("has_animations") and model_node.getTag("has_animations").lower() == "true"
# 检查模型是否已经有动画信息标签(兼容旧数据)
if model_node.hasTag("has_animations"): if model_node.hasTag("has_animations"):
has_animations = model_node.getTag("has_animations").lower() == "true" has_animations = model_node.getTag("has_animations").lower() == "true"
if has_animations: if has_animations:
model_node.setTag("has_animations_checked", "true")
print(f"模型 {model_node.getName()} 已有动画信息") print(f"模型 {model_node.getName()} 已有动画信息")
return True return True
@ -2099,6 +2110,46 @@ class SceneManager:
has_animations = (character_nodes.getNumPaths() > 0 or has_animations = (character_nodes.getNumPaths() > 0 or
anim_bundle_nodes.getNumPaths() > 0) anim_bundle_nodes.getNumPaths() > 0)
# 如果模型树中没检测到,再尝试通过 Actor 从文件路径检测
if not has_animations:
model_path = model_node.getTag("model_path") if model_node.hasTag("model_path") else ""
if model_path:
try:
from direct.actor.Actor import Actor
from panda3d.core import Filename
candidate_paths = [model_path]
candidate_paths.append(Filename.from_os_specific(model_path).get_fullpath())
try:
normalized = util.normalize_model_path(model_path)
if normalized:
candidate_paths.append(normalized)
except Exception:
pass
seen = set()
unique_paths = []
for p in candidate_paths:
if not p or p in seen:
continue
seen.add(p)
unique_paths.append(p)
for candidate in unique_paths:
try:
actor = Actor(candidate)
anim_names = actor.getAnimNames()
actor.cleanup()
actor.removeNode()
if anim_names:
print(f"通过 Actor 路径检测到动画: {candidate} -> {anim_names}")
has_animations = True
break
except Exception:
continue
except Exception:
pass
if has_animations: if has_animations:
print(f"检测到模型 {model_node.getName()} 包含动画:") print(f"检测到模型 {model_node.getName()} 包含动画:")
@ -2114,6 +2165,8 @@ class SceneManager:
model_node.setTag("can_create_actor_from_memory", "true") model_node.setTag("can_create_actor_from_memory", "true")
else: else:
model_node.setTag("has_animations", "false") model_node.setTag("has_animations", "false")
model_node.setTag("has_animations_checked", "true")
return has_animations return has_animations
@ -2645,8 +2698,9 @@ class SceneManager:
# 保存光源对象引用 # 保存光源对象引用
light_node.setPythonTag("rp_light_object", light) light_node.setPythonTag("rp_light_object", light)
# 添加到管理列表 # 添加到管理列表(去重)
self.Spotlight.append(light_node) if light_node not in self.Spotlight:
self.Spotlight.append(light_node)
# 确保灯光节点有正确的标签,以便在场景树更新时被识别 # 确保灯光节点有正确的标签,以便在场景树更新时被识别
if not light_node.hasTag("is_scene_element"): if not light_node.hasTag("is_scene_element"):
@ -2703,8 +2757,9 @@ class SceneManager:
# 保存光源对象引用 # 保存光源对象引用
light_node.setPythonTag("rp_light_object", light) light_node.setPythonTag("rp_light_object", light)
# 添加到管理列表 # 添加到管理列表(去重)
self.Pointlight.append(light_node) if light_node not in self.Pointlight:
self.Pointlight.append(light_node)
# 确保灯光节点有正确的标签,以便在场景树更新时被识别 # 确保灯光节点有正确的标签,以便在场景树更新时被识别
if not light_node.hasTag("is_scene_element"): if not light_node.hasTag("is_scene_element"):
@ -2877,9 +2932,8 @@ class SceneManager:
# 创建挂载节点 - 挂载到选中的父节点 # 创建挂载节点 - 挂载到选中的父节点
light_np = NodePath(light_name) light_np = NodePath(light_name)
light_np.reparentTo(parent_node) # 挂载到父节点而不是render light_np.reparentTo(parent_node) # 挂载到父节点而不是render
light_np.setPos(*pos)
light_np.setTransform(TransformState.makeIdentity()) light_np.setTransform(TransformState.makeIdentity())
light_np.setPos(*pos)
# 创建聚光灯对象 # 创建聚光灯对象
light = SpotLight() light = SpotLight()
@ -2986,10 +3040,9 @@ class SceneManager:
# 创建挂载节点 - 挂载到选中的父节点 # 创建挂载节点 - 挂载到选中的父节点
light_np = NodePath(light_name) light_np = NodePath(light_name)
light_np.reparentTo(parent_node) # 挂载到父节点而不是render light_np.reparentTo(parent_node) # 挂载到父节点而不是render
light_np.setPos(*pos)
# 确保变换矩阵有效 # 确保变换矩阵有效
light_np.setTransform(TransformState.makeIdentity()) light_np.setTransform(TransformState.makeIdentity())
light_np.setPos(*pos)
# 创建点光源对象 # 创建点光源对象
light = PointLight() light = PointLight()
@ -4694,8 +4747,16 @@ except Exception as e:
print(f"✓ RenderPipeline聚光灯创建成功位置: {pos}") print(f"✓ RenderPipeline聚光灯创建成功位置: {pos}")
# 创建包装节点用于场景树显示 # 创建包装节点用于场景树显示
spotlight_node = self.world.render.attachNewNode("spotlight_wrapper") light_name = f"Spotlight_{len(self.Spotlight)}"
spotlight_node.setPos(pos) spotlight_node = self.world.render.attachNewNode(light_name)
spotlight_node.setPos(*pos)
spotlight_node.setTag("light_type", "spot_light")
spotlight_node.setTag("is_scene_element", "1")
spotlight_node.setTag("tree_item_type", "LIGHT_NODE")
spotlight_node.setTag("light_energy", str(getattr(spotlight, "energy", 5000)))
spotlight_node.setTag("created_by_user", "1")
spotlight_node.setTag("element_type", "spotlight")
spotlight_node.setPythonTag("rp_light_object", spotlight)
self.Spotlight.append(spotlight_node) self.Spotlight.append(spotlight_node)
return spotlight_node return spotlight_node
else: else:
@ -4713,6 +4774,11 @@ except Exception as e:
# 创建光源节点 # 创建光源节点
spotlight_node = self.world.render.attachNewNode(spotlight) spotlight_node = self.world.render.attachNewNode(spotlight)
spotlight_node.setPos(pos) spotlight_node.setPos(pos)
spotlight_node.setTag("light_type", "spot_light")
spotlight_node.setTag("is_scene_element", "1")
spotlight_node.setTag("tree_item_type", "LIGHT_NODE")
spotlight_node.setTag("created_by_user", "1")
spotlight_node.setTag("element_type", "spotlight")
# 设置聚光灯方向(向下照射) # 设置聚光灯方向(向下照射)
spotlight_node.lookAt(pos[0], pos[1], pos[2] - 5) # 向下看5个单位 spotlight_node.lookAt(pos[0], pos[1], pos[2] - 5) # 向下看5个单位
@ -4769,8 +4835,16 @@ except Exception as e:
print(f"✓ RenderPipeline点光源创建成功位置: {pos}") print(f"✓ RenderPipeline点光源创建成功位置: {pos}")
# 创建包装节点用于场景树显示 # 创建包装节点用于场景树显示
pointlight_node = self.world.render.attachNewNode("pointlight_wrapper") light_name = f"Pointlight_{len(self.Pointlight)}"
pointlight_node.setPos(pos) pointlight_node = self.world.render.attachNewNode(light_name)
pointlight_node.setPos(*pos)
pointlight_node.setTag("light_type", "point_light")
pointlight_node.setTag("is_scene_element", "1")
pointlight_node.setTag("tree_item_type", "LIGHT_NODE")
pointlight_node.setTag("light_energy", str(getattr(pointlight, "energy", 3000)))
pointlight_node.setTag("created_by_user", "1")
pointlight_node.setTag("element_type", "pointlight")
pointlight_node.setPythonTag("rp_light_object", pointlight)
self.Pointlight.append(pointlight_node) self.Pointlight.append(pointlight_node)
return pointlight_node return pointlight_node
else: else:
@ -4788,6 +4862,11 @@ except Exception as e:
# 创建光源节点 # 创建光源节点
pointlight_node = self.world.render.attachNewNode(pointlight) pointlight_node = self.world.render.attachNewNode(pointlight)
pointlight_node.setPos(pos) pointlight_node.setPos(pos)
pointlight_node.setTag("light_type", "point_light")
pointlight_node.setTag("is_scene_element", "1")
pointlight_node.setTag("tree_item_type", "LIGHT_NODE")
pointlight_node.setTag("created_by_user", "1")
pointlight_node.setTag("element_type", "pointlight")
# 添加到光源列表 # 添加到光源列表
self.Pointlight.append(pointlight_node) self.Pointlight.append(pointlight_node)

File diff suppressed because it is too large Load Diff

View File

@ -1054,11 +1054,21 @@ class AppActions:
self.ssbo_editor.load_model(file_path) self.ssbo_editor.load_model(file_path)
model_np = getattr(self.ssbo_editor, 'model', None) model_np = getattr(self.ssbo_editor, 'model', None)
# Keep legacy ray-pick fallback usable by adding a collision body. # Keep legacy ray-pick fallback usable by adding a collision body.
if model_np and hasattr(self, 'scene_manager') and self.scene_manager: if model_np:
try: # Apply vital tags manually since SSBO overrides SceneManager loader
self.scene_manager.setupCollision(model_np) model_np.setTag("model_path", file_path)
except Exception as e: model_np.setTag("original_path", file_path)
print(f"[SSBO] setupCollision failed: {e}") model_np.setTag("is_model_root", "1")
model_np.setTag("is_scene_element", "1")
model_np.setTag("file", os.path.basename(file_path))
model_np.setName(os.path.basename(file_path))
if hasattr(self, 'scene_manager') and self.scene_manager:
try:
self.scene_manager.setupCollision(model_np)
self.scene_manager._processModelAnimations(model_np)
except Exception as e:
print(f"[SSBO] setup components failed: {e}")
return model_np return model_np
except Exception as e: except Exception as e:
print(f"[SSBO] load_model failed: {e}") print(f"[SSBO] load_model failed: {e}")

View File

@ -1102,29 +1102,41 @@ class EditorPanels:
# 动画徽章优化检测逻辑避免重复创建Actor # 动画徽章优化检测逻辑避免重复创建Actor
has_animation = False has_animation = False
if node_type == "模型": # 只对模型类型进行动画检测 if node_type == "模型": # 只对模型类型进行动画检测
# 首先检查是否已经缓存了检测结果 model_path = node.getTag("model_path") if node.hasTag("model_path") else ""
cached_result = node.getPythonTag('animation') likely_anim_format = bool(model_path and model_path.lower().endswith(('.glb', '.gltf', '.fbx', '.bam', '.egg')))
if cached_result is not None:
has_animation = cached_result # 优先使用场景标签(导入/加载时会写入)
else: if node.hasTag("has_animations"):
# 只有在未缓存时才进行检测 has_animation = node.getTag("has_animations").lower() == "true"
# 再做轻量结构检测(不依赖 Actor
if not has_animation:
try: try:
# 使用轻量级检测:先检查文件扩展名 has_character = node.findAllMatches("**/+Character").getNumPaths() > 0
model_path = node.getTag("model_path") has_bundle = node.findAllMatches("**/+AnimBundleNode").getNumPaths() > 0
if model_path and model_path.lower().endswith(('.glb', '.gltf', '.fbx')): has_animation = has_character or has_bundle
# 对于可能包含动画的格式才进行Actor检测 if has_animation:
actor = self._getActor(node) node.setTag("has_animations", "true")
if actor and actor.getAnimNames(): node.setTag("can_create_actor_from_memory", "true")
has_animation = True except Exception:
# 缓存检测结果 pass
node.setPythonTag('animation', has_animation)
print(f"[动画检测] {node.getName()}: {'有动画' if has_animation else '无动画'}") # 最后才尝试 Actor 检测(只缓存“有动画”,避免把失败结果永久缓存)
else: cached_result = node.getPythonTag('animation')
# 对于不太可能有动画的格式,直接标记为无动画 if cached_result is True:
node.setPythonTag('animation', False) has_animation = True
elif not has_animation and likely_anim_format:
try:
actor = self._getActor(node)
if actor and actor.getAnimNames():
has_animation = True
node.setTag("has_animations", "true")
node.setPythonTag('animation', True)
print(f"[动画检测] {node.getName()}: 有动画")
except Exception as e: except Exception as e:
print(f"动画检测失败: {e}") print(f"动画检测失败: {e}")
node.setPythonTag('animation', False) elif cached_result is False and not likely_anim_format:
has_animation = False
else: else:
# 对于非模型类型,检查已有的动画标签 # 对于非模型类型,检查已有的动画标签
has_animation = hasattr(node, 'getPythonTag') and node.getPythonTag('animation') has_animation = hasattr(node, 'getPythonTag') and node.getPythonTag('animation')
@ -1492,39 +1504,116 @@ class EditorPanels:
def _draw_animation_properties(self, node): def _draw_animation_properties(self, node):
"""绘制动画控制属性面板(优化版本,使用缓存避免重复计算)""" """绘制动画控制属性面板(优化版本,使用缓存避免重复计算)"""
anim_node = node
try:
if hasattr(self, "_resolve_animation_owner_model"):
resolved = self._resolve_animation_owner_model(node)
if resolved and not resolved.isEmpty():
anim_node = resolved
except Exception:
pass
# 路径兜底:当 anim_node 缺少路径时,从 scene_manager.models 中反查祖先模型
try:
needs_path = (not anim_node.hasTag("model_path")) or (not anim_node.getTag("model_path"))
if needs_path and hasattr(self, "scene_manager") and self.scene_manager:
models = getattr(self.scene_manager, "models", [])
for model in list(models):
try:
if not model or model.isEmpty():
continue
if (model == anim_node or model.isAncestorOf(anim_node) or anim_node.isAncestorOf(model)):
if model.hasTag("model_path") and model.getTag("model_path"):
anim_node.setTag("model_path", model.getTag("model_path"))
if model.hasTag("original_path") and model.getTag("original_path"):
anim_node.setTag("original_path", model.getTag("original_path"))
break
if model.hasTag("saved_model_path") and model.getTag("saved_model_path"):
anim_node.setTag("model_path", model.getTag("saved_model_path"))
break
except Exception:
continue
except Exception:
pass
# 先刷新一次模型动画标签,避免“导入后未初始化”导致误判
try:
if hasattr(self, "scene_manager") and self.scene_manager and hasattr(self.scene_manager, "_processModelAnimations"):
self.scene_manager._processModelAnimations(anim_node)
except Exception:
pass
has_animation_tag = anim_node.hasTag("has_animations") and anim_node.getTag("has_animations").lower() == "true"
has_animation_nodes = False
try:
has_animation_nodes = (
anim_node.findAllMatches("**/+Character").getNumPaths() > 0 or
anim_node.findAllMatches("**/+AnimBundleNode").getNumPaths() > 0
)
except Exception:
pass
# 检查是否已经缓存了动画信息 # 检查是否已经缓存了动画信息
cached_anim_info = node.getPythonTag("cached_anim_info") cached_anim_info = anim_node.getPythonTag("cached_anim_info")
cached_processed_names = node.getPythonTag("cached_processed_names") cached_processed_names = anim_node.getPythonTag("cached_processed_names")
# 如果之前缓存的是“格式未知”,但现在已有路径,强制重建缓存
try:
now_has_path = anim_node.hasTag("model_path") and bool(anim_node.getTag("model_path"))
if now_has_path and isinstance(cached_anim_info, str) and "格式: 未知" in cached_anim_info:
cached_anim_info = None
cached_processed_names = None
anim_node.setPythonTag("cached_anim_info", None)
anim_node.setPythonTag("cached_processed_names", None)
anim_node.setPythonTag("animation", None)
self._clear_animation_cache(anim_node)
except Exception:
pass
# 如果节点已被检测为有动画,但缓存是“无动画”,强制重新检测一次
if (has_animation_tag or has_animation_nodes) and (cached_anim_info == "无动画" or cached_processed_names == []):
cached_anim_info = None
cached_processed_names = None
anim_node.setPythonTag("cached_anim_info", None)
anim_node.setPythonTag("cached_processed_names", None)
anim_node.setPythonTag("animation", None)
# 只有在没有缓存时才进行完整的动画检测和处理 # 只有在没有缓存时才进行完整的动画检测和处理
if cached_anim_info is None or cached_processed_names is None: if cached_anim_info is None or cached_processed_names is None:
# 获取Actor # 获取Actor
actor = self._getActor(node) actor = self._getActor(anim_node)
if not actor: if not actor:
imgui.text_colored((0.7, 0.7, 0.7, 1.0), "此模型不包含动画") if has_animation_tag or has_animation_nodes:
imgui.text_colored((1.0, 0.7, 0.3, 1.0), "检测到动画结构但当前未成功绑定Actor")
else:
imgui.text_colored((0.7, 0.7, 0.7, 1.0), "此模型不包含动画")
return return
# 获取和分析动画名称 # 获取和分析动画名称
anim_names = actor.getAnimNames() anim_names = actor.getAnimNames()
processed_names = self._processAnimationNames(node, anim_names) processed_names = self._processAnimationNames(anim_node, anim_names)
if not processed_names: if not processed_names:
imgui.text_colored((0.7, 0.7, 0.7, 1.0), "未检测到动画序列") imgui.text_colored((0.7, 0.7, 0.7, 1.0), "未检测到动画序列")
# 缓存空结果 # 只在明确无动画时缓存空结果,避免误缓存导致后续无法重试
node.setPythonTag("cached_processed_names", []) if not (has_animation_tag or has_animation_nodes):
node.setPythonTag("cached_anim_info", "无动画") anim_node.setPythonTag("cached_processed_names", [])
anim_node.setPythonTag("cached_anim_info", "无动画")
return return
anim_node.setTag("has_animations", "true")
anim_node.setPythonTag("animation", True)
# 计算并缓存动画信息 # 计算并缓存动画信息
format_info = self._getModelFormat(node) format_info = self._getModelFormat(anim_node)
animation_info = self._analyzeAnimationQuality(actor, anim_names, format_info) animation_info = self._analyzeAnimationQuality(actor, anim_names, format_info)
info_text = f"格式: {format_info} | 动画数量: {len(processed_names)}" info_text = f"格式: {format_info} | 动画数量: {len(processed_names)}"
if animation_info: if animation_info:
info_text += f" | {animation_info}" info_text += f" | {animation_info}"
# 缓存结果 # 缓存结果
node.setPythonTag("cached_anim_info", info_text) anim_node.setPythonTag("cached_anim_info", info_text)
node.setPythonTag("cached_processed_names", processed_names) anim_node.setPythonTag("cached_processed_names", processed_names)
else: else:
# 使用缓存的数据 # 使用缓存的数据
@ -1548,10 +1637,11 @@ class EditorPanels:
imgui.same_line() imgui.same_line()
# 获取当前选中的动画 # 获取当前选中的动画
current_anim = node.getPythonTag("selected_animation") current_anim = anim_node.getPythonTag("selected_animation")
if current_anim is None: valid_original_names = [original_name for _, original_name in processed_names]
if current_anim is None or current_anim not in valid_original_names:
current_anim = processed_names[0][1] if processed_names else "" current_anim = processed_names[0][1] if processed_names else ""
node.setPythonTag("selected_animation", current_anim) anim_node.setPythonTag("selected_animation", current_anim)
# 查找当前动画的索引 # 查找当前动画的索引
current_index = 0 current_index = 0
@ -1566,7 +1656,7 @@ class EditorPanels:
if changed and new_index < len(processed_names): if changed and new_index < len(processed_names):
selected_display, selected_original = processed_names[new_index] selected_display, selected_original = processed_names[new_index]
node.setPythonTag("selected_animation", selected_original) anim_node.setPythonTag("selected_animation", selected_original)
print(f"选择动画: {selected_display} (原始名称: {selected_original})") print(f"选择动画: {selected_display} (原始名称: {selected_original})")
imgui.spacing() imgui.spacing()
@ -1576,22 +1666,22 @@ class EditorPanels:
# 播放按钮 # 播放按钮
if imgui.button("播放##play_animation"): if imgui.button("播放##play_animation"):
self._playAnimation(node) self._playAnimation(anim_node)
imgui.same_line() imgui.same_line()
# 暂停按钮 # 暂停按钮
if imgui.button("暂停##pause_animation"): if imgui.button("暂停##pause_animation"):
self._pauseAnimation(node) self._pauseAnimation(anim_node)
imgui.same_line() imgui.same_line()
# 停止按钮 # 停止按钮
if imgui.button("停止##stop_animation"): if imgui.button("停止##stop_animation"):
self._stopAnimation(node) self._stopAnimation(anim_node)
imgui.same_line() imgui.same_line()
# 循环按钮 # 循环按钮
if imgui.button("循环##loop_animation"): if imgui.button("循环##loop_animation"):
self._loopAnimation(node) self._loopAnimation(anim_node)
imgui.spacing() imgui.spacing()
@ -1600,16 +1690,16 @@ class EditorPanels:
imgui.same_line() imgui.same_line()
# 获取当前速度 # 获取当前速度
current_speed = node.getPythonTag("anim_speed") current_speed = anim_node.getPythonTag("anim_speed")
if current_speed is None: if current_speed is None:
current_speed = 1.0 current_speed = 1.0
node.setPythonTag("anim_speed", current_speed) anim_node.setPythonTag("anim_speed", current_speed)
# 速度滑块 # 速度滑块
changed, new_speed = imgui.slider_float("##anim_speed", current_speed, 0.1, 5.0, "%.1f") changed, new_speed = imgui.slider_float("##anim_speed", current_speed, 0.1, 5.0, "%.1f")
if changed: if changed:
node.setPythonTag("anim_speed", new_speed) anim_node.setPythonTag("anim_speed", new_speed)
self._setAnimationSpeed(node, new_speed) self._setAnimationSpeed(anim_node, new_speed)
imgui.same_line() imgui.same_line()
imgui.text_colored((0.7, 0.7, 0.7, 1.0), "倍速") imgui.text_colored((0.7, 0.7, 0.7, 1.0), "倍速")