Files
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

379 lines
13 KiB
C++

//
// AssetBrowserPanel.cpp:项目资产浏览器实现。
//
// 条目视觉/拖拽/tooltip 单元住 AssetItemView(列表行与网格瓦片共用同一
// 套类别色 + 缩略图);本文件只负责:根切换 / 过滤 / 目录树 / 内容区
// 铺排 / 选中态 / 双击分派(打开或实例化)。
//
#include "AssetBrowserPanel.h"
#include "AssetItemView.h" // 列表行 / 网格瓦片 / 目录条目
#include "AssetKind.h" // EditorAsset::AssetKind / KindOfPath
#include "MaterialEditorPanel.h" // kMaterialDragPayload + 双击打开材质编辑器
#include "ControllerEditorPanel.h" // kControllerDragPayload + 双击打开控制器编辑器
#include <Engine/Core/Log.h>
#include <Engine/Serialization/SceneSerializer.h>
#include <imgui.h>
#include <algorithm>
#include <cctype>
#include <filesystem>
#include <utility>
#include <vector>
namespace {
// 固定根:下标与 Editor 无耦合,仅面板内部使用
constexpr const char* kRootNames[] = {"Assets", "Scenes"};
constexpr const char* kRootPaths[] = {"assets", "scenes"};
// 类型过滤(下标即身份):0 = All;其余对齐 AssetKind 顺序
constexpr const char* kTypeNames[] = {"All", "Models",
"Textures", "Scenes",
"Materials", "Controllers",
"Prefabs"};
bool PassFilter(int typeFilter, EditorAsset::AssetKind kind)
{
switch (typeFilter)
{
case 1: return kind == EditorAsset::AssetKind::Model;
case 2: return kind == EditorAsset::AssetKind::Texture;
case 3: return kind == EditorAsset::AssetKind::Scene;
case 4: return kind == EditorAsset::AssetKind::Material;
case 5: return kind == EditorAsset::AssetKind::AnimClip;
case 6: return kind == EditorAsset::AssetKind::Prefab;
default: return true;
}
}
// 相对路径规范化为正斜杠(AssetManager 键与场景文件统一 POSIX 风格)
std::string ToAssetPath(const std::filesystem::path& p)
{
std::string s = p.generic_string();
std::replace(s.begin(), s.end(), '\\', '/');
return s;
}
// 双击分派:材质/控制器 → 打开编辑器;模型/环境 → 缓存预热;
// 场景 → 提示走 File 菜单;预制体 → 快捷实例化(授权位姿落点)
void DispatchOpen(const EditorContext& ctx,
const std::filesystem::path& path,
EditorAsset::AssetKind kind)
{
const std::string assetPath = path.generic_string();
if (kind == EditorAsset::AssetKind::Material && ctx.materialEditor)
{
ctx.materialEditor->OpenAsset(assetPath);
}
else if (kind == EditorAsset::AssetKind::AnimClip && ctx.controllerEditor)
{
ctx.controllerEditor->Open(assetPath);
}
else if (kind == EditorAsset::AssetKind::Model && ctx.assets)
{
const auto ref = ctx.assets->LoadModel(assetPath);
ENGINE_INFO("AssetBrowser: model '{}' loaded (valid={})",
assetPath, ref.Valid());
}
else if (kind == EditorAsset::AssetKind::Texture && ctx.assets)
{
const auto ref = ctx.assets->LoadEnvironment(assetPath);
ENGINE_INFO("AssetBrowser: env '{}' loaded (valid={})",
assetPath, ref.Valid());
}
else if (kind == EditorAsset::AssetKind::Scene)
{
ENGINE_INFO("AssetBrowser: load scenes via File menu ({})",
assetPath);
}
else if (kind == EditorAsset::AssetKind::Prefab && ctx.scene && ctx.assets)
{
// 双击 = 快捷实例化(授权位姿落点,选中交给 Outliner 手动改);
// 拖拽实例化见 Outliner 落点
const auto root = Engine::Serialization::SceneSerializer::
InstantiatePrefab(*ctx.scene, *ctx.assets, assetPath);
ENGINE_INFO("AssetBrowser: prefab '{}' instantiated (valid={})",
assetPath, root != entt::null);
}
}
} // namespace
void AssetBrowserPanel::Draw(const EditorContext& ctx)
{
if (!ImGui::Begin("Content Browser"))
{
ImGui::End();
return;
}
// ---- 顶栏:根切换 + 类型过滤 + 名字过滤 ----
if (ImGui::BeginTabBar("##roots"))
{
for (int i = 0; i < 2; ++i)
{
if (ImGui::BeginTabItem(kRootNames[i]))
{
m_Root = i;
ImGui::EndTabItem();
}
}
ImGui::EndTabBar();
}
ImGui::SameLine();
ImGui::SetNextItemWidth(110.0f);
ImGui::Combo("##type", &m_TypeFilter, kTypeNames, 7);
ImGui::SameLine();
ImGui::SetNextItemWidth(140.0f);
ImGui::InputTextWithHint("##name", "filter", m_NameFilter,
sizeof(m_NameFilter));
ImGui::Separator();
const std::filesystem::path root(kRootPaths[m_Root]);
// 根切换 / 首帧:当前目录回根。前缀比较手写(path::starts_with
// 是 libstdc++ 11 才补齐的成员,本机 GCC 没有);补 '/' 分隔校验
// 防 "assets_x" 之类误匹配
const std::string rootStr = root.generic_string();
const std::string curStr = m_CurrentDir.generic_string();
const bool under = curStr == rootStr ||
(curStr.size() > rootStr.size() &&
curStr.compare(0, rootStr.size(), rootStr) == 0 &&
curStr[rootStr.size()] == '/');
if (m_CurrentDir.empty() || !under)
{
m_CurrentDir = root;
}
// ---- 顶栏下:面包屑(当前目录相对根路径 + 回根按钮)+ 视图切换 ----
if (ImGui::Button("Root"))
{
m_CurrentDir = root;
}
ImGui::SameLine();
std::string rel = std::filesystem::relative(m_CurrentDir, root)
.generic_string();
ImGui::TextDisabled("%s/%s", kRootNames[m_Root],
rel.empty() ? "" : rel.c_str());
// 视图切换(列表 / 网格)+ 网格瓦片尺寸
ImGui::SameLine();
const bool listMode = m_ViewMode == ViewMode::List;
if (ImGui::RadioButton("List", listMode))
{
m_ViewMode = ViewMode::List;
}
ImGui::SameLine();
if (ImGui::RadioButton("Grid", !listMode))
{
m_ViewMode = ViewMode::Grid;
}
if (m_ViewMode == ViewMode::Grid)
{
ImGui::SameLine();
ImGui::SetNextItemWidth(120.0f);
ImGui::SliderFloat("##tile", &m_TileSize, 48.0f, 160.0f, "%.0f px");
}
ImGui::Separator();
// ---- 左:目录树 | 右:当前目录内容(列表 / 网格)----
const float treeWidth = 180.0f;
ImGui::BeginChild("##tree", ImVec2(treeWidth, 0.0f),
ImGuiChildFlags_ResizeX | ImGuiChildFlags_Borders);
if (std::filesystem::exists(root))
{
// 根节点也画进树(可点击选中)
ImGui::SetNextItemOpen(true, ImGuiCond_Once);
if (ImGui::TreeNodeEx(kRootNames[m_Root],
ImGuiTreeNodeFlags_OpenOnArrow))
{
if (ImGui::IsItemClicked(ImGuiMouseButton_Left))
{
m_CurrentDir = root;
}
DrawDirTree(ctx, root);
ImGui::TreePop();
}
}
else
{
ImGui::TextDisabled("(no '%s' dir)", kRootPaths[m_Root]);
}
ImGui::EndChild();
ImGui::SameLine();
ImGui::BeginChild("##files", ImVec2(0.0f, 0.0f),
ImGuiChildFlags_Borders);
if (std::filesystem::exists(m_CurrentDir))
{
DrawContent(ctx, m_CurrentDir);
}
ImGui::EndChild();
ImGui::End();
}
void AssetBrowserPanel::DrawDirTree(const EditorContext& ctx,
const std::filesystem::path& dir)
{
for (const auto& entry : std::filesystem::directory_iterator(dir))
{
if (!entry.is_directory())
{
continue;
}
const std::string name = entry.path().filename().string();
// OpenOnArrow:单击行 = 选中(列表切换),箭头 = 折叠
const bool open = ImGui::TreeNodeEx(
name.c_str(), ImGuiTreeNodeFlags_OpenOnArrow |
ImGuiTreeNodeFlags_SpanFullWidth);
if (ImGui::IsItemClicked(ImGuiMouseButton_Left))
{
m_CurrentDir = entry.path();
}
if (open)
{
DrawDirTree(ctx, entry.path());
ImGui::TreePop();
}
}
}
void AssetBrowserPanel::DrawContent(const EditorContext& ctx,
const std::filesystem::path& dir)
{
namespace fs = std::filesystem;
// 收集:目录(不受类型过滤影响)+ 过滤后的文件;目录迭代序不稳定 → 排序
std::vector<fs::path> dirs;
std::vector<std::pair<fs::path, EditorAsset::AssetKind>> files;
const std::string filter = m_NameFilter;
std::error_code ec;
for (const auto& entry : fs::directory_iterator(dir, ec))
{
if (entry.is_directory())
{
dirs.push_back(entry.path());
continue;
}
const EditorAsset::AssetKind kind =
EditorAsset::KindOfPath(entry.path());
if (!PassFilter(m_TypeFilter, kind))
{
continue;
}
if (!filter.empty())
{
const std::string name = entry.path().filename().string();
if (name.find(filter) == std::string::npos)
{
continue;
}
}
files.emplace_back(entry.path(), kind);
}
std::sort(dirs.begin(), dirs.end(),
[](const fs::path& a, const fs::path& b)
{
return a.filename().string() < b.filename().string();
});
std::sort(files.begin(), files.end(),
[](const auto& a, const auto& b)
{
return a.first.filename().string() <
b.first.filename().string();
});
// 双击打开延迟到循环外:Immediate GUI 语义——绘制循环内不改世界状态
//(prefab 实例化会改场景,边遍历边打开会让后续条目的项状态错乱)
bool pendingOpen = false;
std::filesystem::path pendingOpenPath;
EditorAsset::AssetKind pendingOpenKind = EditorAsset::AssetKind::Other;
if (m_ViewMode == ViewMode::List)
{
for (const auto& d : dirs)
{
if (EditorAssetView::DrawFolderRow(d))
{
m_CurrentDir = d;
m_SelectedPath.clear();
}
}
for (const auto& [path, kind] : files)
{
const std::string assetPath = ToAssetPath(path);
const auto res = EditorAssetView::DrawRow(
ctx, path, assetPath == m_SelectedPath);
if (res.clicked)
{
m_SelectedPath = assetPath;
}
if (res.doubleClicked)
{
pendingOpen = true;
pendingOpenPath = path;
pendingOpenKind = kind;
}
}
}
else
{
// ---- 网格视图:固定列宽表格铺排(列数按可用宽度自适应)----
const float tile = m_TileSize;
const float cellW = tile + ImGui::GetStyle().CellPadding.x * 2.0f;
int cols = static_cast<int>(ImGui::GetContentRegionAvail().x / cellW);
if (cols < 1)
{
cols = 1;
}
if (ImGui::BeginTable("##grid", cols, ImGuiTableFlags_SizingFixedFit))
{
for (int i = 0; i < cols; ++i)
{
ImGui::TableSetupColumn("##c",
ImGuiTableColumnFlags_WidthFixed,
cellW);
}
for (const auto& d : dirs)
{
ImGui::TableNextColumn();
const std::string assetPath = ToAssetPath(d);
if (EditorAssetView::DrawFolderTile(
d, tile, assetPath == m_SelectedPath))
{
m_CurrentDir = d;
m_SelectedPath.clear();
}
}
for (const auto& [path, kind] : files)
{
ImGui::TableNextColumn();
const std::string assetPath = ToAssetPath(path);
const auto res = EditorAssetView::DrawTile(
ctx, path, tile, assetPath == m_SelectedPath);
if (res.clicked)
{
m_SelectedPath = assetPath;
}
if (res.doubleClicked)
{
pendingOpen = true;
pendingOpenPath = path;
pendingOpenKind = kind;
}
}
ImGui::EndTable();
}
}
if (pendingOpen)
{
DispatchOpen(ctx, pendingOpenPath, pendingOpenKind);
}
}