- 资产浏览器可视化与配色:面板重构(网格/列表/缩略图)、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 缓存—— 修大材质目录首次打开卡顿
379 lines
13 KiB
C++
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);
|
|
}
|
|
}
|