// // 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 #include #include #include #include #include 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(v.h) / v.w; else fw = sz * static_cast(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(pl->Data)); } ImGui::EndDragDropTarget(); } } // 纹理选择器内容(弹窗内): 清除入口 + 缩略图网格。 // 返回 true = 本次有选择,选中路径写入 out(空串 = ) bool TexturePickerContent(const EditorContext& ctx, std::string& out) { bool picked = false; if (ImGui::Selectable("")) { 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(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(s)); // 缩略图块(落点:拖入纹理资产即指派该槽) SlotThumb(tp, thumbSz, static_cast(s), pendingSlot, pendingPath); ImGui::SameLine(); ImGui::SetNextItemWidth(200.0f); InputTextString(kSlotNames[s], tp); if (ImGui::IsItemDeactivatedAfterEdit()) { pendingSlot = static_cast(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(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(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(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(s)); InputTextString(kSlotNames[s], tp); ImGui::PopID(); } ImGui::EndDisabled(); }