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:
+101
-27
@@ -16,11 +16,48 @@
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
#include <filesystem>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
#include <type_traits>
|
||||
|
||||
namespace
|
||||
{
|
||||
// 递归收集 assets/ 下模型容器文件(模型槽与动画 attach 槽共用;
|
||||
// 无法从扩展名区分"模型 fbx"与"纯动画 fbx",一并列出)。
|
||||
// 会话级缓存:目录条目几十量级,避免每帧递归扫描
|
||||
const std::vector<std::string>& AssetModelFiles()
|
||||
{
|
||||
static std::vector<std::string> s_Files;
|
||||
static bool s_Cached = false;
|
||||
if (!s_Cached)
|
||||
{
|
||||
s_Cached = true;
|
||||
namespace fs = std::filesystem;
|
||||
if (fs::exists("assets"))
|
||||
{
|
||||
for (const auto& entry :
|
||||
fs::recursive_directory_iterator("assets"))
|
||||
{
|
||||
if (!entry.is_regular_file())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
std::string ext = entry.path().extension().string();
|
||||
std::transform(ext.begin(), ext.end(), ext.begin(),
|
||||
[](unsigned char c)
|
||||
{ return std::tolower(c); });
|
||||
if (ext == ".gltf" || ext == ".glb" || ext == ".fbx" ||
|
||||
ext == ".obj")
|
||||
{
|
||||
s_Files.push_back(entry.path().generic_string());
|
||||
}
|
||||
}
|
||||
std::sort(s_Files.begin(), s_Files.end());
|
||||
}
|
||||
}
|
||||
return s_Files;
|
||||
}
|
||||
|
||||
// InputText 需要 char 缓冲;提交时写回 std::string
|
||||
bool InputTextString(const char* label, std::string& str)
|
||||
{
|
||||
@@ -202,31 +239,7 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
|
||||
// 模型资产槽(UE Details 资产槽语义):下拉列 assets/ 下全部
|
||||
// 模型文件,选中即 LoadModel 应用;已加载路径缓存去重复用
|
||||
{
|
||||
namespace fs = std::filesystem;
|
||||
static std::vector<std::string> s_ModelFiles;
|
||||
s_ModelFiles.clear();
|
||||
if (fs::exists("assets"))
|
||||
{
|
||||
for (const auto& entry :
|
||||
fs::recursive_directory_iterator("assets"))
|
||||
{
|
||||
if (!entry.is_regular_file())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
std::string ext = entry.path().extension().string();
|
||||
std::transform(ext.begin(), ext.end(), ext.begin(),
|
||||
[](unsigned char c)
|
||||
{ return std::tolower(c); });
|
||||
if (ext == ".gltf" || ext == ".glb" || ext == ".fbx" ||
|
||||
ext == ".obj")
|
||||
{
|
||||
s_ModelFiles.push_back(
|
||||
entry.path().generic_string());
|
||||
}
|
||||
}
|
||||
std::sort(s_ModelFiles.begin(), s_ModelFiles.end());
|
||||
}
|
||||
const auto& s_ModelFiles = AssetModelFiles();
|
||||
std::vector<const char*> opts;
|
||||
opts.push_back("<none>");
|
||||
for (const auto& f : s_ModelFiles)
|
||||
@@ -742,17 +755,78 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
|
||||
}
|
||||
const auto* mr = reg.try_get<Engine::ECS::MeshRendererComponent>(selected);
|
||||
// 句柄无效不进 GetModel(缓存对无效句柄 FATAL;手选 <none> 即无效)
|
||||
const Engine::Model* model =
|
||||
Engine::Model* model =
|
||||
(mr && mr->model.Valid()) ? ctx.assets->GetModel(mr->model)
|
||||
: nullptr;
|
||||
|
||||
// ---- 外部动画并入(自愈重放 + attach 槽)----
|
||||
// 重放:attachedAnimPaths 里未并入的源(重启后 Model 缓存是
|
||||
// 空白)幂等重 attach;解析失败的路径会话内不再重试
|
||||
if (model != nullptr)
|
||||
{
|
||||
static std::unordered_set<std::string> s_AttachFailed;
|
||||
for (const auto& p : anim->attachedAnimPaths)
|
||||
{
|
||||
const bool done = std::any_of(
|
||||
model->GetClips().begin(), model->GetClips().end(),
|
||||
[&](const Engine::AnimationClip& c)
|
||||
{ return c.sourcePath == p; });
|
||||
if (!done && !s_AttachFailed.contains(p))
|
||||
{
|
||||
const auto res =
|
||||
ctx.assets->AttachAnimationClips(mr->model, p);
|
||||
if (res.clipsAdded == 0)
|
||||
{
|
||||
s_AttachFailed.insert(p);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 依赖提示:动画作用在 MeshRenderer 的模型骨骼上
|
||||
if (mr == nullptr)
|
||||
{
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.2f, 1.0f),
|
||||
"! requires a MeshRenderer component");
|
||||
}
|
||||
else if (model != nullptr && model->HasAnimations())
|
||||
else if (model != nullptr)
|
||||
{
|
||||
// 动画资产槽:列全部模型容器文件(无法静态区分"模型
|
||||
// fbx"与"纯动画 fbx",一并列出,无 clip 的会警告跳过)
|
||||
const auto& files = AssetModelFiles();
|
||||
std::vector<const char*> opts;
|
||||
opts.push_back("<attach animation file...>");
|
||||
for (const auto& f : files)
|
||||
{
|
||||
opts.push_back(f.c_str());
|
||||
}
|
||||
int sel = 0;
|
||||
ImGui::SetNextItemWidth(-1.0f);
|
||||
if (ImGui::Combo("##anim_slot", &sel, opts.data(),
|
||||
static_cast<int>(opts.size())))
|
||||
{
|
||||
const std::string& path = files[static_cast<size_t>(sel) - 1];
|
||||
const auto res =
|
||||
ctx.assets->AttachAnimationClips(mr->model, path);
|
||||
if (res.clipsAdded > 0 &&
|
||||
std::none_of(anim->attachedAnimPaths.begin(),
|
||||
anim->attachedAnimPaths.end(),
|
||||
[&](const std::string& s)
|
||||
{ return s == path; }))
|
||||
{
|
||||
anim->attachedAnimPaths.push_back(path);
|
||||
}
|
||||
}
|
||||
// 已并入的源文件清单
|
||||
for (const auto& p : anim->attachedAnimPaths)
|
||||
{
|
||||
ImGui::BulletText("%s", std::filesystem::path(p)
|
||||
.filename()
|
||||
.string()
|
||||
.c_str());
|
||||
}
|
||||
}
|
||||
if (model != nullptr && model->HasAnimations())
|
||||
{
|
||||
const auto& clips = model->GetClips();
|
||||
std::vector<const char*> clipNames;
|
||||
|
||||
Reference in New Issue
Block a user