Files
HelloVulkan/App/editor/panels/MaterialEditorPanel.cpp
T
atdunbg 84cf547704 资产浏览器可视化改造 + 材质缩略图 GPU 烘焙 + 纹理上传批式化
- 资产浏览器可视化与配色:面板重构(网格/列表/缩略图)、AssetKind 分类、
  EditorColors 统一配色、AssetItemView 抽出条目绘制;HierarchyPanel/
  MaterialEditorPanel 同步调整
- 材质缩略图改走 GPU 回读烘焙:MaterialPreviewPass 新增 bake pass
  (SDR atlas → host staging copyImageToBuffer)、Renderer 提供
  Request/PollMaterialPreviewBake(无 waitIdle,依赖 per-slot fence),
  Editor 每帧排空后注入 ThumbnailCache——脱离预览 tile 池,消除"滚出视野
  即失效/池耗尽"两问题
- 缩略图失效:BakedIdentity(serial, editVersion) 与当前材质身份比对,
  换贴图槽(重载新 serial)/调参数(editVersion++)不符即 Invalidate 重烘,
  修"修改材质后资产浏览器缩略图不更新"
- 材质加载闸门:每帧至多 1 个 + 按实测耗时冷却,替代固定 2/帧
- 纹理上传批式化:VulkanAssetUploader 新增路径纹理缓存(跨材质去重,
  同一贴图只解码/上传一次)+ 同批新纹理合并为单 cmd/单 submit/单 fence;
  Texture 抽出 RecordPixelsUpload/CreateDeferred/ReleasePendingStaging,
  析构兜底释放 staging;MaterialAsset 纹理所有权移交 uploader 缓存——
  修大材质目录首次打开卡顿
2026-10-18 20:05:29 +08:00

500 lines
18 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//
// 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();
}