- 资产浏览器可视化与配色:面板重构(网格/列表/缩略图)、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 缓存—— 修大材质目录首次打开卡顿
506 lines
19 KiB
C++
506 lines
19 KiB
C++
//
|
||
// AssetItemView.cpp:资产条目绘制单元实现。
|
||
//
|
||
|
||
#include "AssetItemView.h"
|
||
|
||
#include "AssetKind.h"
|
||
#include "EditorColors.h"
|
||
#include "ThumbnailCache.h"
|
||
#include "panels/AssetBrowserPanel.h" // kPrefabDragPayload / kModelDragPayload
|
||
#include "panels/ControllerEditorPanel.h" // kControllerDragPayload
|
||
#include "panels/MaterialEditorPanel.h" // kMaterialDragPayload / kTextureDragPayload
|
||
|
||
#include <Engine/Renderer/Model.h>
|
||
#include <Engine/Renderer/MaterialAsset.h>
|
||
|
||
#include <imgui.h>
|
||
|
||
#include <algorithm>
|
||
#include <chrono>
|
||
#include <cstdio>
|
||
#include <ctime>
|
||
#include <set>
|
||
#include <string>
|
||
#include <unordered_map>
|
||
#include <utility>
|
||
|
||
namespace {
|
||
|
||
constexpr float kBoxRounding = 3.0f;
|
||
|
||
// 类别色压暗背板 + 描边(p0/p1 来自上一个 item rect)
|
||
void DrawBoxChrome(const ImVec2& p0, const ImVec2& p1,
|
||
const ImVec4& accent)
|
||
{
|
||
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||
dl->AddRectFilled(p0, p1,
|
||
ImGui::GetColorU32(ImVec4(accent.x * 0.25f,
|
||
accent.y * 0.25f,
|
||
accent.z * 0.25f, 1.0f)),
|
||
kBoxRounding);
|
||
dl->AddRect(p0, p1, ImGui::GetColorU32(accent), kBoxRounding);
|
||
}
|
||
|
||
// 名字按可用宽度截断(UTF-8 边界安全)
|
||
std::string TruncateToWidth(const std::string& s, float maxW)
|
||
{
|
||
if (ImGui::CalcTextSize(s.c_str()).x <= maxW) return s;
|
||
std::string out = s;
|
||
const char* ell = "..";
|
||
while (!out.empty() &&
|
||
ImGui::CalcTextSize((out + ell).c_str()).x > maxW)
|
||
{
|
||
out.pop_back();
|
||
while (!out.empty() && (static_cast<unsigned char>(out.back()) &
|
||
0xC0) == 0x80)
|
||
{
|
||
out.pop_back(); // 退到 UTF-8 起始字节
|
||
}
|
||
}
|
||
return out + ell;
|
||
}
|
||
|
||
// 已尝试按需加载的材质资产路径:加载失败不再每帧重试(资产缓存接管
|
||
// 成功路径,重复调用是缓存命中)
|
||
std::set<std::string>& MaterialLoadTried()
|
||
{
|
||
static std::set<std::string> s;
|
||
return s;
|
||
}
|
||
|
||
// 材质加载闸门:大目录(上百个材质)逐个同步加载(.material 解析 +
|
||
// GPU 纹理上传——Texture 构造内含 submit + fence 阻塞等待,单个材质
|
||
// 可含多张大贴图)会顶爆单帧 → "卡成 PPT"。闸门 = 每帧至多 1 个 +
|
||
// 按上次实测耗时冷却(1 帧≈16ms;重载 ≈ 冷却相应帧数),把加载占用
|
||
// 压到 ≤ ~50% 帧预算,剩余帧时间还给渲染
|
||
struct MaterialLoadGate
|
||
{
|
||
bool Allow()
|
||
{
|
||
const int frame = ImGui::GetFrameCount();
|
||
return frame != m_LastLoadFrame && frame >= m_CooldownUntil;
|
||
}
|
||
void Charge(double elapsedMs)
|
||
{
|
||
const int frame = ImGui::GetFrameCount();
|
||
m_LastLoadFrame = frame;
|
||
const int cool = static_cast<int>(elapsedMs / 16.0);
|
||
m_CooldownUntil = frame + 1 + std::min(cool, 30);
|
||
}
|
||
private:
|
||
int m_LastLoadFrame = -1;
|
||
int m_CooldownUntil = 0;
|
||
};
|
||
|
||
MaterialLoadGate& MaterialLoad()
|
||
{
|
||
static MaterialLoadGate s_Gate;
|
||
return s_Gate;
|
||
}
|
||
|
||
// 已烘焙材质缩略图对应的 (serial, editVersion)。资产重载(新 serial)
|
||
// 或参数编辑(editVersion++)后与当前不符 = 缩略图过期,须作废重烘
|
||
//(否则浏览器永远停在旧图)——记录时机 = 发起烘焙请求那一帧
|
||
std::unordered_map<std::string, std::pair<Engine::u64, Engine::u32>>&
|
||
BakedIdentity()
|
||
{
|
||
static std::unordered_map<std::string,
|
||
std::pair<Engine::u64, Engine::u32>> s;
|
||
return s;
|
||
}
|
||
|
||
// 材质球缩略图全部走编辑器缩略图缓存:引擎把预览球渲染一次后 GPU→CPU
|
||
// 回读,App 注入缓存(与贴图缩略图同一条出图路径),浏览器不再占用
|
||
// 引擎预览 tile 池——大目录滚动/材质编辑器预览互不饿死
|
||
void DrawCachedThumb(const ThumbView& v, float size, const ImVec2& p0)
|
||
{
|
||
float fw = size, fh = size;
|
||
if (v.w > 0 && v.h > 0)
|
||
{
|
||
if (v.w >= v.h) fh = size * static_cast<float>(v.h) / v.w;
|
||
else fw = size * static_cast<float>(v.w) / v.h;
|
||
}
|
||
const ImVec2 fp0(p0.x + (size - fw) * 0.5f, p0.y + (size - fh) * 0.5f);
|
||
ImGui::GetWindowDrawList()->AddImage(
|
||
v.tex, fp0, ImVec2(fp0.x + fw, fp0.y + fh));
|
||
}
|
||
|
||
// 缩略图/占位块:一个命中项 + 绘制层。
|
||
// interactive=true → InvisibleButton(带 ID,可承载拖拽源/点击;Dummy
|
||
// 无 ID,在其上 BeginDragDropSource 会断言);false → Dummy(行内小图
|
||
// 纯展示,不抢行 Selectable 的点击)。
|
||
// 出图时**不铺任何背板**——透明像素会被类别色染色;背板只在未出图时
|
||
// 作为占位(类别色压暗 + 短标签)。仅可见项才请求缩略图,离屏项不入
|
||
// 解码队列(大目录不再一次性排队,出图延迟可控)
|
||
void DrawThumbBox(const EditorContext& ctx,
|
||
const std::filesystem::path& path, float size,
|
||
EditorAsset::AssetKind kind, bool interactive)
|
||
{
|
||
const bool visible = ImGui::IsRectVisible(ImVec2(size, size));
|
||
if (interactive)
|
||
{
|
||
ImGui::InvisibleButton("##thumb", ImVec2(size, size));
|
||
}
|
||
else
|
||
{
|
||
ImGui::Dummy(ImVec2(size, size));
|
||
}
|
||
const ImVec2 p0 = ImGui::GetItemRectMin();
|
||
const ImVec2 p1 = ImGui::GetItemRectMax();
|
||
const ImVec4 accent = EditorColors::AssetColor(kind);
|
||
|
||
bool drawn = false;
|
||
if (visible && kind == EditorAsset::AssetKind::Material && ctx.assets)
|
||
{
|
||
// 材质资产缩略图 = 引擎烘焙的静态材质球(GPU→CPU 回读 → 缩略图
|
||
// 缓存;与贴图缩略图同一条出图路径)
|
||
auto& cache = EditorThumbnailCache::Get();
|
||
const std::string assetPath = path.generic_string();
|
||
|
||
// 当前材质身份:已加载才有;未加载时无法判新旧
|
||
Engine::Material* curMat = nullptr;
|
||
if (ctx.assets->IsMaterialLoaded(assetPath))
|
||
{
|
||
Engine::MaterialAsset* a =
|
||
ctx.assets->FindMaterialAsset(assetPath);
|
||
curMat = a ? a->GetMaterial() : nullptr;
|
||
}
|
||
|
||
const ThumbView v = cache.Peek(assetPath);
|
||
if (v.state == ThumbView::State::Ready)
|
||
{
|
||
// 失效检测:缓存缩略图对应的材质身份 vs 当前身份——资产
|
||
// 重载(新 serial,换贴图槽)/ 参数编辑(editVersion++)后
|
||
// 不符 = 过期,作废缓存让下一帧重烘(否则浏览器永远旧图)
|
||
bool stale = false;
|
||
if (curMat)
|
||
{
|
||
auto it = BakedIdentity().find(assetPath);
|
||
if (it != BakedIdentity().end() &&
|
||
(it->second.first != curMat->GetSerial() ||
|
||
it->second.second != curMat->GetEditVersion()))
|
||
{
|
||
stale = true;
|
||
}
|
||
}
|
||
if (!stale)
|
||
{
|
||
DrawCachedThumb(v, size, p0);
|
||
drawn = true;
|
||
}
|
||
else
|
||
{
|
||
cache.Invalidate(assetPath);
|
||
BakedIdentity().erase(assetPath);
|
||
}
|
||
}
|
||
else if (v.state != ThumbView::State::Pending && ctx.renderer)
|
||
{
|
||
if (curMat)
|
||
{
|
||
// 未烘焙/已失效:请求烘焙并登记当前身份(key 幂等
|
||
// keepalive,回读完成即进缓存)
|
||
ctx.renderer->RequestMaterialPreviewBake(
|
||
curMat, cache.BakeKeyForPath(assetPath));
|
||
BakedIdentity()[assetPath] = {
|
||
curMat->GetSerial(), curMat->GetEditVersion()};
|
||
}
|
||
else if (!MaterialLoadTried().count(assetPath) &&
|
||
MaterialLoad().Allow())
|
||
{
|
||
// 按需加载材质(闸门限流,避免大目录同帧卡顿)
|
||
MaterialLoadTried().insert(assetPath);
|
||
const auto t0 = std::chrono::steady_clock::now();
|
||
ctx.assets->LoadMaterialAsset(assetPath);
|
||
const double ms = std::chrono::duration<double,
|
||
std::milli>(std::chrono::steady_clock::now() - t0)
|
||
.count();
|
||
MaterialLoad().Charge(ms);
|
||
}
|
||
}
|
||
}
|
||
if (!drawn && visible && EditorAsset::IsThumbnailable(path))
|
||
{
|
||
const ThumbView v =
|
||
EditorThumbnailCache::Get().Request(path.generic_string());
|
||
if (v.state == ThumbView::State::Ready)
|
||
{
|
||
DrawCachedThumb(v, size, p0);
|
||
drawn = true;
|
||
}
|
||
}
|
||
if (!drawn)
|
||
{
|
||
// 占位:类别色压暗背板 + 描边;大框叠加短标签
|
||
DrawBoxChrome(p0, p1, accent);
|
||
if (size >= 28.0f)
|
||
{
|
||
const char* label = EditorAsset::KindShortLabel(kind);
|
||
const ImVec2 ts = ImGui::CalcTextSize(label);
|
||
ImGui::GetWindowDrawList()->AddText(
|
||
ImVec2(p0.x + (size - ts.x) * 0.5f,
|
||
p0.y + (size - ts.y) * 0.5f),
|
||
ImGui::GetColorU32(accent), label);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 文件夹命中项(黄色)+ 短标签
|
||
void DrawFolderBox(float size, bool interactive)
|
||
{
|
||
if (interactive)
|
||
{
|
||
ImGui::InvisibleButton("##dir", ImVec2(size, size));
|
||
}
|
||
else
|
||
{
|
||
ImGui::Dummy(ImVec2(size, size));
|
||
}
|
||
const ImVec2 p0 = ImGui::GetItemRectMin();
|
||
const ImVec2 p1 = ImGui::GetItemRectMax();
|
||
DrawBoxChrome(p0, p1, EditorColors::kFolder);
|
||
if (size >= 28.0f)
|
||
{
|
||
const ImVec2 ts = ImGui::CalcTextSize("DIR");
|
||
ImGui::GetWindowDrawList()->AddText(
|
||
ImVec2(p0.x + (size - ts.x) * 0.5f,
|
||
p0.y + (size - ts.y) * 0.5f),
|
||
ImGui::GetColorU32(EditorColors::kFolder), "DIR");
|
||
}
|
||
}
|
||
|
||
} // namespace
|
||
|
||
namespace EditorAssetView {
|
||
|
||
void DrawDragSource(const std::filesystem::path& path,
|
||
const std::string& name)
|
||
{
|
||
const EditorAsset::AssetKind kind = EditorAsset::KindOfPath(path);
|
||
const char* payload = nullptr;
|
||
switch (kind)
|
||
{
|
||
case EditorAsset::AssetKind::Material: payload = kMaterialDragPayload; break;
|
||
case EditorAsset::AssetKind::AnimClip: payload = kControllerDragPayload; break;
|
||
case EditorAsset::AssetKind::Prefab: payload = kPrefabDragPayload; break;
|
||
case EditorAsset::AssetKind::Model: payload = kModelDragPayload; break;
|
||
case EditorAsset::AssetKind::Texture: payload = kTextureDragPayload; break;
|
||
default: return;
|
||
}
|
||
if (ImGui::BeginDragDropSource())
|
||
{
|
||
const std::string assetPath = path.generic_string();
|
||
ImGui::SetDragDropPayload(payload, assetPath.c_str(),
|
||
assetPath.size() + 1);
|
||
ImGui::TextUnformatted(name.c_str());
|
||
ImGui::EndDragDropSource();
|
||
}
|
||
}
|
||
|
||
void DrawTooltip(const EditorContext& ctx,
|
||
const std::filesystem::path& path)
|
||
{
|
||
namespace fs = std::filesystem;
|
||
const std::string name = path.filename().string();
|
||
const std::string assetPath = path.generic_string();
|
||
const EditorAsset::AssetKind kind = EditorAsset::KindOfPath(path);
|
||
|
||
ImGui::BeginTooltip();
|
||
ImGui::TextUnformatted(name.c_str());
|
||
ImGui::Separator();
|
||
ImGui::Text("type: %s", EditorAsset::KindLabel(kind));
|
||
|
||
std::error_code ec;
|
||
const auto sz = fs::file_size(path, ec);
|
||
if (!ec)
|
||
{
|
||
char szBuf[32];
|
||
if (sz >= 1024ull * 1024ull)
|
||
{
|
||
std::snprintf(szBuf, sizeof(szBuf), "%.1f MB",
|
||
static_cast<double>(sz) / (1024.0 * 1024.0));
|
||
}
|
||
else
|
||
{
|
||
std::snprintf(szBuf, sizeof(szBuf), "%.1f KB",
|
||
static_cast<double>(sz) / 1024.0);
|
||
}
|
||
ImGui::Text("size: %s", szBuf);
|
||
}
|
||
|
||
const auto ftime = fs::last_write_time(path, ec);
|
||
if (!ec)
|
||
{
|
||
// file_clock → system_clock 近似转换(C++17 无官方转换)
|
||
const auto sctp = std::chrono::time_point_cast<
|
||
std::chrono::system_clock::duration>(
|
||
ftime - fs::file_time_type::clock::now() +
|
||
std::chrono::system_clock::now());
|
||
const std::time_t tt = std::chrono::system_clock::to_time_t(sctp);
|
||
char tmBuf[32];
|
||
std::strftime(tmBuf, sizeof(tmBuf), "%Y-%m-%d %H:%M",
|
||
std::localtime(&tt));
|
||
ImGui::Text("modified: %s", tmBuf);
|
||
}
|
||
|
||
if (ctx.assets)
|
||
{
|
||
if (kind == EditorAsset::AssetKind::Model ||
|
||
kind == EditorAsset::AssetKind::Texture)
|
||
{
|
||
const bool loaded =
|
||
(kind == EditorAsset::AssetKind::Model)
|
||
? ctx.assets->IsModelLoaded(assetPath)
|
||
: ctx.assets->IsEnvironmentLoaded(assetPath);
|
||
ImGui::Text("state: %s", loaded ? "loaded" : "not loaded");
|
||
}
|
||
else if (kind == EditorAsset::AssetKind::Prefab)
|
||
{
|
||
ImGui::TextDisabled(
|
||
"template — drag into scene / double-click");
|
||
}
|
||
if (kind == EditorAsset::AssetKind::Model)
|
||
{
|
||
if (const Engine::Model* m =
|
||
ctx.assets->GetModelByPath(assetPath);
|
||
m != nullptr)
|
||
{
|
||
ImGui::Text("meshes: %u, bones: %u, clips: %u",
|
||
static_cast<unsigned>(m->GetMeshes().size()),
|
||
m->GetBoneCount(),
|
||
static_cast<unsigned>(m->GetClips().size()));
|
||
}
|
||
}
|
||
}
|
||
ImGui::TextDisabled("%s", assetPath.c_str());
|
||
ImGui::EndTooltip();
|
||
}
|
||
|
||
ItemResult DrawRow(const EditorContext& ctx,
|
||
const std::filesystem::path& path, bool selected)
|
||
{
|
||
ItemResult res;
|
||
const std::string name = path.filename().string();
|
||
const EditorAsset::AssetKind kind = EditorAsset::KindOfPath(path);
|
||
|
||
ImGui::PushID(name.c_str());
|
||
|
||
// 内联小缩略图 / 类别色占位块(非交互:行点击归 Selectable)
|
||
DrawThumbBox(ctx, path, ImGui::GetTextLineHeight(), kind, false);
|
||
ImGui::SameLine();
|
||
|
||
// 名称(类别色提亮)+ 类别色 hover/选中高亮
|
||
ImGui::PushStyleColor(ImGuiCol_Text, EditorColors::AssetNameColor(kind));
|
||
ImGui::PushStyleColor(ImGuiCol_Header,
|
||
EditorColors::AssetHighlightColor(kind));
|
||
ImGui::PushStyleColor(ImGuiCol_HeaderHovered,
|
||
EditorColors::AssetHighlightColor(kind));
|
||
ImGui::PushStyleColor(ImGuiCol_HeaderActive,
|
||
EditorColors::AssetHighlightColor(kind));
|
||
res.clicked = ImGui::Selectable(name.c_str(), selected,
|
||
ImGuiSelectableFlags_AllowDoubleClick);
|
||
ImGui::PopStyleColor(4);
|
||
if (res.clicked && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left))
|
||
{
|
||
res.doubleClicked = true;
|
||
}
|
||
|
||
DrawDragSource(path, name);
|
||
if (ImGui::IsItemHovered())
|
||
{
|
||
DrawTooltip(ctx, path);
|
||
}
|
||
|
||
ImGui::PopID();
|
||
return res;
|
||
}
|
||
|
||
ItemResult DrawTile(const EditorContext& ctx,
|
||
const std::filesystem::path& path,
|
||
float tileSize, bool selected)
|
||
{
|
||
ItemResult res;
|
||
const std::string name = path.filename().string();
|
||
const EditorAsset::AssetKind kind = EditorAsset::KindOfPath(path);
|
||
const ImVec4 accent = EditorColors::AssetColor(kind);
|
||
|
||
ImGui::PushID(name.c_str());
|
||
ImGui::BeginGroup();
|
||
|
||
DrawThumbBox(ctx, path, tileSize, kind, true);
|
||
|
||
// 选中/hover 描边(叠在缩略图上)
|
||
const ImVec2 p0 = ImGui::GetItemRectMin();
|
||
const ImVec2 p1 = ImGui::GetItemRectMax();
|
||
const bool hovered = ImGui::IsItemHovered();
|
||
if (selected || hovered)
|
||
{
|
||
ImGui::GetWindowDrawList()->AddRect(
|
||
p0, p1, ImGui::GetColorU32(accent), kBoxRounding, 0,
|
||
selected ? 2.0f : 1.0f);
|
||
}
|
||
res.clicked = ImGui::IsItemClicked(ImGuiMouseButton_Left);
|
||
if (hovered && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left))
|
||
{
|
||
res.doubleClicked = true;
|
||
}
|
||
|
||
DrawDragSource(path, name);
|
||
if (ImGui::IsItemHovered())
|
||
{
|
||
DrawTooltip(ctx, path);
|
||
}
|
||
|
||
ImGui::PushStyleColor(ImGuiCol_Text, EditorColors::AssetNameColor(kind));
|
||
ImGui::TextUnformatted(TruncateToWidth(name, tileSize).c_str());
|
||
ImGui::PopStyleColor();
|
||
|
||
ImGui::EndGroup();
|
||
ImGui::PopID();
|
||
return res;
|
||
}
|
||
|
||
bool DrawFolderRow(const std::filesystem::path& dir)
|
||
{
|
||
const std::string dname = dir.filename().string();
|
||
ImGui::PushID(dname.c_str());
|
||
DrawFolderBox(ImGui::GetTextLineHeight(), false);
|
||
ImGui::SameLine();
|
||
ImGui::PushStyleColor(ImGuiCol_Text, EditorColors::kFolder);
|
||
const bool clicked = ImGui::Selectable(dname.c_str());
|
||
ImGui::PopStyleColor();
|
||
ImGui::PopID();
|
||
return clicked;
|
||
}
|
||
|
||
bool DrawFolderTile(const std::filesystem::path& dir, float tileSize,
|
||
bool selected)
|
||
{
|
||
const std::string dname = dir.filename().string();
|
||
ImGui::PushID(dname.c_str());
|
||
ImGui::BeginGroup();
|
||
|
||
DrawFolderBox(tileSize, true);
|
||
|
||
const ImVec2 p0 = ImGui::GetItemRectMin();
|
||
const ImVec2 p1 = ImGui::GetItemRectMax();
|
||
if (selected || ImGui::IsItemHovered())
|
||
{
|
||
ImGui::GetWindowDrawList()->AddRect(
|
||
p0, p1, ImGui::GetColorU32(EditorColors::kFolder), kBoxRounding,
|
||
0, selected ? 2.0f : 1.0f);
|
||
}
|
||
const bool clicked = ImGui::IsItemClicked(ImGuiMouseButton_Left);
|
||
|
||
ImGui::PushStyleColor(ImGuiCol_Text, EditorColors::kFolder);
|
||
ImGui::TextUnformatted(TruncateToWidth(dname, tileSize).c_str());
|
||
ImGui::PopStyleColor();
|
||
|
||
ImGui::EndGroup();
|
||
ImGui::PopID();
|
||
return clicked;
|
||
}
|
||
|
||
} // namespace EditorAssetView
|