1573 lines
67 KiB
Python
1573 lines
67 KiB
Python
import os
|
||
from pathlib import Path
|
||
|
||
from direct.actor.Actor import Actor
|
||
from direct.task.TaskManagerGlobal import taskMgr
|
||
from panda3d.core import NodePath, PartSubset
|
||
|
||
|
||
class _BoundAnimationProxy:
|
||
"""Actor-compatible wrapper backed by auto-bound AnimControls."""
|
||
|
||
def __init__(self, node, controls, owns_node=False):
|
||
self._node = node
|
||
self._controls = controls
|
||
self._owns_node = owns_node
|
||
|
||
def isEmpty(self):
|
||
return self._node.isEmpty()
|
||
|
||
def show(self):
|
||
self._node.show()
|
||
|
||
def hide(self):
|
||
self._node.hide()
|
||
|
||
def setPos(self, *args):
|
||
self._node.setPos(*args)
|
||
|
||
def setHpr(self, *args):
|
||
self._node.setHpr(*args)
|
||
|
||
def setScale(self, *args):
|
||
self._node.setScale(*args)
|
||
|
||
def setTransform(self, *args):
|
||
self._node.setTransform(*args)
|
||
|
||
def getTransform(self, *args):
|
||
return self._node.getTransform(*args)
|
||
|
||
def reparentTo(self, *args):
|
||
self._node.reparentTo(*args)
|
||
|
||
def getAnimNames(self):
|
||
return list(self._controls.keys())
|
||
|
||
def getAnimControl(self, anim_name):
|
||
return self._controls.get(anim_name)
|
||
|
||
def play(self, anim_name):
|
||
control = self.getAnimControl(anim_name)
|
||
if control:
|
||
try:
|
||
control.stop()
|
||
except Exception:
|
||
pass
|
||
try:
|
||
control.pose(0)
|
||
except Exception:
|
||
pass
|
||
control.play()
|
||
|
||
def loop(self, anim_name):
|
||
control = self.getAnimControl(anim_name)
|
||
if control:
|
||
# AnimControl.loop 需要 restart 参数
|
||
try:
|
||
control.stop()
|
||
except Exception:
|
||
pass
|
||
try:
|
||
control.pose(0)
|
||
except Exception:
|
||
pass
|
||
control.loop(True)
|
||
|
||
def stop(self):
|
||
for control in self._controls.values():
|
||
try:
|
||
control.stop()
|
||
except Exception:
|
||
pass
|
||
|
||
def setPlayRate(self, speed, anim_name):
|
||
control = self.getAnimControl(anim_name)
|
||
if control:
|
||
control.setPlayRate(speed)
|
||
|
||
def cleanup(self):
|
||
if self._owns_node:
|
||
try:
|
||
if not self._node.isEmpty():
|
||
self._node.removeNode()
|
||
except Exception:
|
||
pass
|
||
|
||
def removeNode(self):
|
||
if self._owns_node:
|
||
try:
|
||
if not self._node.isEmpty():
|
||
self._node.removeNode()
|
||
except Exception:
|
||
pass
|
||
|
||
|
||
class AnimationTools:
|
||
"""Animation and actor helper methods extracted from main world."""
|
||
|
||
def __init__(self, app):
|
||
self.app = app
|
||
|
||
def __getattr__(self, name):
|
||
return getattr(self.app, name)
|
||
|
||
def __setattr__(self, name, value):
|
||
if name == "app" or name in self.__dict__ or hasattr(type(self), name):
|
||
object.__setattr__(self, name, value)
|
||
else:
|
||
setattr(self.app, name, value)
|
||
|
||
def _is_scene_root_node(self, node):
|
||
try:
|
||
if not node or node.isEmpty():
|
||
return False
|
||
name = node.getName()
|
||
return name in ("render", "render2d", "aspect2d", "pixel2d")
|
||
except Exception:
|
||
return False
|
||
|
||
def _get_owner_parent_node(self, owner_model):
|
||
"""获取动画对象的挂接父节点,确保不是空父节点。"""
|
||
try:
|
||
if owner_model and not owner_model.isEmpty():
|
||
parent = owner_model.getParent()
|
||
if parent and not parent.isEmpty():
|
||
return parent
|
||
except Exception:
|
||
pass
|
||
return self.render
|
||
|
||
def _node_has_geom(self, node):
|
||
try:
|
||
if not node or node.isEmpty():
|
||
return False
|
||
return node.findAllMatches("**/+GeomNode").getNumPaths() > 0
|
||
except Exception:
|
||
return False
|
||
|
||
def _node_has_animation_nodes(self, node):
|
||
try:
|
||
if not node or node.isEmpty():
|
||
return False
|
||
return (
|
||
node.findAllMatches("**/+Character").getNumPaths() > 0 or
|
||
node.findAllMatches("**/+AnimBundleNode").getNumPaths() > 0
|
||
)
|
||
except Exception:
|
||
return False
|
||
|
||
def _prefer_owner_with_visible_geometry(self, origin_model, owner_model):
|
||
"""
|
||
优先选择“带可见几何体”的动画 owner,避免绑定到纯骨骼节点导致播放时不可见。
|
||
"""
|
||
try:
|
||
if self._node_has_geom(owner_model):
|
||
return owner_model
|
||
|
||
candidates = []
|
||
|
||
def _add_candidate(node, bonus=0):
|
||
if not node or node.isEmpty() or self._is_scene_root_node(node):
|
||
return
|
||
for existing, _ in candidates:
|
||
try:
|
||
if existing == node:
|
||
return
|
||
except Exception:
|
||
continue
|
||
candidates.append((node, bonus))
|
||
|
||
_add_candidate(owner_model, 100)
|
||
_add_candidate(origin_model, 90)
|
||
|
||
# origin/owner 祖先链中优先找可见几何体节点
|
||
for seed, base_bonus in ((origin_model, 80), (owner_model, 70)):
|
||
current = seed
|
||
for depth in range(48):
|
||
if not current or current.isEmpty() or self._is_scene_root_node(current):
|
||
break
|
||
_add_candidate(current, base_bonus - depth)
|
||
parent = current.getParent()
|
||
if not parent or parent.isEmpty() or parent == current:
|
||
break
|
||
current = parent
|
||
|
||
# 从 scene_manager.models 中补充候选(含弱关联兜底)
|
||
scene_manager = getattr(self, "scene_manager", None)
|
||
models = getattr(scene_manager, "models", None) if scene_manager else None
|
||
if models:
|
||
origin_name = ""
|
||
try:
|
||
origin_name = (origin_model.getName() or "").strip().lower()
|
||
except Exception:
|
||
origin_name = ""
|
||
|
||
related_models = []
|
||
weak_name_match_models = []
|
||
anim_geom_models = []
|
||
for model in list(models):
|
||
try:
|
||
if not model or model.isEmpty() or self._is_scene_root_node(model):
|
||
continue
|
||
related = (
|
||
model == origin_model or
|
||
model == owner_model or
|
||
model.isAncestorOf(origin_model) or
|
||
origin_model.isAncestorOf(model) or
|
||
model.isAncestorOf(owner_model) or
|
||
owner_model.isAncestorOf(model)
|
||
)
|
||
if related:
|
||
related_models.append(model)
|
||
|
||
model_name = (model.getName() or "").strip().lower()
|
||
if origin_name and model_name == origin_name:
|
||
weak_name_match_models.append(model)
|
||
|
||
if self._node_has_geom(model) and self._node_has_animation_nodes(model):
|
||
anim_geom_models.append(model)
|
||
except Exception:
|
||
continue
|
||
|
||
for m in related_models:
|
||
_add_candidate(m, 75)
|
||
for m in weak_name_match_models:
|
||
_add_candidate(m, 60)
|
||
if len(anim_geom_models) == 1:
|
||
_add_candidate(anim_geom_models[0], 85)
|
||
|
||
best = owner_model
|
||
best_score = -10**9
|
||
for node, bonus in candidates:
|
||
try:
|
||
has_geom = self._node_has_geom(node)
|
||
has_anim = self._node_has_animation_nodes(node)
|
||
has_path = (
|
||
(node.hasTag("model_path") and bool(node.getTag("model_path"))) or
|
||
(node.hasTag("saved_model_path") and bool(node.getTag("saved_model_path"))) or
|
||
(node.hasTag("original_path") and bool(node.getTag("original_path")))
|
||
)
|
||
score = bonus
|
||
score += 220 if has_anim and has_geom else 0
|
||
score += 90 if has_geom else 0
|
||
score += 35 if has_anim else 0
|
||
score += 15 if has_path else 0
|
||
if score > best_score:
|
||
best_score = score
|
||
best = node
|
||
except Exception:
|
||
continue
|
||
|
||
# 把路径标签补齐到最终 owner,避免后续回退到“格式未知/纯内存”
|
||
try:
|
||
if best and best != owner_model:
|
||
for tag_name in ("model_path", "original_path", "saved_model_path", "file"):
|
||
if best.hasTag(tag_name) and best.getTag(tag_name):
|
||
continue
|
||
if owner_model.hasTag(tag_name) and owner_model.getTag(tag_name):
|
||
best.setTag(tag_name, owner_model.getTag(tag_name))
|
||
except Exception:
|
||
pass
|
||
|
||
return best or owner_model
|
||
except Exception:
|
||
return owner_model
|
||
|
||
def _find_scene_model_owner(self, node):
|
||
"""优先从 scene_manager.models 反查真实模型根节点。"""
|
||
try:
|
||
scene_manager = getattr(self, "scene_manager", None)
|
||
models = getattr(scene_manager, "models", None)
|
||
if not models or not node or node.isEmpty():
|
||
return None
|
||
|
||
best = None
|
||
best_score = -1
|
||
related_found = False
|
||
for model in list(models):
|
||
try:
|
||
if not model or model.isEmpty() or self._is_scene_root_node(model):
|
||
continue
|
||
|
||
# 必须与当前节点存在祖先关系
|
||
relation = 0
|
||
if model == node:
|
||
relation = 3
|
||
elif model.isAncestorOf(node):
|
||
relation = 2
|
||
elif node.isAncestorOf(model):
|
||
relation = 1
|
||
if relation == 0:
|
||
continue
|
||
related_found = True
|
||
|
||
has_model_path = model.hasTag("model_path") and bool(model.getTag("model_path"))
|
||
has_saved_path = model.hasTag("saved_model_path") and bool(model.getTag("saved_model_path"))
|
||
has_anim = (
|
||
model.findAllMatches("**/+Character").getNumPaths() > 0 or
|
||
model.findAllMatches("**/+AnimBundleNode").getNumPaths() > 0
|
||
)
|
||
has_geom = model.findAllMatches("**/+GeomNode").getNumPaths() > 0
|
||
is_model_root = model.hasTag("is_model_root") and model.getTag("is_model_root") == "1"
|
||
is_imported_root = model.hasTag("tree_item_type") and model.getTag("tree_item_type") == "IMPORTED_MODEL_NODE"
|
||
|
||
score = 0
|
||
score += relation * 100
|
||
score += 60 if has_model_path else 0
|
||
score += 40 if has_saved_path else 0
|
||
score += 30 if has_anim else 0
|
||
score += 20 if has_geom else 0
|
||
score += 10 if is_model_root else 0
|
||
score += 8 if is_imported_root else 0
|
||
|
||
if score > best_score:
|
||
best_score = score
|
||
best = model
|
||
except Exception:
|
||
continue
|
||
if best:
|
||
return best
|
||
|
||
# 关系反查失败时的兜底:同名优先(常见于场景重建后层级丢失)
|
||
node_name = ""
|
||
try:
|
||
node_name = (node.getName() or "").strip().lower()
|
||
except Exception:
|
||
node_name = ""
|
||
|
||
if not related_found and node_name:
|
||
fallback_best = None
|
||
fallback_score = -1
|
||
for model in list(models):
|
||
try:
|
||
if not model or model.isEmpty() or self._is_scene_root_node(model):
|
||
continue
|
||
model_name = (model.getName() or "").strip().lower()
|
||
if model_name != node_name:
|
||
continue
|
||
|
||
has_model_path = model.hasTag("model_path") and bool(model.getTag("model_path"))
|
||
has_saved_path = model.hasTag("saved_model_path") and bool(model.getTag("saved_model_path"))
|
||
has_anim = (
|
||
model.findAllMatches("**/+Character").getNumPaths() > 0 or
|
||
model.findAllMatches("**/+AnimBundleNode").getNumPaths() > 0
|
||
)
|
||
has_geom = model.findAllMatches("**/+GeomNode").getNumPaths() > 0
|
||
score = 0
|
||
score += 80 if has_anim and has_geom else 0
|
||
score += 40 if has_model_path else 0
|
||
score += 25 if has_saved_path else 0
|
||
if score > fallback_score:
|
||
fallback_score = score
|
||
fallback_best = model
|
||
except Exception:
|
||
continue
|
||
if fallback_best:
|
||
return fallback_best
|
||
|
||
# 单模型场景兜底
|
||
valid_models = []
|
||
for model in list(models):
|
||
try:
|
||
if model and (not model.isEmpty()) and (not self._is_scene_root_node(model)):
|
||
valid_models.append(model)
|
||
except Exception:
|
||
continue
|
||
if len(valid_models) == 1:
|
||
return valid_models[0]
|
||
|
||
# 若场景中仅有一个“动画+几何体”模型,也直接使用它
|
||
anim_models = []
|
||
for model in valid_models:
|
||
try:
|
||
has_anim = (
|
||
model.findAllMatches("**/+Character").getNumPaths() > 0 or
|
||
model.findAllMatches("**/+AnimBundleNode").getNumPaths() > 0
|
||
)
|
||
has_geom = model.findAllMatches("**/+GeomNode").getNumPaths() > 0
|
||
if has_anim and has_geom:
|
||
anim_models.append(model)
|
||
except Exception:
|
||
continue
|
||
if len(anim_models) == 1:
|
||
return anim_models[0]
|
||
|
||
return None
|
||
except Exception:
|
||
return None
|
||
|
||
def _recover_model_path_from_tags(self, node):
|
||
"""从常见标签恢复模型路径,尽量避免退化到纯内存 autoBind。"""
|
||
try:
|
||
project_root = Path(__file__).resolve().parents[2]
|
||
search_roots = [
|
||
project_root,
|
||
project_root / "Resources",
|
||
project_root / "Resources" / "models",
|
||
]
|
||
|
||
raw_candidates = []
|
||
if node and not node.isEmpty():
|
||
for tag_name in ("model_path", "original_path", "saved_model_path", "file"):
|
||
try:
|
||
if node.hasTag(tag_name):
|
||
val = node.getTag(tag_name)
|
||
if val:
|
||
raw_candidates.append(val)
|
||
except Exception:
|
||
continue
|
||
|
||
# 父链继续补齐
|
||
walker = node.getParent() if (node and not node.isEmpty()) else None
|
||
for _ in range(64):
|
||
if not walker or walker.isEmpty() or self._is_scene_root_node(walker):
|
||
break
|
||
for tag_name in ("model_path", "original_path", "saved_model_path", "file"):
|
||
try:
|
||
if walker.hasTag(tag_name):
|
||
val = walker.getTag(tag_name)
|
||
if val:
|
||
raw_candidates.append(val)
|
||
except Exception:
|
||
continue
|
||
parent = walker.getParent()
|
||
if not parent or parent.isEmpty() or parent == walker:
|
||
break
|
||
walker = parent
|
||
|
||
# scene_manager.models 补齐
|
||
model_owner = self._find_scene_model_owner(node)
|
||
if model_owner and not model_owner.isEmpty():
|
||
for tag_name in ("model_path", "original_path", "saved_model_path", "file"):
|
||
try:
|
||
if model_owner.hasTag(tag_name):
|
||
val = model_owner.getTag(tag_name)
|
||
if val:
|
||
raw_candidates.append(val)
|
||
except Exception:
|
||
continue
|
||
|
||
# 全局兜底:当当前节点标签全丢失时,从 scene_manager.models 收集唯一可用路径
|
||
scene_manager = getattr(self, "scene_manager", None)
|
||
models = getattr(scene_manager, "models", None) if scene_manager else None
|
||
if models:
|
||
scene_paths = []
|
||
best_related_path = ""
|
||
best_related_score = -1
|
||
for model in list(models):
|
||
try:
|
||
if not model or model.isEmpty() or self._is_scene_root_node(model):
|
||
continue
|
||
relation = 0
|
||
try:
|
||
if model == node:
|
||
relation = 3
|
||
elif model.isAncestorOf(node):
|
||
relation = 2
|
||
elif node.isAncestorOf(model):
|
||
relation = 1
|
||
except Exception:
|
||
relation = 0
|
||
|
||
has_anim = self._node_has_animation_nodes(model)
|
||
has_geom = self._node_has_geom(model)
|
||
|
||
candidate_path = ""
|
||
for tag_name in ("model_path", "original_path", "saved_model_path", "file"):
|
||
if model.hasTag(tag_name):
|
||
val = model.getTag(tag_name)
|
||
if val:
|
||
scene_paths.append(val)
|
||
if (not candidate_path) and tag_name in ("model_path", "original_path", "saved_model_path"):
|
||
candidate_path = val
|
||
if relation > 0 and candidate_path:
|
||
score = relation * 100
|
||
score += 40 if has_anim else 0
|
||
score += 25 if has_geom else 0
|
||
try:
|
||
if os.path.exists(candidate_path) or os.path.exists(os.path.normpath(candidate_path)):
|
||
score += 35
|
||
except Exception:
|
||
pass
|
||
if score > best_related_score:
|
||
best_related_score = score
|
||
best_related_path = candidate_path
|
||
except Exception:
|
||
continue
|
||
# 去重
|
||
uniq_scene_paths = []
|
||
seen_scene = set()
|
||
for p in scene_paths:
|
||
try:
|
||
key = os.path.normcase(os.path.normpath(p))
|
||
except Exception:
|
||
key = p
|
||
if key in seen_scene:
|
||
continue
|
||
seen_scene.add(key)
|
||
uniq_scene_paths.append(p)
|
||
# 仅在唯一时采用,避免误绑到别的模型
|
||
if len(uniq_scene_paths) == 1:
|
||
raw_candidates.append(uniq_scene_paths[0])
|
||
elif best_related_path:
|
||
raw_candidates.append(best_related_path)
|
||
|
||
# 1) 直接存在的路径优先
|
||
for c in raw_candidates:
|
||
try:
|
||
if os.path.exists(c):
|
||
return c
|
||
norm = os.path.normpath(c)
|
||
if os.path.exists(norm):
|
||
return norm
|
||
except Exception:
|
||
continue
|
||
|
||
# 2) 基于文件名在项目目录中补查
|
||
filenames = []
|
||
for c in raw_candidates:
|
||
try:
|
||
name = os.path.basename(c)
|
||
if name and "." in name:
|
||
filenames.append(name)
|
||
except Exception:
|
||
continue
|
||
|
||
tried = set()
|
||
for filename in filenames:
|
||
for root in search_roots:
|
||
candidate = str(root / filename)
|
||
key = os.path.normcase(os.path.normpath(candidate))
|
||
if key in tried:
|
||
continue
|
||
tried.add(key)
|
||
if os.path.exists(candidate):
|
||
return candidate
|
||
|
||
return ""
|
||
except Exception:
|
||
return ""
|
||
|
||
|
||
def _resolve_animation_owner_model(self, node):
|
||
"""向上查找动画所属的模型根节点,避免选中子节点时绑定失败。"""
|
||
scene_owner = self._find_scene_model_owner(node)
|
||
if scene_owner and not scene_owner.isEmpty():
|
||
try:
|
||
# 同步路径标签,避免后续格式识别为“未知”
|
||
if scene_owner.hasTag("model_path") and scene_owner.getTag("model_path"):
|
||
if (not node.hasTag("model_path")) or (not node.getTag("model_path")):
|
||
node.setTag("model_path", scene_owner.getTag("model_path"))
|
||
if scene_owner.hasTag("original_path") and scene_owner.getTag("original_path"):
|
||
if (not node.hasTag("original_path")) or (not node.getTag("original_path")):
|
||
node.setTag("original_path", scene_owner.getTag("original_path"))
|
||
except Exception:
|
||
pass
|
||
return scene_owner
|
||
|
||
try:
|
||
current = node
|
||
max_depth = 64
|
||
chain = []
|
||
for _ in range(max_depth):
|
||
if not current or current.isEmpty():
|
||
break
|
||
# 绝不把场景根节点当作动画 owner,否则 hide() 会把全场景隐藏
|
||
if self._is_scene_root_node(current):
|
||
break
|
||
|
||
chain.append(current)
|
||
parent = current.getParent()
|
||
if not parent or parent.isEmpty() or parent == current:
|
||
break
|
||
current = parent
|
||
|
||
# 标签缺失/层级复杂时:按可见播放优先级打分选 owner(动画+几何体最高)
|
||
if chain:
|
||
path_source = None
|
||
original_path_source = None
|
||
|
||
def score_candidate(c):
|
||
try:
|
||
has_character = c.findAllMatches("**/+Character").getNumPaths() > 0
|
||
has_bundle = c.findAllMatches("**/+AnimBundleNode").getNumPaths() > 0
|
||
has_anim = has_character or has_bundle
|
||
has_geom = c.findAllMatches("**/+GeomNode").getNumPaths() > 0
|
||
except Exception:
|
||
has_anim = False
|
||
has_geom = False
|
||
|
||
has_model_path = c.hasTag("model_path") and bool(c.getTag("model_path"))
|
||
is_model_root = c.hasTag("is_model_root") and c.getTag("is_model_root") == "1"
|
||
is_imported_root = c.hasTag("tree_item_type") and c.getTag("tree_item_type") == "IMPORTED_MODEL_NODE"
|
||
|
||
# 分数越高越优先:保证可见播放 > 仅有骨骼 > 空节点
|
||
score = 0
|
||
score += 100 if has_anim and has_geom else 0
|
||
score += 60 if has_model_path and has_geom else 0
|
||
score += 40 if has_anim else 0
|
||
score += 20 if has_geom else 0
|
||
score += 15 if has_model_path else 0
|
||
score += 10 if is_model_root else 0
|
||
score += 8 if is_imported_root else 0
|
||
return score
|
||
|
||
best = chain[0]
|
||
best_score = -1
|
||
for candidate in chain:
|
||
try:
|
||
if not path_source and candidate.hasTag("model_path") and candidate.getTag("model_path"):
|
||
path_source = candidate
|
||
if not original_path_source and candidate.hasTag("original_path") and candidate.getTag("original_path"):
|
||
original_path_source = candidate
|
||
except Exception:
|
||
pass
|
||
|
||
s = score_candidate(candidate)
|
||
if s > best_score:
|
||
best_score = s
|
||
best = candidate
|
||
|
||
# 把路径标签补到最终 owner,避免后续格式识别为“未知”
|
||
try:
|
||
if path_source and ((not best.hasTag("model_path")) or (not best.getTag("model_path"))):
|
||
best.setTag("model_path", path_source.getTag("model_path"))
|
||
if original_path_source and ((not best.hasTag("original_path")) or (not best.getTag("original_path"))):
|
||
best.setTag("original_path", original_path_source.getTag("original_path"))
|
||
if best.hasTag("saved_model_path") and ((not best.hasTag("model_path")) or (not best.getTag("model_path"))):
|
||
best.setTag("model_path", best.getTag("saved_model_path"))
|
||
except Exception:
|
||
pass
|
||
|
||
return best
|
||
except Exception:
|
||
pass
|
||
|
||
try:
|
||
if node and not node.isEmpty() and not self._is_scene_root_node(node):
|
||
return node
|
||
except Exception:
|
||
pass
|
||
return node
|
||
|
||
|
||
def _getActor(self, origin_model):
|
||
"""
|
||
获取或创建模型的Actor,用于动画控制
|
||
复用Qt版本经过验证的实现方式
|
||
"""
|
||
owner_model = self._resolve_animation_owner_model(origin_model)
|
||
owner_model = self._prefer_owner_with_visible_geometry(origin_model, owner_model)
|
||
|
||
# owner 节点缺失路径标签时,尝试从当前节点及其祖先继承路径,避免只能走内存分支
|
||
try:
|
||
needs_path = (not owner_model.hasTag("model_path")) or (not owner_model.getTag("model_path"))
|
||
if needs_path:
|
||
walker = origin_model
|
||
for _ in range(64):
|
||
if not walker or walker.isEmpty() or self._is_scene_root_node(walker):
|
||
break
|
||
if walker.hasTag("model_path") and walker.getTag("model_path"):
|
||
owner_model.setTag("model_path", walker.getTag("model_path"))
|
||
if walker.hasTag("original_path") and walker.getTag("original_path"):
|
||
owner_model.setTag("original_path", walker.getTag("original_path"))
|
||
break
|
||
if walker.hasTag("saved_model_path") and walker.getTag("saved_model_path"):
|
||
owner_model.setTag("model_path", walker.getTag("saved_model_path"))
|
||
break
|
||
parent = walker.getParent()
|
||
if not parent or parent.isEmpty() or parent == walker:
|
||
break
|
||
walker = parent
|
||
|
||
# 仍缺路径时,尝试更激进的标签恢复
|
||
if (not owner_model.hasTag("model_path")) or (not owner_model.getTag("model_path")):
|
||
recovered = self._recover_model_path_from_tags(owner_model)
|
||
if recovered:
|
||
owner_model.setTag("model_path", recovered)
|
||
if (not owner_model.hasTag("original_path")) or (not owner_model.getTag("original_path")):
|
||
owner_model.setTag("original_path", recovered)
|
||
except Exception:
|
||
pass
|
||
|
||
def _cleanup_actor(actor):
|
||
try:
|
||
if actor is None:
|
||
return
|
||
if hasattr(actor, "cleanup"):
|
||
actor.cleanup()
|
||
if hasattr(actor, "removeNode"):
|
||
actor.removeNode()
|
||
except Exception:
|
||
pass
|
||
|
||
def _try_create_actor_from_source(source, source_desc):
|
||
try:
|
||
actor = Actor(source)
|
||
# 无论是否已检测到动画名,都显式绑定一次,避免“有名字但无可播放控制”
|
||
try:
|
||
actor.bindAllAnims(allowAsyncBind=False)
|
||
except Exception:
|
||
pass
|
||
|
||
anims = actor.getAnimNames()
|
||
print(f"[Actor加载] {source_desc} 检测到动画: {anims}")
|
||
if not anims:
|
||
_cleanup_actor(actor)
|
||
return None
|
||
|
||
# 确认至少有一个可用的 AnimControl
|
||
playable = False
|
||
for anim_name in anims:
|
||
try:
|
||
control = actor.getAnimControl(anim_name)
|
||
if control and control.getNumFrames() > 1:
|
||
playable = True
|
||
break
|
||
except Exception:
|
||
continue
|
||
|
||
if not playable:
|
||
print(f"[Actor加载] {source_desc} 动画控制无效,尝试其他加载路径")
|
||
_cleanup_actor(actor)
|
||
return None
|
||
|
||
# 无可见几何体的 Actor 会导致“播放后什么都看不到”
|
||
has_geom = False
|
||
try:
|
||
has_geom = actor.findAllMatches("**/+GeomNode").getNumPaths() > 0
|
||
except Exception:
|
||
has_geom = False
|
||
if not has_geom:
|
||
print(f"[Actor加载] {source_desc} 无可见几何体,尝试其他加载路径")
|
||
_cleanup_actor(actor)
|
||
return None
|
||
|
||
actor.reparentTo(self._get_owner_parent_node(owner_model))
|
||
actor.hide()
|
||
return actor
|
||
except Exception as e:
|
||
print(f"[Actor加载] {source_desc} 失败: {e}")
|
||
return None
|
||
|
||
def _try_create_autobind_proxy(model_np, source_desc, owns_node=False):
|
||
try:
|
||
from panda3d.core import AnimControlCollection, autoBind
|
||
|
||
controls = {}
|
||
|
||
def collect_controls(np):
|
||
if not np or np.isEmpty():
|
||
return
|
||
try:
|
||
acc = AnimControlCollection()
|
||
autoBind(np.node(), acc, ~0)
|
||
for i in range(acc.getNumAnims()):
|
||
name = acc.getAnimName(i) or f"anim_{i}"
|
||
control = acc.getAnim(i)
|
||
if not control:
|
||
continue
|
||
if name in controls:
|
||
try:
|
||
old_frames = controls[name].getNumFrames()
|
||
except Exception:
|
||
old_frames = -1
|
||
try:
|
||
new_frames = control.getNumFrames()
|
||
except Exception:
|
||
new_frames = -1
|
||
# 重名时保留帧数更多的控制,避免默认选到“看起来无效”的同名动画
|
||
if new_frames >= old_frames:
|
||
controls[name] = control
|
||
else:
|
||
controls[name] = control
|
||
except Exception:
|
||
pass
|
||
|
||
# 先在根节点尝试,再补扫 Character 节点
|
||
collect_controls(model_np)
|
||
character_nodes = model_np.findAllMatches("**/+Character")
|
||
for i in range(character_nodes.getNumPaths()):
|
||
collect_controls(character_nodes.getPath(i))
|
||
|
||
# autoBind 无结果时,尝试手动把 AnimBundle 绑定到 Character 的 PartBundle
|
||
if not controls and character_nodes.getNumPaths() > 0:
|
||
anim_bundle_nodes = model_np.findAllMatches("**/+AnimBundleNode")
|
||
subset = PartSubset()
|
||
for ci in range(character_nodes.getNumPaths()):
|
||
try:
|
||
character = character_nodes.getPath(ci).node()
|
||
for bi in range(character.getNumBundles()):
|
||
part_bundle = character.getBundle(bi)
|
||
for ai in range(anim_bundle_nodes.getNumPaths()):
|
||
try:
|
||
anim_bundle_node = anim_bundle_nodes.getPath(ai).node()
|
||
anim_bundle = anim_bundle_node.getBundle()
|
||
if not anim_bundle:
|
||
continue
|
||
anim_name = anim_bundle.getName() or anim_bundle_node.getName() or f"anim_{ai}"
|
||
control = part_bundle.bindAnim(anim_bundle, -1, subset)
|
||
if not control:
|
||
continue
|
||
if anim_name in controls:
|
||
try:
|
||
old_frames = controls[anim_name].getNumFrames()
|
||
except Exception:
|
||
old_frames = -1
|
||
try:
|
||
new_frames = control.getNumFrames()
|
||
except Exception:
|
||
new_frames = -1
|
||
if new_frames >= old_frames:
|
||
controls[anim_name] = control
|
||
else:
|
||
controls[anim_name] = control
|
||
except Exception:
|
||
continue
|
||
except Exception:
|
||
continue
|
||
|
||
if not controls:
|
||
return None
|
||
|
||
# 仅骨骼无几何体时会出现“能触发播放但场景不可见”
|
||
try:
|
||
has_geom = model_np.findAllMatches("**/+GeomNode").getNumPaths() > 0
|
||
except Exception:
|
||
has_geom = False
|
||
if not has_geom:
|
||
print(f"[Actor加载] {source_desc} autoBind 检测到动画但无可见几何体,尝试其他节点")
|
||
return None
|
||
|
||
print(f"[Actor加载] {source_desc} autoBind 检测到动画: {list(controls.keys())}")
|
||
return _BoundAnimationProxy(model_np, controls, owns_node=owns_node)
|
||
except Exception as e:
|
||
print(f"[Actor加载] {source_desc} autoBind 失败: {e}")
|
||
return None
|
||
|
||
def _try_create_actor_via_gltf_path(path):
|
||
"""针对部分 GLB/GLTF 文件,使用 gltf 插件强制加载动画后再创建 Actor。"""
|
||
lower_path = str(path).lower()
|
||
if not (lower_path.endswith(".glb") or lower_path.endswith(".gltf")):
|
||
return None
|
||
|
||
model_np = None
|
||
succeeded = False
|
||
try:
|
||
import gltf
|
||
from panda3d.core import Filename
|
||
|
||
panda_path = Filename.from_os_specific(path).get_fullpath()
|
||
os_path = Filename(panda_path).to_os_specific()
|
||
settings = gltf.GltfSettings(skip_animations=False)
|
||
model_root = gltf.load_model(os_path, settings)
|
||
if not model_root:
|
||
return None
|
||
|
||
model_np = NodePath(model_root)
|
||
actor = _try_create_actor_from_source(model_np, f"GLTF专用加载({path})")
|
||
if actor:
|
||
succeeded = True
|
||
return actor
|
||
|
||
proxy = _try_create_autobind_proxy(model_np, f"GLTF专用加载({path})", owns_node=True)
|
||
if proxy:
|
||
proxy.reparentTo(self._get_owner_parent_node(owner_model))
|
||
proxy.hide()
|
||
succeeded = True
|
||
return proxy
|
||
except Exception as e:
|
||
print(f"[Actor加载] GLTF专用加载失败 ({path}): {e}")
|
||
finally:
|
||
# 仅在失败且本地临时节点仍存在时清理,避免泄漏
|
||
try:
|
||
if (not succeeded) and model_np and not model_np.isEmpty():
|
||
model_np.removeNode()
|
||
except Exception:
|
||
pass
|
||
return None
|
||
|
||
# 检查缓存(无效缓存自动清理,避免“无动画”结果被永久缓存)
|
||
cached_actor = self._actor_cache.get(owner_model)
|
||
if cached_actor:
|
||
# 若已获得模型路径,优先重建真实 Actor,避免长期停留在 autoBind 代理导致“能触发但不可见”
|
||
try:
|
||
owner_has_path = owner_model.hasTag("model_path") and bool(owner_model.getTag("model_path"))
|
||
except Exception:
|
||
owner_has_path = False
|
||
if owner_has_path and isinstance(cached_actor, _BoundAnimationProxy):
|
||
_cleanup_actor(cached_actor)
|
||
self._actor_cache.pop(owner_model, None)
|
||
cached_actor = None
|
||
|
||
if cached_actor:
|
||
try:
|
||
has_geom = False
|
||
if isinstance(cached_actor, _BoundAnimationProxy):
|
||
has_geom = self._node_has_geom(getattr(cached_actor, "_node", None))
|
||
else:
|
||
try:
|
||
has_geom = cached_actor.findAllMatches("**/+GeomNode").getNumPaths() > 0
|
||
except Exception:
|
||
has_geom = False
|
||
|
||
if (not cached_actor.isEmpty()) and cached_actor.getAnimNames() and has_geom:
|
||
return cached_actor
|
||
except Exception:
|
||
pass
|
||
_cleanup_actor(cached_actor)
|
||
self._actor_cache.pop(owner_model, None)
|
||
|
||
# 先检测模型树中的动画结构,并写入标签供属性面板快速判断
|
||
try:
|
||
character_nodes = owner_model.findAllMatches("**/+Character")
|
||
anim_bundle_nodes = owner_model.findAllMatches("**/+AnimBundleNode")
|
||
has_animation_nodes = (
|
||
character_nodes.getNumPaths() > 0 or
|
||
anim_bundle_nodes.getNumPaths() > 0
|
||
)
|
||
owner_model.setTag("has_animations", "true" if has_animation_nodes else "false")
|
||
if has_animation_nodes:
|
||
owner_model.setTag("can_create_actor_from_memory", "true")
|
||
except Exception:
|
||
has_animation_nodes = False
|
||
|
||
def _try_memory_fallback():
|
||
def _collect_autobind_source_candidates():
|
||
candidates = []
|
||
|
||
def add_candidate(node):
|
||
try:
|
||
if not node or node.isEmpty() or self._is_scene_root_node(node):
|
||
return
|
||
for existing in candidates:
|
||
if existing == node:
|
||
return
|
||
candidates.append(node)
|
||
except Exception:
|
||
return
|
||
|
||
add_candidate(owner_model)
|
||
add_candidate(origin_model)
|
||
add_candidate(self._prefer_owner_with_visible_geometry(origin_model, owner_model))
|
||
add_candidate(self._find_scene_model_owner(origin_model))
|
||
add_candidate(self._find_scene_model_owner(owner_model))
|
||
|
||
# 祖先链补齐
|
||
for seed in (origin_model, owner_model):
|
||
current = seed
|
||
for _ in range(48):
|
||
if not current or current.isEmpty() or self._is_scene_root_node(current):
|
||
break
|
||
add_candidate(current)
|
||
parent = current.getParent()
|
||
if not parent or parent.isEmpty() or parent == current:
|
||
break
|
||
current = parent
|
||
|
||
# scene_manager.models 补齐候选
|
||
scene_manager = getattr(self, "scene_manager", None)
|
||
models = getattr(scene_manager, "models", None) if scene_manager else None
|
||
if models:
|
||
for model in list(models):
|
||
add_candidate(model)
|
||
|
||
def score(node):
|
||
try:
|
||
has_geom = self._node_has_geom(node)
|
||
has_anim = self._node_has_animation_nodes(node)
|
||
has_path = (
|
||
(node.hasTag("model_path") and bool(node.getTag("model_path"))) or
|
||
(node.hasTag("saved_model_path") and bool(node.getTag("saved_model_path"))) or
|
||
(node.hasTag("original_path") and bool(node.getTag("original_path")))
|
||
)
|
||
related = False
|
||
try:
|
||
related = (
|
||
node == owner_model or
|
||
node == origin_model or
|
||
node.isAncestorOf(origin_model) or
|
||
origin_model.isAncestorOf(node) or
|
||
node.isAncestorOf(owner_model) or
|
||
owner_model.isAncestorOf(node)
|
||
)
|
||
except Exception:
|
||
related = False
|
||
|
||
s = 0
|
||
s += 220 if has_geom and has_anim else 0
|
||
s += 120 if has_geom else 0
|
||
s += 45 if has_anim else 0
|
||
s += 35 if has_path else 0
|
||
s += 40 if node == owner_model else 0
|
||
s += 30 if node == origin_model else 0
|
||
s += 25 if related else 0
|
||
return s
|
||
except Exception:
|
||
return -1
|
||
|
||
candidates.sort(key=score, reverse=True)
|
||
return candidates
|
||
|
||
can_create_from_memory = False
|
||
if owner_model.hasTag("can_create_actor_from_memory"):
|
||
can_create_from_memory = owner_model.getTag("can_create_actor_from_memory").lower() == "true"
|
||
if not can_create_from_memory and has_animation_nodes:
|
||
can_create_from_memory = True
|
||
|
||
if can_create_from_memory:
|
||
# 不能直接 Actor(owner_model);会污染当前场景节点,导致播放后模型消失/选择失效。
|
||
# 先用副本创建真实 Actor,只有失败时才退回 autoBind 代理。
|
||
clone_parent = self._get_owner_parent_node(owner_model)
|
||
clone_np = None
|
||
try:
|
||
clone_np = owner_model.copyTo(clone_parent)
|
||
clone_np.setName(f"{owner_model.getName()}__anim_runtime")
|
||
mem_actor = _try_create_actor_from_source(clone_np, "内存模型副本")
|
||
if mem_actor:
|
||
self._actor_cache[owner_model] = mem_actor
|
||
owner_model.setTag("has_animations", "true")
|
||
return mem_actor
|
||
except Exception as e:
|
||
print(f"[Actor加载] 创建内存模型副本失败: {e}")
|
||
finally:
|
||
# _try_create_actor_from_source 失败时,清理临时副本
|
||
try:
|
||
if clone_np and not clone_np.isEmpty() and owner_model not in self._actor_cache:
|
||
clone_np.removeNode()
|
||
except Exception:
|
||
pass
|
||
|
||
# Actor 副本失败后,从多个候选节点中选择“带几何体”的 autoBind 源
|
||
for source_node in _collect_autobind_source_candidates():
|
||
mem_proxy = _try_create_autobind_proxy(
|
||
source_node,
|
||
f"内存模型({source_node.getName()})",
|
||
owns_node=False
|
||
)
|
||
if mem_proxy:
|
||
self._actor_cache[owner_model] = mem_proxy
|
||
owner_model.setTag("has_animations", "true")
|
||
if source_node != owner_model:
|
||
print(f"[Actor加载] 使用可见节点作为动画源: {owner_model.getName()} -> {source_node.getName()}")
|
||
return mem_proxy
|
||
return None
|
||
|
||
# 始终优先尝试从文件路径加载,因为底层 gltf 插件只有在加载文件时才能抽取完整的动画名称。
|
||
print(f"[Actor加载调试] 传入的 origin_model: {origin_model.getName() if origin_model else 'None'}")
|
||
print(f"[Actor加载调试] origin_model tags: {origin_model.getTags() if origin_model else 'None'}")
|
||
print(f"[Actor加载调试] 解析出的 owner_model: {owner_model.getName() if owner_model else 'None'}")
|
||
try:
|
||
print(f"[Actor加载调试] owner_model tags: {owner_model.getTags() if owner_model else 'None'}")
|
||
print(f"[Actor加载调试] owner_model path tag: {owner_model.getTag('model_path') if owner_model.hasTag('model_path') else 'MISSING'}")
|
||
except Exception as e:
|
||
print(f"[Actor加载调试] 获取 tags 异常: {e}")
|
||
|
||
filepath = owner_model.getTag("model_path") if owner_model.hasTag("model_path") else ""
|
||
if not filepath and owner_model.hasTag("original_path"):
|
||
filepath = owner_model.getTag("original_path")
|
||
|
||
print(f"[Actor加载调试] 获取到的 filepath: '{filepath}'")
|
||
if not filepath:
|
||
print(f"[Actor加载调试] filepath为空,触发 _try_memory_fallback()")
|
||
return _try_memory_fallback()
|
||
|
||
# 针对Actor加载,必须使用Panda3D规范的Unix风格路径,否则Windows绝对路径会导致加载彻底崩溃并返回空节点
|
||
panda_specific_path = ""
|
||
try:
|
||
from panda3d.core import Filename
|
||
panda_specific_path = Filename.from_os_specific(filepath).get_fullpath()
|
||
except:
|
||
panda_specific_path = filepath.replace('\\', '/')
|
||
|
||
candidate_paths = [panda_specific_path, filepath, os.path.normpath(filepath)]
|
||
|
||
# 处理 /d/... 这类路径在 Windows 上无法直接访问的问题 (补充兜底OS路径)
|
||
if filepath.startswith("/") and os.name == "nt":
|
||
parts = filepath.split("/")
|
||
if len(parts) > 2 and len(parts[1]) == 1:
|
||
win_path = f"{parts[1].upper()}:\\{os.path.join(*parts[2:])}" if parts[2:] else f"{parts[1].upper()}:\\"
|
||
candidate_paths.append(win_path)
|
||
candidate_paths.append(os.path.normpath(win_path))
|
||
|
||
# 尝试 Panda3D 路径标准化
|
||
try:
|
||
from scene import util
|
||
candidate_paths.append(util.normalize_model_path(filepath))
|
||
except Exception:
|
||
pass
|
||
|
||
# 在 Resources/models 中按文件名兜底查找
|
||
filename = os.path.basename(filepath)
|
||
if filename:
|
||
candidate_paths.append(str(Path(__file__).resolve().parents[2] / "Resources" / "models" / filename))
|
||
candidate_paths.append(str(Path(__file__).resolve().parents[2] / "Resources" / filename))
|
||
|
||
# 去重并优先使用真实存在的路径 (同时确保panda专属路径排在第一位尝试)
|
||
unique_paths = []
|
||
seen = set()
|
||
for p in candidate_paths:
|
||
if not p:
|
||
continue
|
||
key = os.path.normcase(os.path.normpath(p))
|
||
if key in seen:
|
||
continue
|
||
seen.add(key)
|
||
unique_paths.append(p)
|
||
|
||
# 过滤时注意如果是以 / 开头的 Panda 路径,os.path.exists 可能判断为 False,所以要额外豁免 panda_specific_path 保底加载
|
||
existing_paths = [p for p in unique_paths if os.path.exists(p) or p == panda_specific_path]
|
||
load_paths = existing_paths + [p for p in unique_paths if p not in existing_paths]
|
||
|
||
for p in load_paths:
|
||
print(f"[Actor加载验证] 正在尝试通过路径读取骨骼和动画文件: {p}")
|
||
actor = _try_create_actor_from_source(p, f"文件路径({p})")
|
||
if actor:
|
||
owner_model.setTag("model_path", p)
|
||
owner_model.setTag("has_animations", "true")
|
||
self._actor_cache[owner_model] = actor
|
||
return actor
|
||
|
||
# 标准 Actor 路径失败时,针对 GLTF/GLB 走插件加载兜底
|
||
actor = _try_create_actor_via_gltf_path(p)
|
||
if actor:
|
||
owner_model.setTag("model_path", p)
|
||
owner_model.setTag("has_animations", "true")
|
||
self._actor_cache[owner_model] = actor
|
||
return actor
|
||
|
||
# 路径 Actor 失败后,再尝试把文件作为普通模型加载并 autoBind
|
||
try:
|
||
loaded_model = self.loader.loadModel(p)
|
||
if loaded_model and not loaded_model.isEmpty():
|
||
proxy = _try_create_autobind_proxy(loaded_model, f"文件路径({p})", owns_node=True)
|
||
if proxy:
|
||
loaded_model.reparentTo(self.render)
|
||
loaded_model.hide()
|
||
owner_model.setTag("model_path", p)
|
||
owner_model.setTag("has_animations", "true")
|
||
self._actor_cache[owner_model] = proxy
|
||
return proxy
|
||
loaded_model.removeNode()
|
||
except Exception:
|
||
pass
|
||
|
||
# 所有创建路径失败时由内存加载进行兜底
|
||
return _try_memory_fallback()
|
||
|
||
|
||
def _getModelFormat(self, origin_model):
|
||
"""获取模型格式信息"""
|
||
filepath = origin_model.getTag("model_path") if origin_model.hasTag("model_path") else ""
|
||
if not filepath and origin_model.hasTag("original_path"):
|
||
filepath = origin_model.getTag("original_path")
|
||
if not filepath and origin_model.hasTag("saved_model_path"):
|
||
filepath = origin_model.getTag("saved_model_path")
|
||
if not filepath and origin_model.hasTag("file"):
|
||
filepath = origin_model.getTag("file")
|
||
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
|
||
|
||
format_upper = format_info.upper()
|
||
|
||
if format_upper.startswith("GLB") or format_upper.startswith("GLTF"):
|
||
# 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 ":" in name:
|
||
# 一些导入器会产生前缀:动作名
|
||
display_name = name.split(":")[-1]
|
||
elif "/" in name:
|
||
display_name = name.split("/")[-1]
|
||
|
||
elif format_upper.startswith("FBX"):
|
||
# FBX 格式可能需要特殊处理
|
||
if self._isLikelyBoneGroup(name):
|
||
# 检查是否是骨骼组而非动画
|
||
print(f"[警告] '{name}' 可能不是真正的动画序列,而是骨骼组")
|
||
display_name = f"⚠️ {name} (可能非动画)"
|
||
else:
|
||
display_name = name
|
||
|
||
elif format_upper.startswith("EGG") or format_upper.startswith("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:
|
||
valid_anims = 0
|
||
|
||
# 简化分析:只检查动画是否存在,不详细分析帧数
|
||
for anim_name in anim_names:
|
||
try:
|
||
control = actor.getAnimControl(anim_name)
|
||
if control and control.getNumFrames() > 1:
|
||
valid_anims += 1
|
||
except Exception:
|
||
# 忽略单个动画的分析错误,继续处理其他动画
|
||
continue
|
||
|
||
if valid_anims == 0:
|
||
return "⚠️ 无有效动画"
|
||
elif valid_anims < len(anim_names):
|
||
return f"⚠️ {valid_anims}/{len(anim_names)} 个有效"
|
||
else:
|
||
return f"✓ {valid_anims} 个动画"
|
||
|
||
except Exception as e:
|
||
# 简化错误处理
|
||
return "分析异常"
|
||
|
||
def _resolve_selected_animation_name(self, origin_model, actor, owner_model=None):
|
||
"""返回可播放的动画名;当当前选择无效时自动回退到首个动画并回写标签。"""
|
||
try:
|
||
anim_names = actor.getAnimNames() if actor else []
|
||
except Exception:
|
||
anim_names = []
|
||
|
||
if not anim_names:
|
||
return None
|
||
|
||
current_anim = origin_model.getPythonTag("selected_animation")
|
||
if owner_model and owner_model != origin_model and current_anim not in anim_names:
|
||
current_anim = owner_model.getPythonTag("selected_animation")
|
||
|
||
if current_anim not in anim_names:
|
||
current_anim = anim_names[0]
|
||
|
||
try:
|
||
origin_model.setPythonTag("selected_animation", current_anim)
|
||
if owner_model and owner_model != origin_model:
|
||
owner_model.setPythonTag("selected_animation", current_anim)
|
||
except Exception:
|
||
pass
|
||
|
||
return current_anim
|
||
|
||
def _should_swap_visibility_for_actor(self, owner_model, actor):
|
||
"""
|
||
是否需要“隐藏原模型/显示Actor”的切换。
|
||
当 actor 直接绑定到 owner_model 本体时,不应切换可见性,否则会导致选中节点瞬间丢失。
|
||
"""
|
||
if self._is_scene_root_node(owner_model):
|
||
# 绝不隐藏场景根节点
|
||
return False
|
||
|
||
try:
|
||
if isinstance(actor, _BoundAnimationProxy):
|
||
# 非 owns_node 代理直接绑定场景节点,不能做“隐藏原模型/显示Actor”切换
|
||
if not getattr(actor, "_owns_node", False):
|
||
return False
|
||
return getattr(actor, "_node", None) != owner_model
|
||
except Exception:
|
||
pass
|
||
|
||
try:
|
||
return actor != owner_model
|
||
except Exception:
|
||
return True
|
||
|
||
|
||
def _playAnimation(self, origin_model):
|
||
"""播放动画"""
|
||
try:
|
||
if hasattr(self, "render") and not self.render.isEmpty() and self.render.isHidden():
|
||
self.render.show()
|
||
except Exception:
|
||
pass
|
||
|
||
owner_model = self._resolve_animation_owner_model(origin_model)
|
||
actor = self._getActor(owner_model)
|
||
if not actor:
|
||
return
|
||
|
||
# 若仍在“无路径的内存代理”,播放前强制再尝试一次路径恢复并重建真实 Actor
|
||
if isinstance(actor, _BoundAnimationProxy):
|
||
owner_has_path = owner_model.hasTag("model_path") and bool(owner_model.getTag("model_path"))
|
||
if not owner_has_path:
|
||
recovered_path = self._recover_model_path_from_tags(owner_model) or self._recover_model_path_from_tags(origin_model)
|
||
if recovered_path:
|
||
try:
|
||
owner_model.setTag("model_path", recovered_path)
|
||
if (not owner_model.hasTag("original_path")) or (not owner_model.getTag("original_path")):
|
||
owner_model.setTag("original_path", recovered_path)
|
||
except Exception:
|
||
pass
|
||
self._clear_animation_cache(owner_model)
|
||
actor = self._getActor(owner_model)
|
||
if not actor:
|
||
return
|
||
|
||
is_scene_bound_proxy = isinstance(actor, _BoundAnimationProxy) and (not getattr(actor, "_owns_node", False))
|
||
|
||
original_world_pos = owner_model.getPos(self.render)
|
||
original_world_hpr = owner_model.getHpr(self.render)
|
||
original_world_scale = owner_model.getScale(self.render)
|
||
|
||
if not is_scene_bound_proxy:
|
||
actor.setPos(owner_model.getPos())
|
||
actor.setHpr(owner_model.getHpr())
|
||
actor.setScale(owner_model.getScale())
|
||
|
||
if self._should_swap_visibility_for_actor(owner_model, actor):
|
||
owner_model.hide()
|
||
else:
|
||
owner_model.show()
|
||
actor.show()
|
||
|
||
task_name = f"maintain_anim_pos_{id(actor)}"
|
||
|
||
# 维持 Actor 的世界变换,避免父节点层级差异导致位置抖动/漂移
|
||
def maintainWorldPosition(task):
|
||
try:
|
||
if not actor.isEmpty() and not owner_model.isEmpty():
|
||
actor.setPos(self.render, original_world_pos)
|
||
actor.setHpr(self.render, original_world_hpr)
|
||
actor.setScale(self.render, original_world_scale)
|
||
return task.cont
|
||
else:
|
||
return task.done
|
||
except:
|
||
return task.done
|
||
|
||
taskMgr.remove(task_name)
|
||
if not is_scene_bound_proxy:
|
||
taskMgr.add(maintainWorldPosition, task_name)
|
||
|
||
# 获取当前选中的动画
|
||
current_anim = self._resolve_selected_animation_name(origin_model, actor, owner_model)
|
||
if current_anim:
|
||
try:
|
||
actor.stop()
|
||
except Exception:
|
||
pass
|
||
actor.play(current_anim)
|
||
print(f"『动画播放』:{current_anim}")
|
||
|
||
def _pauseAnimation(self, origin_model):
|
||
"""暂停动画"""
|
||
owner_model = self._resolve_animation_owner_model(origin_model)
|
||
actor = self._getActor(owner_model)
|
||
if not actor:
|
||
return
|
||
|
||
if isinstance(actor, _BoundAnimationProxy):
|
||
owner_has_path = owner_model.hasTag("model_path") and bool(owner_model.getTag("model_path"))
|
||
if not owner_has_path:
|
||
recovered_path = self._recover_model_path_from_tags(owner_model) or self._recover_model_path_from_tags(origin_model)
|
||
if recovered_path:
|
||
try:
|
||
owner_model.setTag("model_path", recovered_path)
|
||
if (not owner_model.hasTag("original_path")) or (not owner_model.getTag("original_path")):
|
||
owner_model.setTag("original_path", recovered_path)
|
||
except Exception:
|
||
pass
|
||
self._clear_animation_cache(owner_model)
|
||
actor = self._getActor(owner_model)
|
||
if not actor:
|
||
return
|
||
|
||
is_scene_bound_proxy = isinstance(actor, _BoundAnimationProxy) and (not getattr(actor, "_owns_node", False))
|
||
if not is_scene_bound_proxy:
|
||
actor.setPos(self.render, owner_model.getPos(self.render))
|
||
actor.setHpr(self.render, owner_model.getHpr(self.render))
|
||
actor.setScale(self.render, owner_model.getScale(self.render))
|
||
|
||
# 隐藏原始模型,显示Actor
|
||
if self._should_swap_visibility_for_actor(owner_model, actor):
|
||
owner_model.hide()
|
||
else:
|
||
owner_model.show()
|
||
actor.show()
|
||
|
||
# 停止动画(保持当前姿势)
|
||
actor.stop()
|
||
print("『动画』暂停")
|
||
|
||
def _stopAnimation(self, origin_model):
|
||
"""停止动画"""
|
||
try:
|
||
if hasattr(self, "render") and not self.render.isEmpty() and self.render.isHidden():
|
||
self.render.show()
|
||
except Exception:
|
||
pass
|
||
|
||
owner_model = self._resolve_animation_owner_model(origin_model)
|
||
actor = self._getActor(owner_model)
|
||
if not actor:
|
||
return
|
||
|
||
# 停止动画
|
||
actor.stop()
|
||
|
||
# 获取当前选中的动画
|
||
current_anim = self._resolve_selected_animation_name(origin_model, actor, owner_model)
|
||
if current_anim and actor.getAnimControl(current_anim):
|
||
actor.getAnimControl(current_anim).pose(0)
|
||
|
||
# 隐藏Actor,显示原始模型
|
||
if self._should_swap_visibility_for_actor(owner_model, actor):
|
||
actor.hide()
|
||
owner_model.show()
|
||
|
||
# 移除位置维护任务
|
||
taskMgr.remove(f"maintain_anim_pos_{id(actor)}")
|
||
|
||
print("『动画』停止切换至原始模型")
|
||
|
||
def _loopAnimation(self, origin_model):
|
||
"""循环播放动画"""
|
||
try:
|
||
if hasattr(self, "render") and not self.render.isEmpty() and self.render.isHidden():
|
||
self.render.show()
|
||
except Exception:
|
||
pass
|
||
|
||
owner_model = self._resolve_animation_owner_model(origin_model)
|
||
actor = self._getActor(owner_model)
|
||
if not actor:
|
||
return
|
||
|
||
is_scene_bound_proxy = isinstance(actor, _BoundAnimationProxy) and (not getattr(actor, "_owns_node", False))
|
||
|
||
original_world_pos = owner_model.getPos(self.render)
|
||
original_world_hpr = owner_model.getHpr(self.render)
|
||
original_world_scale = owner_model.getScale(self.render)
|
||
|
||
if not is_scene_bound_proxy:
|
||
actor.setPos(owner_model.getPos())
|
||
actor.setHpr(owner_model.getHpr())
|
||
actor.setScale(owner_model.getScale())
|
||
|
||
if self._should_swap_visibility_for_actor(owner_model, actor):
|
||
owner_model.hide()
|
||
else:
|
||
owner_model.show()
|
||
actor.show()
|
||
|
||
task_name = f"maintain_anim_pos_{id(actor)}"
|
||
|
||
def maintainWorldPosition(task):
|
||
try:
|
||
if not actor.isEmpty() and not owner_model.isEmpty():
|
||
actor.setPos(self.render, original_world_pos)
|
||
actor.setHpr(self.render, original_world_hpr)
|
||
actor.setScale(self.render, original_world_scale)
|
||
return task.cont
|
||
else:
|
||
return task.done
|
||
except:
|
||
return task.done
|
||
|
||
taskMgr.remove(task_name)
|
||
if not is_scene_bound_proxy:
|
||
taskMgr.add(maintainWorldPosition, task_name)
|
||
|
||
# 获取当前选中的动画
|
||
current_anim = self._resolve_selected_animation_name(origin_model, actor, owner_model)
|
||
if current_anim:
|
||
try:
|
||
actor.stop()
|
||
except Exception:
|
||
pass
|
||
actor.loop(current_anim)
|
||
print(f"[动画] 循环: {current_anim}")
|
||
|
||
def _setAnimationSpeed(self, origin_model, speed):
|
||
"""设置动画播放速度"""
|
||
owner_model = self._resolve_animation_owner_model(origin_model)
|
||
actor = self._getActor(owner_model)
|
||
if not actor:
|
||
return
|
||
|
||
# 获取当前选中的动画
|
||
current_anim = self._resolve_selected_animation_name(origin_model, actor, owner_model)
|
||
if current_anim:
|
||
actor.setPlayRate(speed, current_anim)
|
||
print(f"[动画] 速度设为: {speed} ({current_anim})")
|
||
else:
|
||
# 兜底:尝试所有动画
|
||
anim_names = actor.getAnimNames()
|
||
for anim_name in anim_names:
|
||
actor.setPlayRate(speed, anim_name)
|
||
print(f"[动画] 速度设为: {speed} (所有动画)")
|
||
|
||
|
||
def _clear_animation_cache(self, node):
|
||
"""清除节点的动画缓存,当模型发生变化时调用"""
|
||
owner_node = self._resolve_animation_owner_model(node)
|
||
node.setPythonTag("cached_anim_info", None)
|
||
node.setPythonTag("cached_processed_names", None)
|
||
node.setPythonTag("animation", None) # 同时清除动画检测结果
|
||
|
||
# 如果Actor在缓存中,也需要清理
|
||
if owner_node in self._actor_cache:
|
||
actor = self._actor_cache[owner_node]
|
||
try:
|
||
# 清理相关任务
|
||
taskMgr.remove(f"maintain_anim_pos_{id(actor)}")
|
||
# 清理Actor
|
||
if not actor.isEmpty():
|
||
actor.cleanup()
|
||
actor.removeNode()
|
||
except Exception as e:
|
||
print(f"清理Actor缓存失败: {e}")
|
||
finally:
|
||
del self._actor_cache[owner_node]
|
||
print(f"[缓存清理] 清除节点 {owner_node.getName()} 的动画缓存")
|