- 资产浏览器可视化与配色:面板重构(网格/列表/缩略图)、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 缓存—— 修大材质目录首次打开卡顿
500 lines
18 KiB
C++
500 lines
18 KiB
C++
//
|
||
// MaterialEditorPanel.cpp:独立材质编辑器实现。
|
||
//
|
||
// 编辑语义原样承接自原 Inspector "Edit Material" 内嵌块(行为不变的
|
||
// 搬家):参数 EditParams 直写 UBO + IsItemDeactivatedAfterEdit 落盘;
|
||
// 纹理槽改动落盘 + ReloadMaterialAsset 重建(旧指针即刻悬空,本帧
|
||
// 不再触碰 asset)。
|
||
//
|
||
|
||
#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>
|
||
|
||
#include <imgui.h>
|
||
|
||
#include <cstdio>
|
||
#include <filesystem>
|
||
#include <string>
|
||
|
||
namespace {
|
||
|
||
// InputText 需要 char 缓冲;提交时写回 std::string(与 Inspector 同款)
|
||
bool InputTextString(const char* label, std::string& str)
|
||
{
|
||
char buf[256];
|
||
std::snprintf(buf, sizeof(buf), "%s", str.c_str());
|
||
if (ImGui::InputText(label, buf, sizeof(buf)))
|
||
{
|
||
str = buf;
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
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)
|
||
{
|
||
m_AssetMode = true;
|
||
m_Path = path;
|
||
m_LoadTried = false;
|
||
m_Owner = entt::null;
|
||
m_Open = true;
|
||
}
|
||
|
||
void MaterialEditorPanel::OpenBuiltin(entt::entity owner, Engine::u32 meshIdx,
|
||
const std::string& displayName)
|
||
{
|
||
m_AssetMode = false;
|
||
m_Owner = owner;
|
||
m_MeshIdx = meshIdx;
|
||
m_DisplayName = displayName;
|
||
m_Path.clear();
|
||
m_Open = true;
|
||
}
|
||
|
||
void MaterialEditorPanel::Draw(const EditorContext& ctx)
|
||
{
|
||
if (!m_Open)
|
||
{
|
||
return;
|
||
}
|
||
if (!ImGui::Begin("Material Editor", &m_Open))
|
||
{
|
||
ImGui::End();
|
||
return;
|
||
}
|
||
|
||
if (m_AssetMode)
|
||
{
|
||
DrawAssetMode(ctx);
|
||
}
|
||
else
|
||
{
|
||
DrawBuiltinMode(ctx);
|
||
}
|
||
|
||
ImGui::End();
|
||
}
|
||
|
||
// 顶部预览球(tile 池请求幂等——窗口开着才请求,池按可见项换页)
|
||
void MaterialEditorPanel::DrawPreview(const EditorContext& ctx,
|
||
Engine::Material* mat)
|
||
{
|
||
Engine::i32 tile = -1;
|
||
void* atlasTex = nullptr;
|
||
if (ctx.renderer && mat)
|
||
{
|
||
tile = ctx.renderer->RequestMaterialPreview(mat);
|
||
atlasTex = ctx.renderer->GetMaterialPreviewTexture();
|
||
}
|
||
if (atlasTex && tile >= 0)
|
||
{
|
||
const float uvStep = 1.0f / float(Engine::Passes::kPreviewTilesPerSide);
|
||
const float u0 =
|
||
float(tile % Engine::Passes::kPreviewTilesPerSide) * uvStep;
|
||
const float v0 =
|
||
float(tile / Engine::Passes::kPreviewTilesPerSide) * uvStep;
|
||
ImGui::Image(atlasTex, ImVec2(kPreviewSize, kPreviewSize),
|
||
ImVec2(u0, v0), ImVec2(u0 + uvStep, v0 + uvStep));
|
||
}
|
||
else
|
||
{
|
||
ImGui::Dummy(ImVec2(kPreviewSize, kPreviewSize));
|
||
}
|
||
}
|
||
|
||
void MaterialEditorPanel::DrawAssetMode(const EditorContext& ctx)
|
||
{
|
||
if (!ctx.assets)
|
||
{
|
||
return;
|
||
}
|
||
|
||
// 每帧重解析:ReloadMaterialAsset 换实例 / 未加载自动 Load 一次
|
||
Engine::MaterialAsset* asset = ctx.assets->FindMaterialAsset(m_Path);
|
||
if (!asset && !m_LoadTried)
|
||
{
|
||
m_LoadTried = true;
|
||
ctx.assets->LoadMaterialAsset(m_Path);
|
||
asset = ctx.assets->FindMaterialAsset(m_Path);
|
||
}
|
||
if (!asset)
|
||
{
|
||
ImGui::TextDisabled("material not loaded: %s", m_Path.c_str());
|
||
if (ImGui::Button("Retry Load"))
|
||
{
|
||
m_LoadTried = false;
|
||
}
|
||
return;
|
||
}
|
||
|
||
const std::string& path = m_Path;
|
||
const std::string name =
|
||
std::filesystem::path(path).filename().generic_string();
|
||
|
||
// ---- 头部:预览球 + 资产名/路径 ----
|
||
DrawPreview(ctx, asset->GetMaterial());
|
||
ImGui::SameLine();
|
||
ImGui::BeginGroup();
|
||
ImGui::TextUnformatted(name.c_str());
|
||
ImGui::TextDisabled("%s", path.c_str());
|
||
ImGui::TextDisabled("shared asset: edits apply to all entities "
|
||
"referencing it");
|
||
ImGui::EndGroup();
|
||
ImGui::Separator();
|
||
|
||
// ---- 透明队列归属(收集侧 CPU 标志)----
|
||
bool trans = asset->GetMaterial()->IsTransparent();
|
||
if (ImGui::Checkbox("Transparent", &trans))
|
||
{
|
||
asset->SetTransparent(trans);
|
||
Engine::MaterialAsset::Save(path, asset->GetData());
|
||
}
|
||
|
||
// ---- 参数(变化直写 UBO 实时预览;松手落盘,拖拽中不产生每帧 IO)----
|
||
Engine::MaterialParams p = asset->GetData().params;
|
||
auto EditParam = [&](bool changed)
|
||
{
|
||
if (changed)
|
||
{
|
||
asset->EditParams(p);
|
||
}
|
||
if (ImGui::IsItemDeactivatedAfterEdit())
|
||
{
|
||
Engine::MaterialAsset::Save(path, asset->GetData());
|
||
}
|
||
};
|
||
EditParam(
|
||
ImGui::DragFloat("Metallic", &p.factors.x, 0.01f, 0.0f, 1.0f));
|
||
EditParam(
|
||
ImGui::DragFloat("Roughness", &p.factors.y, 0.01f, 0.0f, 1.0f));
|
||
EditParam(ImGui::ColorEdit4("Base Color", &p.baseColor.x));
|
||
// albedo 贴图 alpha 作细节叠加遮罩(脸部腮红/卧蚕等):rgb 叠加色,
|
||
// a 强度(0 = 关闭);需 albedo 贴图自带 alpha 遮罩才有局部效果
|
||
EditParam(ImGui::ColorEdit4("Alpha Overlay", &p.alphaOverlay.x));
|
||
EditParam(ImGui::DragFloat("IOR", &p.surface.x, 0.01f, 1.0f, 3.0f));
|
||
// 发光 = emissiveFactor × emissive 贴图 × Strength(Emissive 槽
|
||
// 兜底贴图恒黑,无贴图时颜色因子是唯一入口)
|
||
EditParam(ImGui::ColorEdit3("Emissive Color", &p.emissiveFactor.x));
|
||
EditParam(ImGui::DragFloat("Emissive Strength", &p.surface.z, 0.05f,
|
||
0.0f, 100.0f));
|
||
// transmission/thickness 只有不透明段不读——透明 shader 专属
|
||
//(未勾 Transparent 时禁用编辑,防"拖了没反应"的困惑)
|
||
ImGui::BeginDisabled(!trans);
|
||
EditParam(ImGui::DragFloat("Transmission", &p.surface.y, 0.01f, 0.0f,
|
||
1.0f));
|
||
EditParam(ImGui::DragFloat("Thickness", &p.surface.w, 0.01f, 0.0f,
|
||
10.0f));
|
||
ImGui::EndDisabled();
|
||
if (!trans)
|
||
{
|
||
ImGui::TextDisabled("Transmission/Thickness need Transparent");
|
||
}
|
||
|
||
// UV 镜像(材质级开关,作用于全部纹理槽):源资产 UV 朝向与引擎
|
||
// 上左原点约定相反时贴图整体翻转,这里一键纠正。写入 misc.y/z,
|
||
// 旧 .material 无该分量 → 缺省 0 = 关闭
|
||
{
|
||
bool flipU = p.misc.y > 0.5f;
|
||
const bool c = ImGui::Checkbox("Flip U (left-right)", &flipU);
|
||
if (c) { p.misc.y = flipU ? 1.0f : 0.0f; }
|
||
EditParam(c);
|
||
}
|
||
ImGui::SameLine();
|
||
{
|
||
bool flipV = p.misc.z > 0.5f;
|
||
const bool c = ImGui::Checkbox("Flip V (up-down)", &flipV);
|
||
if (c) { p.misc.z = flipV ? 1.0f : 0.0f; }
|
||
EditParam(c);
|
||
}
|
||
ImGui::SameLine();
|
||
{
|
||
// DirectX 法线绿通道翻转(UE 打包 Specular 资产由导入器默认为开;
|
||
// 方向反了在此逐材质纠正)。写 misc.w,旧 .material 缺省 0 = 关闭
|
||
bool flipG = p.misc.w > 0.5f;
|
||
const bool c = ImGui::Checkbox("Flip Green (DirectX)", &flipG);
|
||
if (c) { p.misc.w = flipG ? 1.0f : 0.0f; }
|
||
EditParam(c);
|
||
}
|
||
|
||
// ---- 纹理槽(变化 → 落盘 + waitIdle 重建)----
|
||
ImGui::SeparatorText("Textures");
|
||
static constexpr const char* kSlotNames[Engine::Material::kSlotCount] = {
|
||
"Albedo", "Normal", "MetallicRoughness",
|
||
"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())
|
||
{
|
||
pendingSlot = static_cast<int>(s);
|
||
pendingPath = tp;
|
||
}
|
||
ImGui::SameLine();
|
||
if (ImGui::SmallButton("..."))
|
||
{
|
||
ImGui::OpenPopup("tex_pick");
|
||
}
|
||
if (ImGui::BeginPopup("tex_pick"))
|
||
{
|
||
// 弹窗内只做选择;pending 统一在循环外落盘(避免 popup 内
|
||
// 立即重建导致 asset 指针悬空)
|
||
std::string chosen;
|
||
if (TexturePickerContent(ctx, chosen))
|
||
{
|
||
pendingSlot = static_cast<int>(s);
|
||
pendingPath = chosen;
|
||
}
|
||
ImGui::EndPopup();
|
||
}
|
||
ImGui::PopID();
|
||
}
|
||
if (pendingSlot >= 0)
|
||
{
|
||
// 拷出数据段 → 落盘 → 重建资产(旧指针即刻悬空:本帧到此不再
|
||
// 触碰 asset,下帧 Find 取新实例)。旧材质已在本帧 Request 进
|
||
// 预览池脏表(新 tile 未画满三槽位时每帧重入队)——先 drop 再
|
||
// 销毁,否则 Render 段解引用悬空指针
|
||
Engine::MaterialAssetData d = asset->GetData();
|
||
d.texPaths[static_cast<Engine::u32>(pendingSlot)] = pendingPath;
|
||
Engine::MaterialAsset::Save(path, d);
|
||
if (ctx.renderer)
|
||
{
|
||
ctx.renderer->DropMaterialPreview(asset->GetMaterial());
|
||
}
|
||
ctx.assets->ReloadMaterialAsset(path);
|
||
}
|
||
}
|
||
|
||
void MaterialEditorPanel::DrawBuiltinMode(const EditorContext& ctx)
|
||
{
|
||
// 每帧重解析:实体可销毁(PIE Stop 换血)/ 模型可卸载
|
||
if (!ctx.scene || !ctx.assets)
|
||
{
|
||
ImGui::End();
|
||
return;
|
||
}
|
||
auto& reg = ctx.scene->GetRegistry();
|
||
auto* mr = reg.valid(m_Owner)
|
||
? reg.try_get<Engine::ECS::MeshRendererComponent>(m_Owner)
|
||
: nullptr;
|
||
const Engine::Model* model =
|
||
(mr && mr->model.Valid()) ? ctx.assets->GetModel(mr->model) : nullptr;
|
||
const Engine::MeshMaterialInfo* info =
|
||
(model && m_MeshIdx < model->GetMeshMaterialInfos().size())
|
||
? &model->GetMeshMaterialInfos()[m_MeshIdx]
|
||
: nullptr;
|
||
if (!mr || !model || !info)
|
||
{
|
||
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f),
|
||
"built-in material no longer available");
|
||
ImGui::TextDisabled("(entity destroyed or model unloaded)");
|
||
return;
|
||
}
|
||
|
||
const std::string meshName =
|
||
(!info->name.empty())
|
||
? info->name
|
||
: "Slot " + std::to_string(m_MeshIdx);
|
||
|
||
// ---- 头部:预览球 + 名称/来源 ----
|
||
DrawPreview(ctx, model->GetMeshes()[m_MeshIdx]->GetMaterial());
|
||
ImGui::SameLine();
|
||
ImGui::BeginGroup();
|
||
ImGui::TextUnformatted(meshName.c_str());
|
||
ImGui::TextDisabled("built-in material of %s",
|
||
std::filesystem::path(
|
||
ctx.assets->Models().Path(mr->model))
|
||
.filename()
|
||
.generic_string()
|
||
.c_str());
|
||
ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.3f, 1.0f),
|
||
"read-only: built-in materials belong to the model");
|
||
ImGui::EndGroup();
|
||
ImGui::Separator();
|
||
|
||
// ---- 一键转可编辑:Create Override → 自动切到资产模式 ----
|
||
if (ImGui::Button("Create Override"))
|
||
{
|
||
const std::string rel = EditorMaterialUtil::CreateMaterialOverride(
|
||
ctx.assets->Models().Path(mr->model), *info, m_MeshIdx, *mr,
|
||
*ctx.assets);
|
||
if (!rel.empty())
|
||
{
|
||
OpenAsset(rel); // 数据已快照进资产,无缝进入编辑
|
||
}
|
||
}
|
||
ImGui::Separator();
|
||
|
||
// ---- 只读展示(与资产模式同款控件,锁编辑看值)----
|
||
ImGui::BeginDisabled(true);
|
||
bool trans = info->transparent;
|
||
ImGui::Checkbox("Transparent", &trans);
|
||
Engine::MaterialParams p = info->params;
|
||
ImGui::DragFloat("Metallic", &p.factors.x, 0.01f, 0.0f, 1.0f);
|
||
ImGui::DragFloat("Roughness", &p.factors.y, 0.01f, 0.0f, 1.0f);
|
||
ImGui::ColorEdit4("Base Color", &p.baseColor.x);
|
||
ImGui::ColorEdit4("Alpha Overlay", &p.alphaOverlay.x);
|
||
ImGui::DragFloat("IOR", &p.surface.x, 0.01f, 1.0f, 3.0f);
|
||
ImGui::ColorEdit3("Emissive Color", &p.emissiveFactor.x);
|
||
ImGui::DragFloat("Emissive Strength", &p.surface.z, 0.05f, 0.0f, 100.0f);
|
||
ImGui::BeginDisabled(!trans);
|
||
ImGui::DragFloat("Transmission", &p.surface.y, 0.01f, 0.0f, 1.0f);
|
||
ImGui::DragFloat("Thickness", &p.surface.w, 0.01f, 0.0f, 10.0f);
|
||
ImGui::EndDisabled();
|
||
if (!trans)
|
||
{
|
||
ImGui::TextDisabled("Transmission/Thickness need Transparent");
|
||
}
|
||
ImGui::SeparatorText("Textures");
|
||
static constexpr const char* kSlotNames[Engine::Material::kSlotCount] = {
|
||
"Albedo", "Normal", "MetallicRoughness",
|
||
"Emissive", "Ao", "Roughness"};
|
||
for (Engine::u32 s = 0; s < Engine::Material::kSlotCount; ++s)
|
||
{
|
||
std::string tp = info->texPaths[s];
|
||
ImGui::PushID(static_cast<int>(s));
|
||
InputTextString(kSlotNames[s], tp);
|
||
ImGui::PopID();
|
||
}
|
||
ImGui::EndDisabled();
|
||
}
|