Files
HelloVulkan/App/editor/panels/HierarchyPanel.cpp
T
atdunbg 84cf547704 资产浏览器可视化改造 + 材质缩略图 GPU 烘焙 + 纹理上传批式化
- 资产浏览器可视化与配色:面板重构(网格/列表/缩略图)、AssetKind 分类、
  EditorColors 统一配色、AssetItemView 抽出条目绘制;HierarchyPanel/
  MaterialEditorPanel 同步调整
- 材质缩略图改走 GPU 回读烘焙:MaterialPreviewPass 新增 bake pass
  (SDR atlas → host staging copyImageToBuffer)、Renderer 提供
  Request/PollMaterialPreviewBake(无 waitIdle,依赖 per-slot fence),
  Editor 每帧排空后注入 ThumbnailCache——脱离预览 tile 池,消除"滚出视野
  即失效/池耗尽"两问题
- 缩略图失效:BakedIdentity(serial, editVersion) 与当前材质身份比对,
  换贴图槽(重载新 serial)/调参数(editVersion++)不符即 Invalidate 重烘,
  修"修改材质后资产浏览器缩略图不更新"
- 材质加载闸门:每帧至多 1 个 + 按实测耗时冷却,替代固定 2/帧
- 纹理上传批式化:VulkanAssetUploader 新增路径纹理缓存(跨材质去重,
  同一贴图只解码/上传一次)+ 同批新纹理合并为单 cmd/单 submit/单 fence;
  Texture 抽出 RecordPixelsUpload/CreateDeferred/ReleasePendingStaging,
  析构兜底释放 staging;MaterialAsset 纹理所有权移交 uploader 缓存——
  修大材质目录首次打开卡顿
2026-10-18 20:05:29 +08:00

