- .animctrl state machine asset (params/transitions/blend trees, 1D+2D directional blending) with Unity Animator-style editor panel: state list, transitions, params, directional pad preview driving live CrossFade on the selected entity - Split animation evaluation out of SceneRenderer into AnimationSystem/AnimationEvaluator; skin palettes with rest-pose fallback and blend-weight epsilon filtering - MaterialAsset (.material) with reload-safe preview tile pool (DropMaterial clears dirty refs before instance destruction) - EditorGizmoPass skeleton wires + viewport joint overlay; camera projection rework (baked Vulkan Y-flip, ortho support) - Editor/game build split via HELLOVULKAN_EDITOR option
322 lines
10 KiB
C++
322 lines
10 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));
|
|
EditParam(ImGui::DragFloat("Transmission", &p.surface.y, 0.01f, 0.0f,
|
|
1.0f));
|
|
EditParam(ImGui::DragFloat("Emissive Strength", &p.surface.z, 0.05f,
|
|
0.0f, 100.0f));
|
|
EditParam(ImGui::DragFloat("Thickness", &p.surface.w, 0.01f, 0.0f,
|
|
10.0f));
|
|
|
|
// ---- 纹理槽(变化 → 落盘 + 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::DragFloat("Transmission", &p.surface.y, 0.01f, 0.0f, 1.0f);
|
|
ImGui::DragFloat("Emissive Strength", &p.surface.z, 0.05f, 0.0f, 100.0f);
|
|
ImGui::DragFloat("Thickness", &p.surface.w, 0.01f, 0.0f, 10.0f);
|
|
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();
|
|
}
|