340 lines
11 KiB
C++
340 lines
11 KiB
C++
//
|
||
// MaterialEditorPanel.cpp:独立材质编辑器实现。
|
||
//
|
||
// 编辑语义原样承接自原 Inspector "Edit Material" 内嵌块(行为不变的
|
||
// 搬家):参数 EditParams 直写 UBO + IsItemDeactivatedAfterEdit 落盘;
|
||
// 纹理槽改动落盘 + ReloadMaterialAsset 重建(旧指针即刻悬空,本帧
|
||
// 不再触碰 asset)。
|
||
//
|
||
|
||
#include "MaterialEditorPanel.h"
|
||
#include "MaterialAssetUtil.h"
|
||
|
||
#include <Engine/ECS/Components.h>
|
||
#include <Engine/Renderer/Model.h>
|
||
|
||
#include <imgui.h>
|
||
|
||
#include <cstdio>
|
||
#include <filesystem>
|
||
|
||
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²)
|
||
} // 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));
|
||
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");
|
||
}
|
||
|
||
// ---- 纹理槽(变化 → 落盘 + waitIdle 重建)----
|
||
ImGui::SeparatorText("Textures");
|
||
static constexpr const char* kSlotNames[Engine::Material::kSlotCount] = {
|
||
"Albedo", "Normal", "MetallicRoughness",
|
||
"Emissive", "Ao", "Roughness"};
|
||
int pendingSlot = -1;
|
||
std::string pendingPath;
|
||
for (Engine::u32 s = 0; s < Engine::Material::kSlotCount; ++s)
|
||
{
|
||
std::string tp = asset->GetData().texPaths[s];
|
||
ImGui::PushID(static_cast<int>(s));
|
||
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"))
|
||
{
|
||
for (const auto& f : EditorMaterialUtil::AssetTextureFiles())
|
||
{
|
||
if (ImGui::Selectable(f.c_str()))
|
||
{
|
||
pendingSlot = static_cast<int>(s);
|
||
pendingPath = f;
|
||
}
|
||
}
|
||
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::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();
|
||
}
|