362 lines
14 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//
// HierarchyPanel.cpp:实体层级树实现(UE Outliner 语义)。
//
// 树形展示(根 = 无有效父链接的实体,子 = HierarchyComponent 挂载序)、
// 点击选中、拖拽改父/拖到空白区挂根(keepWorld = 挂载前后世界位姿
// 不变)、右键 Create Child / Detach / Delete(级联随葬,延迟销毁)。
//
#include "HierarchyPanel.h"
#include "AssetBrowserPanel.h" // kPrefabDragPayload(Content Browser 源)
#include "EditorColors.h" // 实体名/组件徽标配色
#include "Engine/ECS/Components.h"
#include "Engine/ECS/EntityHandle.h" // CreateEntity 返回完整 EntityHandle
#include "Engine/Serialization/SceneSerializer.h"
#include "Engine/Core/Log.h"
#include <imgui.h>
#include <cstring>
#include <filesystem>
#include <string>
#include <vector>
using namespace Engine;
namespace {
// 拖拽 payload 类型名(Editor 内部约定;实体位模式 4 字节)
constexpr const char* kHierPayload = "HIER_ENTITY";
// 实体显示名
std::string DisplayName(const entt::registry& reg, entt::entity e)
{
if (const auto* n = reg.try_get<Engine::ECS::NameComponent>(e);
n && !n->name.empty()) {
return n->name;
}
return "(unnamed)";
}
// Prefab 拖入实例化(树节点/空白区落点共用)。根实体挂在场景根、
// 保留授权位姿;返回根实体(失败 entt::null)
entt::entity InstantiatePrefabDrop(const EditorContext& ctx,
const char* path)
{
if (!ctx.scene || !ctx.assets || !path) {
return entt::null;
}
return Engine::Serialization::SceneSerializer::InstantiatePrefab(
*ctx.scene, *ctx.assets, path);
}
// 模型拖入落点:创建带 MeshRenderer 的单实体(名字 = 文件名
// stem,默认位姿;模型句柄 = LoadModel 磁盘路径)。拖到节点下 =
// SetParent keepWorld;空白区 = 场景根。加载失败不建空实体
entt::entity SpawnModelDrop(const EditorContext& ctx, const char* path)
{
if (!ctx.scene || !ctx.assets || !path) {
return entt::null;
}
const auto model = ctx.assets->LoadModel(path);
if (!model.Valid()) {
return entt::null;
}
std::string name =
std::filesystem::path(path).stem().string();
if (name.empty()) {
name = "model entity";
}
auto e = ctx.scene->CreateEntity(name);
e.AddComponent<Engine::ECS::MeshRendererComponent>().model = model;
ENGINE_INFO("Hierarchy: model entity '{}' created from '{}'",
name, path);
return static_cast<entt::entity>(e);
}
// 主组件 → 实体名颜色(优先级高者胜,用于 Outliner 可读性)
ImVec4 PrimaryColor(const entt::registry& reg, entt::entity e)
{
using namespace Engine::ECS;
if (reg.all_of<CameraComponent>(e)) return EditorColors::kCamera;
if (reg.all_of<LightComponent>(e)) return EditorColors::kLight;
if (reg.all_of<SkyboxComponent>(e) ||
reg.all_of<SkyLightComponent>(e)) return EditorColors::kSky;
if (reg.all_of<MeshRendererComponent>(e)) return EditorColors::kMesh;
if (reg.all_of<AnimationComponent>(e)) return EditorColors::kAnimB;
if (reg.all_of<ScriptComponent>(e)) return EditorColors::kScript;
if (reg.all_of<RigidBodyComponent>(e) ||
reg.all_of<CharacterControllerComponent>(e))
{
return EditorColors::kPhysics;
}
return EditorColors::kEntityNeutral;
}
// 实体名 → 合法文件名(路径分隔符/保留符净化;空名兜底)
std::string PrefabFileName(std::string name)
{
for (char& c : name) {
if (c == '/' || c == '\\' || c == ':' || c == '*' ||
c == '?' || c == '"' || c == '<' || c == '>' ||
c == '|') {
c = '_';
}
}
if (name.empty() || name == "(unnamed)") {
name = "prefab";
}
return name;
}
// 递归绘制单节点(含子树)。返回值无意义,选中写 selected
void DrawEntityNode(const EditorContext& ctx, entt::entity e,
entt::entity& selected)
{
auto& reg = ctx.scene->GetRegistry();
// 稳定行 ID(UUID):折叠态跨帧保持 + 与 Inspector PushID 隔离
u64 rowId = static_cast<u64>(static_cast<u32>(e));
if (const auto* id = reg.try_get<Engine::ECS::IdComponent>(e)) {
rowId = id->uuid.Value();
}
ImGui::PushID(reinterpret_cast<const void*>(rowId));
// 子表(过滤悬空 + 无 Name 的防御性兜底)
const auto* h = reg.try_get<Engine::ECS::HierarchyComponent>(e);
std::vector<entt::entity> kids;
if (h) {
for (entt::entity c : h->children) {
if (reg.valid(c) && reg.all_of<Engine::ECS::NameComponent>(c)) {
kids.push_back(c);
}
}
}
const std::string label = DisplayName(reg, e);
const ImGuiTreeNodeFlags flags =
ImGuiTreeNodeFlags_OpenOnArrow |
ImGuiTreeNodeFlags_SpanAvailWidth |
(kids.empty() ? ImGuiTreeNodeFlags_Leaf : 0) |
(selected == e ? ImGuiTreeNodeFlags_Selected : 0);
// 实体名按主组件着色(无组件 = 中性色)
ImGui::PushStyleColor(ImGuiCol_Text, PrimaryColor(reg, e));
const bool opened = ImGui::TreeNodeEx(label.c_str(), flags);
ImGui::PopStyleColor();
const bool toggled = ImGui::IsItemToggledOpen();
if (ImGui::IsItemClicked(ImGuiMouseButton_Left) && !toggled) {
selected = e;
}
// ---- 拖拽源:拖动本实体去改父 ----
if (ImGui::BeginDragDropSource()) {
const u32 bits = static_cast<u32>(entt::to_integral(e));
ImGui::SetDragDropPayload(kHierPayload, &bits, sizeof(bits));
ImGui::TextUnformatted(label.c_str());
ImGui::EndDragDropSource();
}
// ---- 拖放目标:挂到本实体下(keepWorld,世界位姿不变);
// Prefab 拖入 = 实例化子树后挂到本实体下(keepWorld 保持
// 授权世界位姿)----
if (ImGui::BeginDragDropTarget()) {
if (const ImGuiPayload* pl =
ImGui::AcceptDragDropPayload(kHierPayload)) {
u32 bits = 0;
std::memcpy(&bits, pl->Data, sizeof(bits));
const entt::entity dragged = static_cast<entt::entity>(bits);
// 拒挂自身/自身后代(SetParent 内部同样防环,这里提前
// 静默拦截,避免"看起来挂上了"的错觉)
if (dragged != e && !ctx.scene->IsDescendantOf(dragged, e)) {
ctx.scene->SetParent(dragged, e);
}
}
if (const ImGuiPayload* pl =
ImGui::AcceptDragDropPayload(kPrefabDragPayload)) {
if (const entt::entity root =
InstantiatePrefabDrop(ctx,
static_cast<const char*>(
pl->Data));
root != entt::null) {
ctx.scene->SetParent(root, e); // keepWorld 默认 true
selected = root;
}
}
if (const ImGuiPayload* pl =
ImGui::AcceptDragDropPayload(kModelDragPayload)) {
if (const entt::entity me = SpawnModelDrop(
ctx, static_cast<const char*>(pl->Data));
me != entt::null) {
ctx.scene->SetParent(me, e); // keepWorld 默认 true
selected = me;
}
}
ImGui::EndDragDropTarget();
}
// ---- 右键菜单 ----
if (ImGui::BeginPopupContextItem("hier_ctx")) {
if (ImGui::MenuItem("Create Child")) {
// keepWorld=true:新实体出现在父的位置(UE attach 语义)
const auto child = ctx.scene->CreateEntity("child entity");
ctx.scene->SetParent(child, e);
selected = child;
}
if (ImGui::MenuItem("Save As Prefab")) {
// 固定目录 + 实体名派生文件名(无文件对话框库,与场景
// File 菜单同策略;重名覆盖,改名走文件系统)
const std::string path =
"assets/prefabs/" +
PrefabFileName(DisplayName(reg, e)) + ".prefab";
std::error_code ec;
std::filesystem::create_directories("assets/prefabs", ec);
if (Serialization::SceneSerializer::SerializePrefab(
*ctx.scene, *ctx.assets, e, path)) {
ENGINE_INFO("Hierarchy: prefab saved to '{}'", path);
}
}
if (ctx.scene->GetParent(e) != entt::null &&
ImGui::MenuItem("Detach to Root")) {
ctx.scene->SetParent(e, entt::null);
}
if (ImGui::MenuItem("Delete")) {
// 级联随葬:选中若是被删子树的一部分,一并清除
const bool selInSubtree = ctx.scene->IsDescendantOf(selected, e);
ctx.scene->DestroyEntity(e);
if (selInSubtree) {
selected = entt::null;
}
}
ImGui::EndPopup();
}
// ---- 组件徽标(逐组件着色,与行内同级显示)----
bool anyBadge = false;
auto badge = [&](const char* text, const ImVec4& col)
{
ImGui::SameLine(0.0f, 6.0f);
ImGui::PushStyleColor(ImGuiCol_Text, col);
ImGui::TextUnformatted(text);
ImGui::PopStyleColor();
anyBadge = true;
};
if (reg.all_of<Engine::ECS::MeshRendererComponent>(e))
badge("M", EditorColors::kMesh);
if (reg.all_of<Engine::ECS::AnimationComponent>(e))
badge("A", EditorColors::kAnimB);
if (reg.all_of<Engine::ECS::CameraComponent>(e))
badge("C", EditorColors::kCamera);
if (reg.all_of<Engine::ECS::LightComponent>(e))
badge("L", EditorColors::kLight);
if (reg.all_of<Engine::ECS::RigidBodyComponent>(e) ||
reg.all_of<Engine::ECS::CharacterControllerComponent>(e))
badge("P", EditorColors::kPhysics);
if (reg.all_of<Engine::ECS::ScriptComponent>(e))
badge("S", EditorColors::kScript);
if (reg.all_of<Engine::ECS::SkyboxComponent>(e) ||
reg.all_of<Engine::ECS::SkyLightComponent>(e))
badge("K", EditorColors::kSky);
if (anyBadge && ImGui::IsItemHovered()) {
ImGui::SetTooltip(
"M=Mesh A=Animation C=Camera L=Light P=Physics "
"S=Script K=Sky");
}
if (opened) {
for (entt::entity c : kids) {
DrawEntityNode(ctx, c, selected);
}
ImGui::TreePop();
}
ImGui::PopID();
}
} // namespace
void HierarchyPanel::Draw(const EditorContext& ctx, entt::entity& selected)
{
ImGui::Begin("Outliner");
// 工具栏:创建实体(在遍历之前,不修改遍历中的注册表结构)。
// Place Actors 简化语义:常用类型一键带组件,其余 Add Component 补
if (ImGui::Button("+ Create"))
{
ImGui::OpenPopup("create_entity");
}
if (ImGui::BeginPopup("create_entity"))
{
if (ImGui::MenuItem("Empty"))
{
selected = ctx.scene->CreateEntity("Entity");
}
if (ImGui::MenuItem("Light"))
{
auto e = ctx.scene->CreateEntity("Light");
e.AddComponent<Engine::ECS::LightComponent>();
selected = e;
}
if (ImGui::MenuItem("Camera"))
{
auto e = ctx.scene->CreateEntity("Camera");
e.AddComponent<Engine::ECS::CameraComponent>();
selected = e;
}
if (ImGui::MenuItem("Environment"))
{
// UE 新关卡环境集简化:Skybox(背景)+ SkyLight(环境光)一体
auto e = ctx.scene->CreateEntity("Environment");
e.AddComponent<Engine::ECS::SkyboxComponent>();
e.AddComponent<Engine::ECS::SkyLightComponent>();
selected = e;
}
ImGui::EndPopup();
}
ImGui::Separator();
auto& reg = ctx.scene->GetRegistry();
// 树形遍历:根 = 无有效父链接的实体(creation 序),子按挂载序递归
reg.view<Engine::ECS::NameComponent>().each(
[&](const entt::entity e, const Engine::ECS::NameComponent&)
{
if (ctx.scene->GetParent(e) != entt::null) return; // 非根
DrawEntityNode(ctx, e, selected);
});
// ---- 空白区 = 挂根落点(UE Outliner 拖到空白 = 顶层);
// Prefab 拖入 = 实例化到场景根(授权位姿)----
ImVec2 rem = ImGui::GetContentRegionAvail();
if (rem.y > 4.0f) {
ImGui::InvisibleButton("##hier_drop_root", rem);
if (ImGui::BeginDragDropTarget()) {
if (const ImGuiPayload* pl =
ImGui::AcceptDragDropPayload(kHierPayload)) {
u32 bits = 0;
std::memcpy(&bits, pl->Data, sizeof(bits));
ctx.scene->SetParent(static_cast<entt::entity>(bits),
entt::null);
}
if (const ImGuiPayload* pl =
ImGui::AcceptDragDropPayload(kPrefabDragPayload)) {
if (const entt::entity root = InstantiatePrefabDrop(
ctx, static_cast<const char*>(pl->Data));
root != entt::null) {
selected = root;
}
}
if (const ImGuiPayload* pl =
ImGui::AcceptDragDropPayload(kModelDragPayload)) {
if (const entt::entity me = SpawnModelDrop(
ctx, static_cast<const char*>(pl->Data));
me != entt::null) {
selected = me;
}
}
ImGui::EndDragDropTarget();
}
}
ImGui::End();
}