fix: animation-only FBX assets loadable; clip data viewer
Pure-animation FBX (no meshes, skeleton clips only) was rejected at the cache layer: ParsedModel::Valid() required non-empty meshes, so Model::FromParsed/LoadModel returned null and the asset never made it into the cache. Animation assets are now first-class: - ParsedModel::Valid() = meshes or clips non-empty; parse failures (both empty) still rejected. Empty-mesh consumers already guarded (rendering skips empty mesh loop, mesh collider falls back to Box) - ParseFromDisk: fix null-deref when assimp returns a null scene - AssetCache::Find(path) + AssetManager::GetModelByPath for panels - Asset Browser hover card shows meshes/bones/clips for loaded models - Inspector model line flags "animation-only" when meshes == 0 - Animation section: collapsible clip data table (name/duration/ channel count) to verify bone-name matching of attached clips
This commit is contained in:
@@ -329,9 +329,23 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
|
||||
model != nullptr)
|
||||
{
|
||||
const auto& clips = model->GetClips();
|
||||
ImGui::Text("model: valid (%u clips, %u bones)",
|
||||
static_cast<unsigned>(clips.size()),
|
||||
static_cast<unsigned>(model->GetBoneCount()));
|
||||
if (model->GetMeshes().empty())
|
||||
{
|
||||
// 纯动画容器(无网格):不能作为渲染模型,提示
|
||||
// 挂到 MeshRenderer 没有视觉效果
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.2f, 1.0f),
|
||||
"model: animation-only "
|
||||
"(%u clips, %u bones, no meshes)",
|
||||
static_cast<unsigned>(clips.size()),
|
||||
model->GetBoneCount());
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui::Text("model: valid (%u meshes, %u clips, %u bones)",
|
||||
static_cast<unsigned>(model->GetMeshes().size()),
|
||||
static_cast<unsigned>(clips.size()),
|
||||
model->GetBoneCount());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -910,6 +924,45 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
|
||||
ImGui::Text("t: %.2f / %.2f s", anim->time,
|
||||
clips[anim->clipIndex].duration);
|
||||
}
|
||||
// 动画数据表:每个 clip 的时长与有效轨道数(attach 进来的
|
||||
// 外部动画在此核对骨骼匹配结果——轨道数明显偏少 = 骨骼
|
||||
// 命名不同源被跳过)
|
||||
if (ImGui::TreeNode("clip data"))
|
||||
{
|
||||
ImGui::Columns(3, nullptr, false);
|
||||
ImGui::SetColumnWidth(0, 150.0f);
|
||||
ImGui::SetColumnWidth(1, 90.0f);
|
||||
ImGui::TextUnformatted("name");
|
||||
ImGui::NextColumn();
|
||||
ImGui::TextUnformatted("dur (s)");
|
||||
ImGui::NextColumn();
|
||||
ImGui::TextUnformatted("channels");
|
||||
ImGui::NextColumn();
|
||||
ImGui::Separator();
|
||||
for (const auto& c : clips)
|
||||
{
|
||||
const bool curClip =
|
||||
&c == &clips[static_cast<size_t>(
|
||||
std::clamp(anim->clipIndex, 0,
|
||||
static_cast<int>(clips.size()) - 1))];
|
||||
if (curClip)
|
||||
{
|
||||
ImGui::TextColored(ImVec4(0.3f, 0.85f, 0.4f, 1.0f),
|
||||
">%s", c.name.c_str());
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui::TextUnformatted(c.name.c_str());
|
||||
}
|
||||
ImGui::NextColumn();
|
||||
ImGui::Text("%.2f", c.duration);
|
||||
ImGui::NextColumn();
|
||||
ImGui::Text("%u", static_cast<unsigned>(c.channels.size()));
|
||||
ImGui::NextColumn();
|
||||
}
|
||||
ImGui::Columns(1);
|
||||
ImGui::TreePop();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user