- 动画:HumanoidRig 三层架构(standard 标准槽 / dialects 方言映射 / mapper 配对), 指/趾统一走世界增量两遍式烘焙;AnimationAttach 拆为纯算法 + facade;修正 HumanoidBasis 行走基朝向与跨骨架去重、根运动通道 - 物理:CharacterController 角色互碰撞 / shapeOffset / 接触事件;PhysicsWorld 接触合成与 字符冲量判定 - 渲染与材质:Model 多材质按材质拆子 mesh 与贴图路径解析、DDS 直传、GLB 内嵌纹理落盘、 不透明通道 alpha 固定 1.0、glTF FlipUVs 对齐;SceneSerializer 支持 meshIndex 与材质覆盖 - 脚本:FractureLib 抽为共享库(HostView 桥接)、WallFracture 参数直绑、新增 ThirdPersonCamera / Follower / WASDMove 玩法;移除 GameDirector / PlayerController / Target - 编辑器:新增 GamepadPanel、LogPanel(LogRing),改进 Inspector / 材质 / 统计 / 视口面板 - 依赖:vendored ufbx(FBX 5.x/6.x/7.x 全版本解析,替代 assimp 对 FBX 6.1 的拒读)
368 lines
12 KiB
C++
368 lines
12 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));
|
||
// 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);
|
||
}
|
||
|
||
// ---- 纹理槽(变化 → 落盘 + 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"))
|
||
{
|
||
// 清除入口(Unity/UE 语义):该槽置空 → 回退兜底贴图
|
||
if (ImGui::Selectable("<none>"))
|
||
{
|
||
pendingSlot = static_cast<int>(s);
|
||
pendingPath.clear();
|
||
}
|
||
ImGui::Separator();
|
||
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::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();
|
||
}
|