资产浏览器可视化改造 + 材质缩略图 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 缓存—— 修大材质目录首次打开卡顿
This commit is contained in:
@@ -0,0 +1,505 @@
|
||||
//
|
||||
// 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
|
||||
@@ -0,0 +1,46 @@
|
||||
//
|
||||
// AssetItemView:资产条目的绘制单元(列表行 / 网格瓦片 / 目录 / 拖拽源 /
|
||||
// tooltip)。Content Browser 与材质纹理选择器共用同一套视觉与交互。
|
||||
//
|
||||
|
||||
#ifndef HELLOVULKAN_ASSETITEMVIEW_H
|
||||
#define HELLOVULKAN_ASSETITEMVIEW_H
|
||||
|
||||
#include "EditorContext.h"
|
||||
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
|
||||
namespace EditorAssetView {
|
||||
|
||||
// 单击 / 双击结果(调用方决定选中与打开语义)
|
||||
struct ItemResult {
|
||||
bool clicked = false;
|
||||
bool doubleClicked = false;
|
||||
};
|
||||
|
||||
// 拖拽源:按资产类别发出对应 payload(材质/控制器/预制体/模型/纹理)
|
||||
void DrawDragSource(const std::filesystem::path& path,
|
||||
const std::string& name);
|
||||
|
||||
// 统一 tooltip(类型/大小/修改时间/加载状态)
|
||||
void DrawTooltip(const EditorContext& ctx,
|
||||
const std::filesystem::path& path);
|
||||
|
||||
// 列表行:内联小缩略图 + 彩色短标签 + 类别色名称
|
||||
ItemResult DrawRow(const EditorContext& ctx,
|
||||
const std::filesystem::path& path, bool selected);
|
||||
|
||||
// 网格瓦片:缩略图/类别色占位块 + 名称(截断到瓦片宽)
|
||||
ItemResult DrawTile(const EditorContext& ctx,
|
||||
const std::filesystem::path& path,
|
||||
float tileSize, bool selected);
|
||||
|
||||
// 目录(文件夹色):返回是否被点击(选中/进入)
|
||||
bool DrawFolderRow(const std::filesystem::path& dir);
|
||||
bool DrawFolderTile(const std::filesystem::path& dir, float tileSize,
|
||||
bool selected);
|
||||
|
||||
} // namespace EditorAssetView
|
||||
|
||||
#endif //HELLOVULKAN_ASSETITEMVIEW_H
|
||||
@@ -0,0 +1,122 @@
|
||||
//
|
||||
// AssetKind.h:资产分类的单一事实来源(Content Browser / 编辑器共用)。
|
||||
//
|
||||
// 原先分类逻辑散在 AssetBrowserPanel.cpp 匿名 ns(Kind/KindOf/KindTag),
|
||||
// 现在上提为头文件:面板、拖拽源、缩略图缓存按同一份扩展名表分派。
|
||||
//
|
||||
|
||||
#ifndef HELLOVULKAN_ASSETKIND_H
|
||||
#define HELLOVULKAN_ASSETKIND_H
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
|
||||
namespace EditorAsset {
|
||||
|
||||
// 资产类别(按扩展名分派;同时是类型过滤的身份)
|
||||
enum class AssetKind {
|
||||
Model, Texture, Scene, Material, AnimClip, Prefab, Other
|
||||
};
|
||||
|
||||
// 统一小写扩展名(含点,如 ".png";无扩展名 = 空串)
|
||||
inline std::string LowerExt(const std::filesystem::path& p)
|
||||
{
|
||||
std::string ext = p.extension().string();
|
||||
std::transform(ext.begin(), ext.end(), ext.begin(),
|
||||
[](unsigned char c) { return std::tolower(c); });
|
||||
return ext;
|
||||
}
|
||||
|
||||
// 纹理扩展名表(单一事实来源:分类 / 缩略图 / 材质纹理槽选择器共用)。
|
||||
// 口径 = 引擎 Texture 可直接加载者:
|
||||
// stb_image 8-bit 系 png/jpg/tga/bmp/psd/gif/pnm
|
||||
// + .hdr(环境贴图,stbi_loadf / stbi_load 走内置 hdr→ldr)
|
||||
// + .dds(引擎 DdsLoader,BC 压缩直传)
|
||||
inline bool IsTexturePath(const std::filesystem::path& p)
|
||||
{
|
||||
static const char* kExts[] = {
|
||||
".png", ".jpg", ".jpeg", ".tga", ".bmp", ".psd",
|
||||
".gif", ".ppm", ".pgm", ".pnm", ".hdr", ".dds"};
|
||||
const std::string ext = LowerExt(p);
|
||||
for (const char* e : kExts)
|
||||
{
|
||||
if (ext == e) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// 扩展名 → 类别
|
||||
inline AssetKind KindOfPath(const std::filesystem::path& p)
|
||||
{
|
||||
const std::string ext = LowerExt(p);
|
||||
if (ext == ".gltf" || ext == ".glb" || ext == ".fbx" ||
|
||||
ext == ".obj")
|
||||
{
|
||||
return AssetKind::Model;
|
||||
}
|
||||
if (IsTexturePath(p))
|
||||
{
|
||||
return AssetKind::Texture;
|
||||
}
|
||||
if (ext == ".scene")
|
||||
{
|
||||
return AssetKind::Scene;
|
||||
}
|
||||
if (ext == ".material")
|
||||
{
|
||||
return AssetKind::Material;
|
||||
}
|
||||
if (ext == ".animctrl")
|
||||
{
|
||||
return AssetKind::AnimClip;
|
||||
}
|
||||
if (ext == ".prefab")
|
||||
{
|
||||
return AssetKind::Prefab;
|
||||
}
|
||||
return AssetKind::Other;
|
||||
}
|
||||
|
||||
// 全称标签(tooltip / 网格标题;替代原下标对齐数组)
|
||||
inline const char* KindLabel(AssetKind kind)
|
||||
{
|
||||
switch (kind)
|
||||
{
|
||||
case AssetKind::Model: return "Model";
|
||||
case AssetKind::Texture: return "Texture/Environment";
|
||||
case AssetKind::Scene: return "Scene";
|
||||
case AssetKind::Material: return "Material";
|
||||
case AssetKind::AnimClip: return "Controller";
|
||||
case AssetKind::Prefab: return "Prefab";
|
||||
default: return "File";
|
||||
}
|
||||
}
|
||||
|
||||
// 列表类型列的短标签(替代原 "[M]"/"[T]" 前缀)
|
||||
inline const char* KindShortLabel(AssetKind kind)
|
||||
{
|
||||
switch (kind)
|
||||
{
|
||||
case AssetKind::Model: return "MDL";
|
||||
case AssetKind::Texture: return "TEX";
|
||||
case AssetKind::Scene: return "SCN";
|
||||
case AssetKind::Material: return "MAT";
|
||||
case AssetKind::AnimClip: return "CTRL";
|
||||
case AssetKind::Prefab: return "PFB";
|
||||
default: return "FILE";
|
||||
}
|
||||
}
|
||||
|
||||
// 可解码为 8-bit 缩略图的扩展名:stbi_load 原生支持者(含 .hdr ——
|
||||
// stbi_load 内部走 hdr→ldr 色调映射)。仅 .dds 例外(BC 压缩块,需
|
||||
// 专用解码器)→ 占位块
|
||||
inline bool IsThumbnailable(const std::filesystem::path& p)
|
||||
{
|
||||
return IsTexturePath(p) && LowerExt(p) != ".dds";
|
||||
}
|
||||
|
||||
} // namespace EditorAsset
|
||||
|
||||
#endif //HELLOVULKAN_ASSETKIND_H
|
||||
@@ -12,6 +12,7 @@
|
||||
//
|
||||
|
||||
#include "Editor.h"
|
||||
#include "ThumbnailCache.h" // 缩略图缓存(每帧 BeginFrame)
|
||||
#include "panels/AssetBrowserPanel.h"
|
||||
#include "panels/HierarchyPanel.h"
|
||||
#include "panels/InspectorPanel.h"
|
||||
@@ -167,6 +168,24 @@ void Editor::TogglePause()
|
||||
|
||||
void Editor::Draw()
|
||||
{
|
||||
// 缩略图缓存推进(解码限量 / 上传就绪 / LRU 淘汰):ImGui::NewFrame
|
||||
// 已由框架 ImGuiLayer 完成,此处处于面板绘制之前
|
||||
EditorThumbnailCache::Get().BeginFrame();
|
||||
|
||||
// 材质球缩略图烘焙:排空引擎异步回读结果 → 注入缩略图缓存(浏览器
|
||||
// 材质条目随后从缓存出图,不再占用引擎预览 tile 池)
|
||||
if (m_Ctx.renderer)
|
||||
{
|
||||
Engine::u64 key = 0;
|
||||
std::vector<unsigned char> pixels;
|
||||
Engine::u32 w = 0, h = 0;
|
||||
while (m_Ctx.renderer->PollMaterialPreviewBake(key, pixels, w, h))
|
||||
{
|
||||
EditorThumbnailCache::Get().CompleteBake(
|
||||
key, pixels.data(), static_cast<int>(w), static_cast<int>(h));
|
||||
}
|
||||
}
|
||||
|
||||
// PIE 镜像同步(面板只读指针消费;权威 = GameFlow,此处适配)
|
||||
if (m_Ctx.flow)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
//
|
||||
// EditorColors.h:编辑器面板配色的单一事实来源。
|
||||
//
|
||||
// 资产浏览器条目、Outliner 实体名/组件徽标的着色集中在此:面板不再
|
||||
// 各自散落硬编码 ImVec4。ImVec4 构造为 constexpr(imgui.h),故可用
|
||||
// inline constexpr 常量。
|
||||
//
|
||||
|
||||
#ifndef HELLOVULKAN_EDITORCOLORS_H
|
||||
#define HELLOVULKAN_EDITORCOLORS_H
|
||||
|
||||
#include "AssetKind.h"
|
||||
|
||||
#include <imgui.h>
|
||||
|
||||
namespace EditorColors {
|
||||
|
||||
// ---- 资产类别色(列表标签 / 名称 / 网格瓦片 / 选中高亮)----
|
||||
inline constexpr ImVec4 kModel = ImVec4(0.36f, 0.62f, 0.90f, 1.0f); // 蓝
|
||||
inline constexpr ImVec4 kTexture = ImVec4(0.34f, 0.78f, 0.52f, 1.0f); // 绿
|
||||
inline constexpr ImVec4 kScene = ImVec4(0.62f, 0.50f, 0.90f, 1.0f); // 紫
|
||||
inline constexpr ImVec4 kMaterial = ImVec4(0.92f, 0.60f, 0.25f, 1.0f); // 橙
|
||||
inline constexpr ImVec4 kAnim = ImVec4(0.30f, 0.76f, 0.76f, 1.0f); // 青
|
||||
inline constexpr ImVec4 kPrefab = ImVec4(0.42f, 0.70f, 0.95f, 1.0f); // 亮蓝
|
||||
inline constexpr ImVec4 kOther = ImVec4(0.72f, 0.72f, 0.75f, 1.0f); // 灰
|
||||
inline constexpr ImVec4 kFolder = ImVec4(0.88f, 0.74f, 0.34f, 1.0f); // 黄
|
||||
|
||||
inline ImVec4 AssetColor(EditorAsset::AssetKind kind)
|
||||
{
|
||||
switch (kind)
|
||||
{
|
||||
case EditorAsset::AssetKind::Model: return kModel;
|
||||
case EditorAsset::AssetKind::Texture: return kTexture;
|
||||
case EditorAsset::AssetKind::Scene: return kScene;
|
||||
case EditorAsset::AssetKind::Material: return kMaterial;
|
||||
case EditorAsset::AssetKind::AnimClip: return kAnim;
|
||||
case EditorAsset::AssetKind::Prefab: return kPrefab;
|
||||
default: return kOther;
|
||||
}
|
||||
}
|
||||
|
||||
// 向白提亮(保可读性);t = 0 原色,1 全白
|
||||
inline ImVec4 Lighten(const ImVec4& c, float t)
|
||||
{
|
||||
return ImVec4(c.x + (1.0f - c.x) * t, c.y + (1.0f - c.y) * t,
|
||||
c.z + (1.0f - c.z) * t, c.w);
|
||||
}
|
||||
|
||||
// 名称色:类别色提亮(行/瓦片名用)
|
||||
inline ImVec4 AssetNameColor(EditorAsset::AssetKind kind)
|
||||
{
|
||||
return Lighten(AssetColor(kind), 0.40f);
|
||||
}
|
||||
|
||||
// 选中/hover 高亮底色:类别色压暗(ImGuiCol_Header/HeaderHovered)
|
||||
inline ImVec4 AssetHighlightColor(EditorAsset::AssetKind kind)
|
||||
{
|
||||
return ImVec4(AssetColor(kind).x * 0.42f, AssetColor(kind).y * 0.42f,
|
||||
AssetColor(kind).z * 0.42f, 1.0f);
|
||||
}
|
||||
|
||||
// ---- Outliner 组件徽标色 ----
|
||||
inline constexpr ImVec4 kMesh = ImVec4(0.36f, 0.72f, 0.95f, 1.0f);
|
||||
inline constexpr ImVec4 kAnimB = ImVec4(0.95f, 0.72f, 0.35f, 1.0f);
|
||||
inline constexpr ImVec4 kCamera = ImVec4(0.70f, 0.55f, 0.95f, 1.0f);
|
||||
inline constexpr ImVec4 kLight = ImVec4(0.96f, 0.86f, 0.36f, 1.0f);
|
||||
inline constexpr ImVec4 kPhysics = ImVec4(0.45f, 0.85f, 0.55f, 1.0f);
|
||||
inline constexpr ImVec4 kScript = ImVec4(0.90f, 0.55f, 0.76f, 1.0f);
|
||||
inline constexpr ImVec4 kSky = ImVec4(0.46f, 0.80f, 0.92f, 1.0f);
|
||||
// 无组件/兜底实体名色(近白)
|
||||
inline constexpr ImVec4 kEntityNeutral = ImVec4(0.86f, 0.86f, 0.90f, 1.0f);
|
||||
|
||||
} // namespace EditorColors
|
||||
|
||||
#endif //HELLOVULKAN_EDITORCOLORS_H
|
||||
@@ -12,6 +12,8 @@
|
||||
#ifndef HELLOVULKAN_MATERIALASSETUTIL_H
|
||||
#define HELLOVULKAN_MATERIALASSETUTIL_H
|
||||
|
||||
#include "AssetKind.h" // EditorAsset::IsTexturePath(纹理扩展名单一事实来源)
|
||||
|
||||
#include <Engine/Assets/AssetManager.h>
|
||||
#include <Engine/Core/Log.h>
|
||||
#include <Engine/ECS/Components.h>
|
||||
@@ -53,12 +55,7 @@ namespace EditorMaterialUtil {
|
||||
{
|
||||
continue;
|
||||
}
|
||||
std::string ext = entry.path().extension().string();
|
||||
std::transform(ext.begin(), ext.end(), ext.begin(),
|
||||
[](unsigned char c)
|
||||
{ return std::tolower(c); });
|
||||
if (ext == ".jpg" || ext == ".jpeg" || ext == ".png" ||
|
||||
ext == ".tga" || ext == ".bmp" || ext == ".hdr")
|
||||
if (EditorAsset::IsTexturePath(entry.path()))
|
||||
{
|
||||
s_Files.push_back(entry.path().generic_string());
|
||||
}
|
||||
|
||||
@@ -0,0 +1,341 @@
|
||||
//
|
||||
// ThumbnailCache.cpp:缩略图解碼 + 降采样 + ImGui 用户纹理上传 + LRU 淘汰。
|
||||
//
|
||||
// 生命周期(依赖 vendored ImGui 1.93.0-WIP 语义,与后端/主循环逐帧对齐):
|
||||
// 帧 N :Request 入队 → BeginFrame 解码 → ImTextureData::Create +
|
||||
// RegisterUserTexture(Status=WantCreate,TexID=Invalid)
|
||||
// 帧 N 末:UpdateTexturesEndFrame 把用户纹理并入 PlatformIO.Textures;
|
||||
// ImGui_ImplVulkan_RenderDrawData 内 UpdateTexture 建 VkImage +
|
||||
// 描述符,置 TexID、Status=OK
|
||||
// 帧 N+1:BeginFrame 见 TexID != Invalid → Ready(当帧注册、次帧出图)
|
||||
// 淘汰 :WantDestroyNextFrame=true → 后端 UnusedFrames>=ImageCount 时
|
||||
// 销毁并清 BackendUserData → 下一帧见 Destroyed/无 BackendUserData
|
||||
// → UnregisterUserTexture + IM_DELETE
|
||||
// 关机 :本单例是进程级 function-local static,析构晚于 main 内的
|
||||
// SandboxApp(含 ImGuiLayer)——ImGui_ImplVulkan_Shutdown 已对
|
||||
// RefCount==1 的用户纹理统一释放 GPU;析构仅 IM_DELETE(只
|
||||
// IM_FREE(Pixels),不触任何已销毁的 ImGui 上下文)。
|
||||
//
|
||||
|
||||
#include "ThumbnailCache.h"
|
||||
|
||||
#include <imgui_internal.h> // RegisterUserTexture / UnregisterUserTexture
|
||||
|
||||
#include <stb/stb_image.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr int kThumbMaxDim = 128; // 缩略图最长边
|
||||
constexpr size_t kMaxEntries = 256; // 缓存上限(≤ 描述符池)
|
||||
constexpr int kMaxDecodesPerFrame = 2; // 每帧最多解码张数
|
||||
constexpr long long kMaxSourcePixels = 4096ll * 4096ll; // 超大源图跳过
|
||||
constexpr int kReadyTimeoutFrames = 3; // 上传超时 → Failed
|
||||
|
||||
// box filter 等比降采样(RGBA8):输出像素在源块内最多取
|
||||
// kMaxSamplesPerAxis² 个采样点(按步长抽样)。大图降采样时避免对
|
||||
// 整块(如 4096² → 128² 时每像素 1024 次读)全量累加——那是单张
|
||||
// 百毫秒级的卡顿源
|
||||
void ResampleBoxRGBA8(const unsigned char* src, int sw, int sh,
|
||||
unsigned char* dst, int dw, int dh)
|
||||
{
|
||||
constexpr int kMaxSamplesPerAxis = 8;
|
||||
for (int y = 0; y < dh; ++y)
|
||||
{
|
||||
const int sy0 = y * sh / dh;
|
||||
int sy1 = (y + 1) * sh / dh;
|
||||
if (sy1 <= sy0) sy1 = sy0 + 1;
|
||||
const int syStep = std::max(
|
||||
1, (sy1 - sy0 + kMaxSamplesPerAxis - 1) / kMaxSamplesPerAxis);
|
||||
for (int x = 0; x < dw; ++x)
|
||||
{
|
||||
const int sx0 = x * sw / dw;
|
||||
int sx1 = (x + 1) * sw / dw;
|
||||
if (sx1 <= sx0) sx1 = sx0 + 1;
|
||||
const int sxStep = std::max(
|
||||
1,
|
||||
(sx1 - sx0 + kMaxSamplesPerAxis - 1) / kMaxSamplesPerAxis);
|
||||
unsigned int r = 0, g = 0, b = 0, a = 0, n = 0;
|
||||
for (int sy = sy0; sy < sy1; sy += syStep)
|
||||
{
|
||||
const unsigned char* row =
|
||||
src + static_cast<size_t>(sy) * sw * 4;
|
||||
for (int sx = sx0; sx < sx1; sx += sxStep)
|
||||
{
|
||||
const unsigned char* p =
|
||||
row + static_cast<size_t>(sx) * 4;
|
||||
r += p[0]; g += p[1]; b += p[2]; a += p[3]; ++n;
|
||||
}
|
||||
}
|
||||
unsigned char* q = dst + (static_cast<size_t>(y) * dw + x) * 4;
|
||||
q[0] = static_cast<unsigned char>(r / n);
|
||||
q[1] = static_cast<unsigned char>(g / n);
|
||||
q[2] = static_cast<unsigned char>(b / n);
|
||||
q[3] = static_cast<unsigned char>(a / n);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
EditorThumbnailCache& EditorThumbnailCache::Get()
|
||||
{
|
||||
static EditorThumbnailCache s_Cache;
|
||||
return s_Cache;
|
||||
}
|
||||
|
||||
EditorThumbnailCache::~EditorThumbnailCache()
|
||||
{
|
||||
// 仅回收 CPU 侧(见文件头注释)。勿调 UnregisterUserTexture——
|
||||
// 此刻 ImGui 上下文已随 ImGuiLayer 销毁
|
||||
for (auto& [path, e] : m_Entries)
|
||||
{
|
||||
if (e.tex) IM_DELETE(e.tex);
|
||||
}
|
||||
for (ImTextureData* tex : m_Retire) IM_DELETE(tex);
|
||||
}
|
||||
|
||||
void EditorThumbnailCache::BeginFrame()
|
||||
{
|
||||
++m_Frame;
|
||||
|
||||
DrainRetired();
|
||||
|
||||
// 就绪推进:注册后等后端上传(次帧可见)
|
||||
for (auto& [path, e] : m_Entries)
|
||||
{
|
||||
if (e.state != ThumbView::State::Pending || !e.tex) continue;
|
||||
++e.pendingFrames;
|
||||
if (e.tex->TexID != ImTextureID_Invalid)
|
||||
{
|
||||
e.state = ThumbView::State::Ready;
|
||||
}
|
||||
else if (e.pendingFrames > kReadyTimeoutFrames &&
|
||||
e.tex->Status == ImTextureStatus_Destroyed)
|
||||
{
|
||||
e.state = ThumbView::State::Failed;
|
||||
}
|
||||
}
|
||||
|
||||
// 限量解码(未解码 = tex 为空)
|
||||
int decodes = 0;
|
||||
while (decodes < kMaxDecodesPerFrame && !m_Queue.empty())
|
||||
{
|
||||
const std::string p = m_Queue.front();
|
||||
m_Queue.pop_front();
|
||||
auto it = m_Entries.find(p);
|
||||
if (it == m_Entries.end()) continue; // 已被淘汰
|
||||
if (it->second.tex) continue; // 已解码(防御)
|
||||
DecodeOne(p, it->second);
|
||||
++decodes;
|
||||
}
|
||||
|
||||
// 超上限 → 淘汰最久未用
|
||||
while (m_Entries.size() > kMaxEntries)
|
||||
{
|
||||
if (!RetireOne()) break; // 全在用:本帧放弃
|
||||
}
|
||||
}
|
||||
|
||||
ThumbView EditorThumbnailCache::Request(const std::string& assetPath)
|
||||
{
|
||||
ThumbView v;
|
||||
if (assetPath.empty())
|
||||
{
|
||||
v.state = ThumbView::State::Failed;
|
||||
return v;
|
||||
}
|
||||
|
||||
auto it = m_Entries.find(assetPath);
|
||||
if (it == m_Entries.end())
|
||||
{
|
||||
Entry e;
|
||||
e.lastUseFrame = m_Frame;
|
||||
m_Entries.emplace(assetPath, e);
|
||||
m_Queue.push_back(assetPath);
|
||||
return v; // Pending
|
||||
}
|
||||
|
||||
Entry& e = it->second;
|
||||
e.lastUseFrame = m_Frame;
|
||||
v.state = e.state;
|
||||
v.w = e.w;
|
||||
v.h = e.h;
|
||||
if (e.state == ThumbView::State::Ready && e.tex)
|
||||
{
|
||||
v.tex = e.tex->GetTexRef();
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
void EditorThumbnailCache::UploadRGBA8(const unsigned char* src, int sw, int sh,
|
||||
Entry& e)
|
||||
{
|
||||
// 等比缩放到最长边 ≤ kThumbMaxDim
|
||||
int dw = sw, dh = sh;
|
||||
if (sw >= sh && sw > kThumbMaxDim)
|
||||
{
|
||||
dw = kThumbMaxDim;
|
||||
dh = std::max(1, sh * kThumbMaxDim / sw);
|
||||
}
|
||||
else if (sh > sw && sh > kThumbMaxDim)
|
||||
{
|
||||
dh = kThumbMaxDim;
|
||||
dw = std::max(1, sw * kThumbMaxDim / sh);
|
||||
}
|
||||
|
||||
ImTextureData* tex = IM_NEW(ImTextureData)();
|
||||
tex->Create(ImTextureFormat_RGBA32, dw, dh);
|
||||
if (dw == sw && dh == sh)
|
||||
{
|
||||
std::memcpy(tex->GetPixels(), src, static_cast<size_t>(dw) * dh * 4);
|
||||
}
|
||||
else
|
||||
{
|
||||
ResampleBoxRGBA8(src, sw, sh,
|
||||
static_cast<unsigned char*>(tex->GetPixels()), dw, dh);
|
||||
}
|
||||
|
||||
ImGui::RegisterUserTexture(tex);
|
||||
tex->SetStatus(ImTextureStatus_WantCreate);
|
||||
|
||||
e.tex = tex;
|
||||
e.w = dw;
|
||||
e.h = dh;
|
||||
e.state = ThumbView::State::Pending;
|
||||
e.pendingFrames = 0;
|
||||
}
|
||||
|
||||
void EditorThumbnailCache::DecodeOne(const std::string& path, Entry& e)
|
||||
{
|
||||
int sw = 0, sh = 0, ch = 0;
|
||||
if (!stbi_info(path.c_str(), &sw, &sh, &ch) || sw <= 0 || sh <= 0 ||
|
||||
static_cast<long long>(sw) * sh > kMaxSourcePixels)
|
||||
{
|
||||
e.state = ThumbView::State::Failed;
|
||||
return;
|
||||
}
|
||||
|
||||
unsigned char* src = stbi_load(path.c_str(), &sw, &sh, &ch, 4);
|
||||
if (!src)
|
||||
{
|
||||
e.state = ThumbView::State::Failed;
|
||||
return;
|
||||
}
|
||||
|
||||
UploadRGBA8(src, sw, sh, e);
|
||||
stbi_image_free(src);
|
||||
e.lastUseFrame = m_Frame;
|
||||
}
|
||||
|
||||
ThumbView EditorThumbnailCache::Peek(const std::string& assetPath)
|
||||
{
|
||||
ThumbView v;
|
||||
auto it = m_Entries.find(assetPath);
|
||||
if (it == m_Entries.end())
|
||||
{
|
||||
v.state = ThumbView::State::Failed; // 缺省 = 未注入
|
||||
return v;
|
||||
}
|
||||
Entry& e = it->second;
|
||||
e.lastUseFrame = m_Frame;
|
||||
v.state = e.state;
|
||||
v.w = e.w;
|
||||
v.h = e.h;
|
||||
if (e.state == ThumbView::State::Ready && e.tex)
|
||||
{
|
||||
v.tex = e.tex->GetTexRef();
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
void EditorThumbnailCache::Invalidate(const std::string& assetPath)
|
||||
{
|
||||
auto it = m_Entries.find(assetPath);
|
||||
if (it == m_Entries.end()) return;
|
||||
if (it->second.tex)
|
||||
{
|
||||
// 交回回收流程(与 LRU 淘汰同一路径:WantDestroyNextFrame →
|
||||
// 后端释放 → DrainRetired 注销 + 回收)
|
||||
it->second.tex->WantDestroyNextFrame = true;
|
||||
m_Retire.push_back(it->second.tex);
|
||||
}
|
||||
m_Entries.erase(it);
|
||||
}
|
||||
|
||||
unsigned long long EditorThumbnailCache::BakeKeyForPath(const std::string& path)
|
||||
{
|
||||
auto it = m_PathToBakeKey.find(path);
|
||||
if (it != m_PathToBakeKey.end()) return it->second;
|
||||
const unsigned long long key = ++m_BakeKeyCounter;
|
||||
m_PathToBakeKey.emplace(path, key);
|
||||
m_BakeKeyToPath.emplace(key, path);
|
||||
return key;
|
||||
}
|
||||
|
||||
void EditorThumbnailCache::CompleteBake(unsigned long long key,
|
||||
const unsigned char* rgba, int w, int h)
|
||||
{
|
||||
auto it = m_BakeKeyToPath.find(key);
|
||||
if (it == m_BakeKeyToPath.end()) return; // 已注销(重复回读)
|
||||
const std::string path = it->second;
|
||||
m_BakeKeyToPath.erase(it);
|
||||
m_PathToBakeKey.erase(path);
|
||||
InjectRGBA8(path, rgba, w, h);
|
||||
}
|
||||
|
||||
void EditorThumbnailCache::InjectRGBA8(const std::string& path,
|
||||
const unsigned char* rgba, int w, int h)
|
||||
{
|
||||
if (!rgba || w <= 0 || h <= 0) return;
|
||||
Entry& e = m_Entries[path]; // 材质路径不经解码队列,缺省即新建
|
||||
if (e.tex) return; // 已有纹理(重复注入):忽略
|
||||
UploadRGBA8(rgba, w, h, e);
|
||||
e.lastUseFrame = m_Frame;
|
||||
}
|
||||
|
||||
bool EditorThumbnailCache::RetireOne()
|
||||
{
|
||||
auto worst = m_Entries.end();
|
||||
for (auto it = m_Entries.begin(); it != m_Entries.end(); ++it)
|
||||
{
|
||||
if (it->second.lastUseFrame == m_Frame) continue; // 本帧在用
|
||||
if (worst == m_Entries.end() ||
|
||||
it->second.lastUseFrame < worst->second.lastUseFrame)
|
||||
{
|
||||
worst = it;
|
||||
}
|
||||
}
|
||||
if (worst == m_Entries.end()) return false;
|
||||
|
||||
if (worst->second.tex)
|
||||
{
|
||||
worst->second.tex->WantDestroyNextFrame = true;
|
||||
m_Retire.push_back(worst->second.tex);
|
||||
}
|
||||
m_Entries.erase(worst);
|
||||
return true;
|
||||
}
|
||||
|
||||
void EditorThumbnailCache::DrainRetired()
|
||||
{
|
||||
for (size_t i = 0; i < m_Retire.size();)
|
||||
{
|
||||
ImTextureData* tex = m_Retire[i];
|
||||
// 后端销毁后清 BackendUserData 并置 Destroyed → 可安全回收
|
||||
if (tex->BackendUserData == nullptr &&
|
||||
tex->Status == ImTextureStatus_Destroyed)
|
||||
{
|
||||
ImGui::UnregisterUserTexture(tex);
|
||||
IM_DELETE(tex);
|
||||
m_Retire[i] = m_Retire.back();
|
||||
m_Retire.pop_back();
|
||||
}
|
||||
else
|
||||
{
|
||||
++i;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
//
|
||||
// ThumbnailCache.h:编辑器缩略图缓存(Content Browser / 材质纹理槽共用)。
|
||||
//
|
||||
// 逐帧限量 CPU 解码 + box 降采样 → ImGui 用户纹理(ImTextureData +
|
||||
// RegisterUserTexture)→ 后端建/传 VkImage。零引擎改动。
|
||||
//
|
||||
// 生命周期(详见 .cpp 注释):进程级静态单例,析构晚于 ImGuiLayer,
|
||||
// CPU 侧由静态析构回收、GPU 侧由 ImGui_ImplVulkan_Shutdown 回收。
|
||||
//
|
||||
|
||||
#ifndef HELLOVULKAN_THUMBNAILCACHE_H
|
||||
#define HELLOVULKAN_THUMBNAILCACHE_H
|
||||
|
||||
#include <imgui.h> // ImTextureData / ImTextureRef
|
||||
|
||||
#include <deque>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
// 一次请求的结果:State==Ready 时 tex 可用于 ImGui::Image(tex, size)
|
||||
struct ThumbView {
|
||||
enum class State { Pending, Ready, Failed };
|
||||
State state = State::Pending;
|
||||
ImTextureRef tex{};
|
||||
int w = 0; // 缩略图原始像素尺寸(按比例显示用)
|
||||
int h = 0;
|
||||
};
|
||||
|
||||
class EditorThumbnailCache {
|
||||
public:
|
||||
// 进程级单例(编辑器首帧 BeginFrame 时惰性构造)
|
||||
static EditorThumbnailCache& Get();
|
||||
|
||||
// 每帧一次:Editor::Draw 开头(ImGui::NewFrame 之后、面板绘制之前)
|
||||
void BeginFrame();
|
||||
|
||||
// 幂等请求:命中且就绪即返回;未命中则入队,本帧返回 Pending
|
||||
ThumbView Request(const std::string& assetPath);
|
||||
|
||||
// 只读查状态(**不**触发解码入队):缺省 → Failed。材质球烘焙用——
|
||||
// 材质路径不走 stbi 解码,注入前必须能区分"未注入"与"上传中"
|
||||
//(Pending 表示已注入、等后端建纹理)
|
||||
ThumbView Peek(const std::string& assetPath);
|
||||
|
||||
// 作废某条目的缓存纹理(材质重载/参数编辑后缩略图过期 → 重烘)。
|
||||
// 丢弃 CPU 记录、纹理交回回收流程;随后 Peek 缺省 → Failed
|
||||
void Invalidate(const std::string& assetPath);
|
||||
|
||||
// ---- 材质球烘焙(GPU 回读结果注入)----
|
||||
// path ↔ key 幂等映射:Renderer 只接受/回传 u64 令牌,App 侧登记
|
||||
unsigned long long BakeKeyForPath(const std::string& path);
|
||||
// 回读结果注入:RGBA8 像素(w×h)→ 缩略图纹理(内部等比降到最长边
|
||||
// ≤ kThumbMaxDim)。key 未登记则忽略
|
||||
void CompleteBake(unsigned long long key, const unsigned char* rgba,
|
||||
int w, int h);
|
||||
|
||||
EditorThumbnailCache(const EditorThumbnailCache&) = delete;
|
||||
EditorThumbnailCache& operator=(const EditorThumbnailCache&) = delete;
|
||||
|
||||
private:
|
||||
EditorThumbnailCache() = default;
|
||||
~EditorThumbnailCache();
|
||||
|
||||
struct Entry {
|
||||
ImTextureData* tex = nullptr; // 非拥有(删由回收流程)
|
||||
int w = 0;
|
||||
int h = 0;
|
||||
ThumbView::State state = ThumbView::State::Pending;
|
||||
unsigned long long lastUseFrame = 0;
|
||||
int pendingFrames = 0; // 注册后等待后端上传的帧数
|
||||
};
|
||||
|
||||
void DecodeOne(const std::string& path, Entry& e); // 解码→降采样→注册
|
||||
// 源像素(RGBA8,sw×sh)→ 等比降采样 → ImTextureData 注册(解码与
|
||||
// 烘焙回读注入共用同一条上传路径)
|
||||
static void UploadRGBA8(const unsigned char* src, int sw, int sh, Entry& e);
|
||||
void InjectRGBA8(const std::string& path, const unsigned char* rgba,
|
||||
int w, int h);
|
||||
bool RetireOne(); // 淘汰最久未用(返回是否淘汰)
|
||||
void DrainRetired(); // 后端已释放→回收
|
||||
|
||||
std::unordered_map<std::string, Entry> m_Entries;
|
||||
std::deque<std::string> m_Queue; // 待解码(重复请求命 entries,天然去重)
|
||||
std::vector<ImTextureData*> m_Retire; // 已标 WantDestroy,等后端释放
|
||||
unsigned long long m_Frame = 0;
|
||||
|
||||
// 材质球烘焙 path ↔ key 映射(幂等分配;回读完成即注销)
|
||||
unsigned long long m_BakeKeyCounter = 0;
|
||||
std::unordered_map<std::string, unsigned long long> m_PathToBakeKey;
|
||||
std::unordered_map<unsigned long long, std::string> m_BakeKeyToPath;
|
||||
};
|
||||
|
||||
#endif //HELLOVULKAN_THUMBNAILCACHE_H
|
||||
@@ -1,10 +1,17 @@
|
||||
//
|
||||
// AssetBrowserPanel.cpp:项目资产浏览器实现。
|
||||
//
|
||||
// 条目视觉/拖拽/tooltip 单元住 AssetItemView(列表行与网格瓦片共用同一
|
||||
// 套类别色 + 缩略图);本文件只负责:根切换 / 过滤 / 目录树 / 内容区
|
||||
// 铺排 / 选中态 / 双击分派(打开或实例化)。
|
||||
//
|
||||
|
||||
#include "AssetBrowserPanel.h"
|
||||
#include "MaterialEditorPanel.h" // kMaterialDragPayload + 双击打开材质编辑器
|
||||
#include "ControllerEditorPanel.h" // kControllerDragPayload + 双击打开控制器编辑器
|
||||
|
||||
#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>
|
||||
@@ -13,10 +20,8 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <ctime>
|
||||
#include <filesystem>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
@@ -25,79 +30,26 @@ namespace {
|
||||
constexpr const char* kRootNames[] = {"Assets", "Scenes"};
|
||||
constexpr const char* kRootPaths[] = {"assets", "scenes"};
|
||||
|
||||
// 类型过滤(下标即身份):0 = All
|
||||
// 类型过滤(下标即身份):0 = All;其余对齐 AssetKind 顺序
|
||||
constexpr const char* kTypeNames[] = {"All", "Models",
|
||||
"Textures", "Scenes",
|
||||
"Materials", "Controllers",
|
||||
"Prefabs"};
|
||||
|
||||
// 资产类别:按扩展名分派(过滤用)
|
||||
enum class Kind { Model, Texture, Scene, Material, AnimClip, Prefab,
|
||||
Other };
|
||||
|
||||
Kind KindOf(const std::filesystem::path& p)
|
||||
{
|
||||
std::string ext = p.extension().string();
|
||||
std::transform(ext.begin(), ext.end(), ext.begin(),
|
||||
[](unsigned char c) { return std::tolower(c); });
|
||||
if (ext == ".gltf" || ext == ".glb" || ext == ".fbx" ||
|
||||
ext == ".obj")
|
||||
{
|
||||
return Kind::Model;
|
||||
}
|
||||
if (ext == ".hdr" || ext == ".png" || ext == ".jpg" ||
|
||||
ext == ".jpeg")
|
||||
{
|
||||
return Kind::Texture;
|
||||
}
|
||||
if (ext == ".scene")
|
||||
{
|
||||
return Kind::Scene;
|
||||
}
|
||||
if (ext == ".material")
|
||||
{
|
||||
return Kind::Material;
|
||||
}
|
||||
if (ext == ".animctrl")
|
||||
{
|
||||
return Kind::AnimClip;
|
||||
}
|
||||
if (ext == ".prefab")
|
||||
{
|
||||
return Kind::Prefab;
|
||||
}
|
||||
return Kind::Other;
|
||||
}
|
||||
|
||||
bool PassFilter(int typeFilter, Kind kind)
|
||||
bool PassFilter(int typeFilter, EditorAsset::AssetKind kind)
|
||||
{
|
||||
switch (typeFilter)
|
||||
{
|
||||
case 1: return kind == Kind::Model;
|
||||
case 2: return kind == Kind::Texture;
|
||||
case 3: return kind == Kind::Scene;
|
||||
case 4: return kind == Kind::Material;
|
||||
case 5: return kind == Kind::AnimClip;
|
||||
case 6: return kind == Kind::Prefab;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// 列表条目前缀符号(ImGui 默认字体无图标字形,用 ASCII 标签)
|
||||
const char* KindTag(Kind kind)
|
||||
{
|
||||
switch (kind)
|
||||
{
|
||||
case Kind::Model: return "[M]";
|
||||
case Kind::Texture: return "[T]";
|
||||
case Kind::Scene: return "[S]";
|
||||
case Kind::Material: return "[Ma]";
|
||||
case Kind::AnimClip: return "[An]";
|
||||
case Kind::Prefab: return "[Pf]";
|
||||
default: return "[ ]";
|
||||
}
|
||||
}
|
||||
|
||||
// 相对路径规范化为正斜杠(AssetManager 键与场景文件统一 POSIX 风格)
|
||||
std::string ToAssetPath(const std::filesystem::path& p)
|
||||
{
|
||||
@@ -105,6 +57,50 @@ namespace {
|
||||
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)
|
||||
@@ -152,7 +148,7 @@ void AssetBrowserPanel::Draw(const EditorContext& ctx)
|
||||
m_CurrentDir = root;
|
||||
}
|
||||
|
||||
// ---- 顶栏下:面包屑(当前目录相对根路径 + 回根按钮)----
|
||||
// ---- 顶栏下:面包屑(当前目录相对根路径 + 回根按钮)+ 视图切换 ----
|
||||
if (ImGui::Button("Root"))
|
||||
{
|
||||
m_CurrentDir = root;
|
||||
@@ -162,9 +158,28 @@ void AssetBrowserPanel::Draw(const EditorContext& ctx)
|
||||
.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);
|
||||
@@ -194,7 +209,7 @@ void AssetBrowserPanel::Draw(const EditorContext& ctx)
|
||||
ImGuiChildFlags_Borders);
|
||||
if (std::filesystem::exists(m_CurrentDir))
|
||||
{
|
||||
DrawFileList(ctx, m_CurrentDir);
|
||||
DrawContent(ctx, m_CurrentDir);
|
||||
}
|
||||
ImGui::EndChild();
|
||||
|
||||
@@ -227,34 +242,25 @@ void AssetBrowserPanel::DrawDirTree(const EditorContext& ctx,
|
||||
}
|
||||
}
|
||||
|
||||
void AssetBrowserPanel::DrawFileList(const EditorContext& ctx,
|
||||
const std::filesystem::path& dir)
|
||||
void AssetBrowserPanel::DrawContent(const EditorContext& ctx,
|
||||
const std::filesystem::path& dir)
|
||||
{
|
||||
// 收集 + 过滤 + 排序(名字序;目录迭代序不稳定)
|
||||
struct Item
|
||||
{
|
||||
std::filesystem::path path;
|
||||
Kind kind;
|
||||
};
|
||||
std::vector<Item> items;
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
// 收集:目录(不受类型过滤影响)+ 过滤后的文件;目录迭代序不稳定 → 排序
|
||||
std::vector<fs::path> dirs;
|
||||
std::vector<std::pair<fs::path, EditorAsset::AssetKind>> files;
|
||||
const std::string filter = m_NameFilter;
|
||||
for (const auto& entry : std::filesystem::directory_iterator(dir))
|
||||
std::error_code ec;
|
||||
for (const auto& entry : fs::directory_iterator(dir, ec))
|
||||
{
|
||||
if (entry.is_directory())
|
||||
{
|
||||
// 子目录也进列表:单行点击进入(树/列表双向导航)
|
||||
const std::string dname = entry.path().filename().string();
|
||||
ImGui::PushID(dname.c_str());
|
||||
ImGui::TextUnformatted("[D]");
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Selectable(dname.c_str()))
|
||||
{
|
||||
m_CurrentDir = entry.path();
|
||||
}
|
||||
ImGui::PopID();
|
||||
dirs.push_back(entry.path());
|
||||
continue;
|
||||
}
|
||||
const Kind kind = KindOf(entry.path());
|
||||
const EditorAsset::AssetKind kind =
|
||||
EditorAsset::KindOfPath(entry.path());
|
||||
if (!PassFilter(m_TypeFilter, kind))
|
||||
{
|
||||
continue;
|
||||
@@ -267,191 +273,106 @@ void AssetBrowserPanel::DrawFileList(const EditorContext& ctx,
|
||||
continue;
|
||||
}
|
||||
}
|
||||
items.push_back({entry.path(), kind});
|
||||
files.emplace_back(entry.path(), kind);
|
||||
}
|
||||
std::sort(items.begin(), items.end(),
|
||||
[](const Item& a, const Item& b)
|
||||
std::sort(dirs.begin(), dirs.end(),
|
||||
[](const fs::path& a, const fs::path& b)
|
||||
{
|
||||
return a.path.filename().string() <
|
||||
b.path.filename().string();
|
||||
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();
|
||||
});
|
||||
|
||||
for (const auto& item : items)
|
||||
// 双击打开延迟到循环外:Immediate GUI 语义——绘制循环内不改世界状态
|
||||
//(prefab 实例化会改场景,边遍历边打开会让后续条目的项状态错乱)
|
||||
bool pendingOpen = false;
|
||||
std::filesystem::path pendingOpenPath;
|
||||
EditorAsset::AssetKind pendingOpenKind = EditorAsset::AssetKind::Other;
|
||||
|
||||
if (m_ViewMode == ViewMode::List)
|
||||
{
|
||||
const std::string name = item.path.filename().string();
|
||||
const std::string assetPath = ToAssetPath(item.path);
|
||||
ImGui::PushID(name.c_str());
|
||||
|
||||
ImGui::TextUnformatted(KindTag(item.kind));
|
||||
ImGui::SameLine();
|
||||
ImGui::Selectable(name.c_str());
|
||||
|
||||
// .material 拖拽源:拖到 Details 材质槽球上指派(kMaterialDragPayload)
|
||||
if (item.kind == Kind::Material && ImGui::BeginDragDropSource())
|
||||
for (const auto& d : dirs)
|
||||
{
|
||||
ImGui::SetDragDropPayload(kMaterialDragPayload,
|
||||
assetPath.c_str(), assetPath.size() + 1);
|
||||
ImGui::TextUnformatted(name.c_str());
|
||||
ImGui::EndDragDropSource();
|
||||
}
|
||||
|
||||
// .animctrl 拖拽源:拖到 Details Controller 槽指派
|
||||
//(kControllerDragPayload,对齐材质槽交互)
|
||||
if (item.kind == Kind::AnimClip && ImGui::BeginDragDropSource())
|
||||
{
|
||||
ImGui::SetDragDropPayload(kControllerDragPayload,
|
||||
assetPath.c_str(), assetPath.size() + 1);
|
||||
ImGui::TextUnformatted(name.c_str());
|
||||
ImGui::EndDragDropSource();
|
||||
}
|
||||
|
||||
// .prefab 拖拽源:拖到 Outliner(节点=挂其下 / 空白区=挂根)
|
||||
// 实例化整棵子树(kPrefabDragPayload)
|
||||
if (item.kind == Kind::Prefab && ImGui::BeginDragDropSource())
|
||||
{
|
||||
ImGui::SetDragDropPayload(kPrefabDragPayload,
|
||||
assetPath.c_str(), assetPath.size() + 1);
|
||||
ImGui::TextUnformatted(name.c_str());
|
||||
ImGui::EndDragDropSource();
|
||||
}
|
||||
|
||||
// 模型拖拽源:拖到 Outliner 落点创建带 MeshRenderer 的实体
|
||||
//(kModelDragPayload;实体名 = 文件名)
|
||||
if (item.kind == Kind::Model && ImGui::BeginDragDropSource())
|
||||
{
|
||||
ImGui::SetDragDropPayload(kModelDragPayload,
|
||||
assetPath.c_str(), assetPath.size() + 1);
|
||||
ImGui::TextUnformatted(name.c_str());
|
||||
ImGui::EndDragDropSource();
|
||||
}
|
||||
|
||||
// 双击:.material 打开 Material Editor;.animctrl 打开
|
||||
// Controller Editor;model/HDR 进缓存预热(路径去重,重复
|
||||
// 双击零成本);场景加载含环境恢复/选中清理流程,提示走
|
||||
// File 菜单
|
||||
if (ImGui::IsItemHovered() &&
|
||||
ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left))
|
||||
{
|
||||
if (item.kind == Kind::Material && ctx.materialEditor)
|
||||
if (EditorAssetView::DrawFolderRow(d))
|
||||
{
|
||||
ctx.materialEditor->OpenAsset(assetPath);
|
||||
}
|
||||
else if (item.kind == Kind::AnimClip && ctx.controllerEditor)
|
||||
{
|
||||
ctx.controllerEditor->Open(assetPath);
|
||||
}
|
||||
else if (item.kind == Kind::Model && ctx.assets)
|
||||
{
|
||||
const auto ref = ctx.assets->LoadModel(assetPath);
|
||||
ENGINE_INFO("AssetBrowser: model '{}' loaded (valid={})",
|
||||
assetPath, ref.Valid());
|
||||
}
|
||||
else if (item.kind == Kind::Texture && ctx.assets)
|
||||
{
|
||||
const auto ref = ctx.assets->LoadEnvironment(assetPath);
|
||||
ENGINE_INFO("AssetBrowser: env '{}' loaded (valid={})",
|
||||
assetPath, ref.Valid());
|
||||
}
|
||||
else if (item.kind == Kind::Scene)
|
||||
{
|
||||
ENGINE_INFO("AssetBrowser: load scenes via File menu ({})",
|
||||
assetPath);
|
||||
}
|
||||
else if (item.kind == Kind::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);
|
||||
m_CurrentDir = d;
|
||||
m_SelectedPath.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// hover 信息卡(UE Content Browser 式):类型/大小/修改时间/加载状态
|
||||
if (ImGui::IsItemHovered())
|
||||
for (const auto& [path, kind] : files)
|
||||
{
|
||||
ImGui::BeginTooltip();
|
||||
ImGui::TextUnformatted(name.c_str());
|
||||
ImGui::Separator();
|
||||
// 与 Kind 枚举逐位对齐(Kind::Other = 6)——漏项 = tooltip
|
||||
// 数组越界读
|
||||
static const char* kKindNames[] = {"Model", "Texture/Environment",
|
||||
"Scene", "Material",
|
||||
"Controller", "Prefab",
|
||||
"File"};
|
||||
ImGui::Text("type: %s", kKindNames[static_cast<int>(item.kind)]);
|
||||
namespace fs = std::filesystem;
|
||||
std::error_code ec;
|
||||
const auto sz = fs::file_size(item.path, ec);
|
||||
if (!ec)
|
||||
const std::string assetPath = ToAssetPath(path);
|
||||
const auto res = EditorAssetView::DrawRow(
|
||||
ctx, path, assetPath == m_SelectedPath);
|
||||
if (res.clicked)
|
||||
{
|
||||
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);
|
||||
m_SelectedPath = assetPath;
|
||||
}
|
||||
const auto ftime = fs::last_write_time(item.path, ec);
|
||||
if (!ec)
|
||||
if (res.doubleClicked)
|
||||
{
|
||||
// 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);
|
||||
pendingOpen = true;
|
||||
pendingOpenPath = path;
|
||||
pendingOpenKind = kind;
|
||||
}
|
||||
// 加载状态:只对"加载"型资产有意义(查询不触发加载)。
|
||||
// Prefab 是实例化模板、Scene 走 File 菜单——显示 "not
|
||||
// loaded" 会误导(曾让用户当成 bug)
|
||||
if (ctx.assets)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
if (item.kind == Kind::Model ||
|
||||
item.kind == Kind::Texture)
|
||||
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))
|
||||
{
|
||||
const bool loaded =
|
||||
(item.kind == Kind::Model)
|
||||
? ctx.assets->IsModelLoaded(assetPath)
|
||||
: ctx.assets->IsEnvironmentLoaded(assetPath);
|
||||
ImGui::Text("state: %s", loaded ? "loaded"
|
||||
: "not loaded");
|
||||
}
|
||||
else if (item.kind == Kind::Prefab)
|
||||
{
|
||||
ImGui::TextDisabled(
|
||||
"template — drag into scene / double-click");
|
||||
}
|
||||
// 已加载的 model 展开资产数据概览(meshes=0 即纯动画
|
||||
// 容器;未加载不显示——查询不触发加载)
|
||||
if (item.kind == Kind::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()));
|
||||
}
|
||||
m_CurrentDir = d;
|
||||
m_SelectedPath.clear();
|
||||
}
|
||||
}
|
||||
ImGui::TextDisabled("%s", assetPath.c_str());
|
||||
ImGui::EndTooltip();
|
||||
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();
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
|
||||
if (pendingOpen)
|
||||
{
|
||||
DispatchOpen(ctx, pendingOpenPath, pendingOpenKind);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,20 +32,28 @@ public:
|
||||
void Draw(const EditorContext& ctx);
|
||||
|
||||
private:
|
||||
// 目录树节点(递归);右侧列表画 dir 下的文件
|
||||
// 视图模式:列表(行,含内联小缩略图)/ 网格(缩略图瓦片)
|
||||
enum class ViewMode { List, Grid };
|
||||
|
||||
// 目录树节点(递归);右侧内容区画 dir 下的目录+文件
|
||||
void DrawDirTree(const EditorContext& ctx,
|
||||
const std::filesystem::path& dir);
|
||||
void DrawFileList(const EditorContext& ctx,
|
||||
const std::filesystem::path& dir);
|
||||
void DrawContent(const EditorContext& ctx,
|
||||
const std::filesystem::path& dir);
|
||||
|
||||
// 固定根(下标即身份):项目资产 / 场景文件
|
||||
int m_Root = 0;
|
||||
// 当前浏览目录(点目录树切换;列表显示这里的文件)
|
||||
// 当前浏览目录(点目录树切换;内容区显示这里的条目)
|
||||
std::filesystem::path m_CurrentDir;
|
||||
// 类型过滤(0 = All;其余见 kTypeNames)
|
||||
int m_TypeFilter = 0;
|
||||
// 名字子串过滤(空 = 不过滤)
|
||||
char m_NameFilter[64] = "";
|
||||
// 视图模式 + 网格瓦片尺寸(像素)
|
||||
ViewMode m_ViewMode = ViewMode::List;
|
||||
float m_TileSize = 96.0f;
|
||||
// 当前选中条目(资产相对路径;空 = 无选中)
|
||||
std::string m_SelectedPath;
|
||||
};
|
||||
|
||||
#endif //HELLOVULKAN_ASSETBROWSERPANEL_H
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
#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"
|
||||
@@ -73,6 +74,25 @@ namespace {
|
||||
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)
|
||||
{
|
||||
@@ -120,7 +140,10 @@ namespace {
|
||||
(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;
|
||||
@@ -208,20 +231,36 @@ namespace {
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
|
||||
// ---- 组件徽标(与行内同级显示)----
|
||||
std::string badge;
|
||||
if (reg.all_of<Engine::ECS::MeshRendererComponent>(e)) badge += "[M]";
|
||||
if (reg.all_of<Engine::ECS::AnimationComponent>(e)) badge += "[A]";
|
||||
if (reg.all_of<Engine::ECS::CameraComponent>(e)) badge += "[C]";
|
||||
if (reg.all_of<Engine::ECS::LightComponent>(e)) badge += "[L]";
|
||||
if (reg.all_of<Engine::ECS::RigidBodyComponent>(e)) badge += "[P]";
|
||||
if (!badge.empty()) {
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled("%s", badge.c_str());
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip(
|
||||
"M=Mesh A=Animation C=Camera L=Light P=Physics");
|
||||
}
|
||||
// ---- 组件徽标(逐组件着色,与行内同级显示)----
|
||||
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) {
|
||||
|
||||
@@ -9,6 +9,9 @@
|
||||
|
||||
#include "MaterialEditorPanel.h"
|
||||
#include "MaterialAssetUtil.h"
|
||||
#include "AssetItemView.h" // 纹理选择器网格(缩略图瓦片)
|
||||
#include "EditorColors.h"
|
||||
#include "ThumbnailCache.h"
|
||||
|
||||
#include <Engine/ECS/Components.h>
|
||||
#include <Engine/Renderer/Model.h>
|
||||
@@ -17,6 +20,7 @@
|
||||
|
||||
#include <cstdio>
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -34,6 +38,125 @@ namespace {
|
||||
}
|
||||
|
||||
constexpr float kPreviewSize = 128.0f; // 编辑器顶部大球(Inspector 槽位为 48²)
|
||||
|
||||
// 纹理槽缩略图(同时是拖拽落点):Content Browser 拖纹理资产落入即
|
||||
// 指派该槽(kTextureDragPayload)。空槽显示 "nil" 占位
|
||||
void SlotThumb(const std::string& texPath, float sz, int slotIdx,
|
||||
int& pendingSlot, std::string& pendingPath)
|
||||
{
|
||||
const ImVec4 accent = EditorColors::kTexture;
|
||||
ImGui::Dummy(ImVec2(sz, sz));
|
||||
const ImVec2 p0 = ImGui::GetItemRectMin();
|
||||
const ImVec2 p1 = ImGui::GetItemRectMax();
|
||||
const bool hovered = ImGui::IsItemHovered();
|
||||
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||
|
||||
bool drawn = false;
|
||||
if (!texPath.empty())
|
||||
{
|
||||
const ThumbView v = EditorThumbnailCache::Get().Request(texPath);
|
||||
if (v.state == ThumbView::State::Ready)
|
||||
{
|
||||
float fw = sz, fh = sz;
|
||||
if (v.w > 0 && v.h > 0)
|
||||
{
|
||||
if (v.w >= v.h) fh = sz * static_cast<float>(v.h) / v.w;
|
||||
else fw = sz * static_cast<float>(v.w) / v.h;
|
||||
}
|
||||
const ImVec2 f0(p0.x + (sz - fw) * 0.5f,
|
||||
p0.y + (sz - fh) * 0.5f);
|
||||
dl->AddImage(v.tex, f0, ImVec2(f0.x + fw, f0.y + fh));
|
||||
drawn = true;
|
||||
}
|
||||
}
|
||||
if (!drawn)
|
||||
{
|
||||
// 背板只在未出图时铺:出图时叠加会污染透明像素
|
||||
dl->AddRectFilled(
|
||||
p0, p1,
|
||||
ImGui::GetColorU32(ImVec4(accent.x * 0.25f, accent.y * 0.25f,
|
||||
accent.z * 0.25f, 1.0f)),
|
||||
3.0f);
|
||||
const char* lbl = texPath.empty() ? "nil" : "TEX";
|
||||
const ImVec2 ts = ImGui::CalcTextSize(lbl);
|
||||
dl->AddText(ImVec2(p0.x + (sz - ts.x) * 0.5f,
|
||||
p0.y + (sz - ts.y) * 0.5f),
|
||||
ImGui::GetColorU32(texPath.empty()
|
||||
? EditorColors::kOther
|
||||
: accent),
|
||||
lbl);
|
||||
}
|
||||
dl->AddRect(p0, p1,
|
||||
ImGui::GetColorU32(hovered ? EditorColors::Lighten(accent,
|
||||
0.3f)
|
||||
: accent),
|
||||
3.0f);
|
||||
|
||||
if (ImGui::BeginDragDropTarget())
|
||||
{
|
||||
if (const ImGuiPayload* pl =
|
||||
ImGui::AcceptDragDropPayload(kTextureDragPayload))
|
||||
{
|
||||
pendingSlot = slotIdx;
|
||||
pendingPath = std::string(static_cast<const char*>(pl->Data));
|
||||
}
|
||||
ImGui::EndDragDropTarget();
|
||||
}
|
||||
}
|
||||
|
||||
// 纹理选择器内容(弹窗内):<none> 清除入口 + 缩略图网格。
|
||||
// 返回 true = 本次有选择,选中路径写入 out(空串 = <none>)
|
||||
bool TexturePickerContent(const EditorContext& ctx, std::string& out)
|
||||
{
|
||||
bool picked = false;
|
||||
if (ImGui::Selectable("<none>"))
|
||||
{
|
||||
out.clear();
|
||||
picked = true;
|
||||
}
|
||||
ImGui::Separator();
|
||||
|
||||
const auto& files = EditorMaterialUtil::AssetTextureFiles();
|
||||
if (files.empty())
|
||||
{
|
||||
ImGui::TextDisabled("(no textures under assets/)");
|
||||
return picked;
|
||||
}
|
||||
|
||||
constexpr float kTile = 64.0f;
|
||||
const float cellW = kTile + ImGui::GetStyle().CellPadding.x * 2.0f;
|
||||
int cols = static_cast<int>(360.0f / cellW);
|
||||
if (cols < 1)
|
||||
{
|
||||
cols = 1;
|
||||
}
|
||||
ImGui::BeginChild("##texgrid", ImVec2(360.0f, 260.0f),
|
||||
ImGuiChildFlags_Borders);
|
||||
if (ImGui::BeginTable("##grid", cols, ImGuiTableFlags_SizingFixedFit))
|
||||
{
|
||||
for (int i = 0; i < cols; ++i)
|
||||
{
|
||||
ImGui::TableSetupColumn("##c",
|
||||
ImGuiTableColumnFlags_WidthFixed,
|
||||
cellW);
|
||||
}
|
||||
for (const auto& f : files)
|
||||
{
|
||||
ImGui::TableNextColumn();
|
||||
const auto r =
|
||||
EditorAssetView::DrawTile(ctx, f, kTile, false);
|
||||
if (r.clicked)
|
||||
{
|
||||
out = f;
|
||||
picked = true;
|
||||
}
|
||||
}
|
||||
ImGui::EndTable();
|
||||
}
|
||||
ImGui::EndChild();
|
||||
return picked;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void MaterialEditorPanel::OpenAsset(const std::string& path)
|
||||
@@ -228,10 +351,17 @@ void MaterialEditorPanel::DrawAssetMode(const EditorContext& ctx)
|
||||
"Emissive", "Ao", "Roughness"};
|
||||
int pendingSlot = -1;
|
||||
std::string pendingPath;
|
||||
const float thumbSz = ImGui::GetTextLineHeight() * 1.8f;
|
||||
for (Engine::u32 s = 0; s < Engine::Material::kSlotCount; ++s)
|
||||
{
|
||||
std::string tp = asset->GetData().texPaths[s];
|
||||
ImGui::PushID(static_cast<int>(s));
|
||||
|
||||
// 缩略图块(落点:拖入纹理资产即指派该槽)
|
||||
SlotThumb(tp, thumbSz, static_cast<int>(s), pendingSlot, pendingPath);
|
||||
ImGui::SameLine();
|
||||
|
||||
ImGui::SetNextItemWidth(200.0f);
|
||||
InputTextString(kSlotNames[s], tp);
|
||||
if (ImGui::IsItemDeactivatedAfterEdit())
|
||||
{
|
||||
@@ -245,20 +375,13 @@ void MaterialEditorPanel::DrawAssetMode(const EditorContext& ctx)
|
||||
}
|
||||
if (ImGui::BeginPopup("tex_pick"))
|
||||
{
|
||||
// 清除入口(Unity/UE 语义):该槽置空 → 回退兜底贴图
|
||||
if (ImGui::Selectable("<none>"))
|
||||
// 弹窗内只做选择;pending 统一在循环外落盘(避免 popup 内
|
||||
// 立即重建导致 asset 指针悬空)
|
||||
std::string chosen;
|
||||
if (TexturePickerContent(ctx, chosen))
|
||||
{
|
||||
pendingSlot = static_cast<int>(s);
|
||||
pendingPath.clear();
|
||||
}
|
||||
ImGui::Separator();
|
||||
for (const auto& f : EditorMaterialUtil::AssetTextureFiles())
|
||||
{
|
||||
if (ImGui::Selectable(f.c_str()))
|
||||
{
|
||||
pendingSlot = static_cast<int>(s);
|
||||
pendingPath = f;
|
||||
}
|
||||
pendingPath = chosen;
|
||||
}
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
|
||||
@@ -31,6 +31,9 @@
|
||||
// 材质资产拖拽 payload(Content Browser 拖 .material → Details 槽位)
|
||||
inline constexpr const char* kMaterialDragPayload = "MAT_PATH";
|
||||
|
||||
// 纹理资产拖拽 payload(Content Browser 拖 .png/.jpg/... → 材质纹理槽)
|
||||
inline constexpr const char* kTextureDragPayload = "TEXTURE_PATH";
|
||||
|
||||
class MaterialEditorPanel {
|
||||
public:
|
||||
// 打开 .material 资产(可编辑模式)
|
||||
|
||||
Reference in New Issue
Block a user