feat(animation): attach external animation files to a model

Animation-only FBX files (no mesh, clips only) could not be used:
clips are keyed by node index into the parsed model's node table, so
there was no path to get them onto a different model's skeleton.

Add AssetManager::AttachAnimationClips: parses the container with the
CPU-only path (no GPU upload -- empty meshes cannot build a Model),
remaps each clip's channels to the target model's node indices by
bone name, and appends to its clip list. Channels whose bone name is
absent are skipped and counted; clips with zero matching channels are
dropped. Same-source attach is idempotent via a new
AnimationClip::sourcePath marker; name clashes get the file stem
prefixed.

Inspector Animation block gains an attach slot (UE Details semantics)
listing model-container files plus a bullet list of attached sources;
already-attached paths are replayed idempotently when the entity is
selected, so clips survive restarts. The paths persist in the scene
(Animation.Attached); the clips themselves stay runtime state.

Limitations (documented, by design): same-skeleton retargeting only
-- bone names must match between source and target (same DCC export
or Mixamo-style per-animation downloads); cross-skeleton retargeting
is out of scope.
This commit is contained in:
2026-10-04 01:25:04 +08:00
parent 69671cf4eb
commit 90bf63e641
6 changed files with 235 additions and 27 deletions
@@ -236,6 +236,14 @@ namespace Engine::Serialization {
out << YAML::Key << "Loop" << YAML::Value << a->loop;
out << YAML::Key << "Speed" << YAML::Value << a->speed;
out << YAML::Key << "Active" << YAML::Value << a->active;
// 外部动画源(编辑器 attach 列表):clip 本体运行时并入
// Model 资产不落盘,加载端按此列表幂等重放
out << YAML::Key << "Attached" << YAML::Value << YAML::Flow
<< YAML::BeginSeq;
for (const auto& p : a->attachedAnimPaths) {
out << p;
}
out << YAML::EndSeq;
out << YAML::EndMap;
}
@@ -556,6 +564,12 @@ namespace Engine::Serialization {
a.loop = n["Loop"].as<bool>(a.loop);
a.speed = n["Speed"].as<f32>(a.speed);
a.active = n["Active"].as<bool>(true);
if (const YAML::Node att = n["Attached"]) {
for (const auto& it : att) {
a.attachedAnimPaths.push_back(
it.as<std::string>(""));
}
}
}
if (const YAML::Node n = comps["Camera"]) {