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:
@@ -9,6 +9,8 @@
|
||||
#include <yaml-cpp/yaml.h>
|
||||
|
||||
#include <fstream>
|
||||
#include <filesystem>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace Engine::Assets {
|
||||
|
||||
@@ -124,6 +126,93 @@ namespace Engine::Assets {
|
||||
});
|
||||
}
|
||||
|
||||
AssetManager::AnimAttachResult AssetManager::AttachAnimationClips(
|
||||
ModelRef target, const std::string& animPath)
|
||||
{
|
||||
AnimAttachResult res;
|
||||
Model* tm = GetModel(target);
|
||||
if (tm == nullptr) {
|
||||
return res;
|
||||
}
|
||||
// 幂等:该源文件已并入过(自愈重放/重复 attach 直接返回)
|
||||
for (const auto& c : tm->GetClips()) {
|
||||
if (c.sourcePath == animPath) {
|
||||
return res;
|
||||
}
|
||||
}
|
||||
// 纯 CPU 解析:动画容器只要 nodes + clips,不建 GPU 资产
|
||||
//(meshes 空的文件过不了 Model 构建,也不需要构建)
|
||||
ParsedModel src = Model::ParseFromDisk(animPath);
|
||||
if (src.clips.empty()) {
|
||||
ENGINE_WARN("AttachAnimationClips: no clips in '{}'", animPath);
|
||||
return res;
|
||||
}
|
||||
|
||||
// 目标模型 骨骼节点名 → nodeIdx 重映射表
|
||||
std::unordered_map<std::string, u32> nameToIdx;
|
||||
nameToIdx.reserve(tm->GetNodes().size());
|
||||
for (u32 i = 0; i < tm->GetNodes().size(); ++i) {
|
||||
nameToIdx.emplace(tm->GetNodes()[i].name, i);
|
||||
}
|
||||
|
||||
const std::string stem =
|
||||
std::filesystem::path(animPath).stem().string();
|
||||
std::vector<AnimationClip> extra;
|
||||
extra.reserve(src.clips.size());
|
||||
for (auto& clip : src.clips) {
|
||||
AnimationClip out;
|
||||
out.name = clip.name;
|
||||
out.duration = clip.duration;
|
||||
out.tps = clip.tps;
|
||||
out.sourcePath = animPath;
|
||||
// 同名冲突(内嵌 clip 或其它源):源文件名做前缀保持可辨识
|
||||
for (const auto& c : tm->GetClips()) {
|
||||
if (c.name == out.name) {
|
||||
out.name = stem + "_" + out.name;
|
||||
break;
|
||||
}
|
||||
}
|
||||
for (auto& ch : clip.channels) {
|
||||
if (ch.positions.empty() && ch.rotations.empty() &&
|
||||
ch.scales.empty()) {
|
||||
continue; // 空轨道无用
|
||||
}
|
||||
if (ch.nodeIdx >= src.nodes.size()) {
|
||||
++res.channelsSkipped;
|
||||
continue;
|
||||
}
|
||||
const auto it = nameToIdx.find(src.nodes[ch.nodeIdx].name);
|
||||
if (it == nameToIdx.end()) {
|
||||
++res.channelsSkipped; // 骨骼名不同源
|
||||
continue;
|
||||
}
|
||||
AnimChannel remapped;
|
||||
remapped.nodeIdx = it->second;
|
||||
remapped.positions = std::move(ch.positions);
|
||||
remapped.rotations = std::move(ch.rotations);
|
||||
remapped.scales = std::move(ch.scales);
|
||||
out.channels.push_back(std::move(remapped));
|
||||
++res.channelsMatched;
|
||||
}
|
||||
// 全部轨道对不上 = 名字空间完全不同源,clip 不入表
|
||||
if (out.channels.empty()) {
|
||||
ENGINE_WARN("AttachAnimationClips: clip '{}' has no matching "
|
||||
"bones in target (skipped)", clip.name);
|
||||
continue;
|
||||
}
|
||||
extra.push_back(std::move(out));
|
||||
++res.clipsAdded;
|
||||
}
|
||||
if (!extra.empty()) {
|
||||
tm->AppendClips(std::move(extra));
|
||||
}
|
||||
ENGINE_INFO("AttachAnimationClips: '{}' -> {} clips "
|
||||
"({} channels matched, {} skipped)",
|
||||
animPath, res.clipsAdded, res.channelsMatched,
|
||||
res.channelsSkipped);
|
||||
return res;
|
||||
}
|
||||
|
||||
ModelRef AssetManager::CreatePlane(f32 halfSize, u8 r, u8 g, u8 b)
|
||||
{
|
||||
// 程序化资产复用同一缓存:键 = 伪路径 + 参数
|
||||
|
||||
Reference in New Issue
Block a user