Reorganize directory layout: editor panels/, grouped shaders, drop stale header
- App/editor: move 7 panels into panels/ (Editor core stays at root) - shaders/: group into pbr/ post/ debug/ env/ (runtime load paths updated) - Delete outdated duplicate Engine/src/Renderer/VulkanAssetUploader.h
This commit is contained in:
@@ -0,0 +1,412 @@
|
||||
//
|
||||
// AssetBrowserPanel.cpp:项目资产浏览器实现。
|
||||
//
|
||||
|
||||
#include "AssetBrowserPanel.h"
|
||||
#include "MaterialEditorPanel.h" // kMaterialDragPayload + 双击打开材质编辑器
|
||||
#include "ControllerEditorPanel.h" // kControllerDragPayload + 双击打开控制器编辑器
|
||||
|
||||
#include <Engine/Core/Log.h>
|
||||
|
||||
#include <imgui.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <ctime>
|
||||
#include <filesystem>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
// 固定根:下标与 Editor 无耦合,仅面板内部使用
|
||||
constexpr const char* kRootNames[] = {"Assets", "Scenes"};
|
||||
constexpr const char* kRootPaths[] = {"assets", "scenes"};
|
||||
|
||||
// 类型过滤(下标即身份):0 = All
|
||||
constexpr const char* kTypeNames[] = {"All", "Models",
|
||||
"Textures", "Scenes",
|
||||
"Materials", "Controllers"};
|
||||
|
||||
// 资产类别:按扩展名分派(过滤用)
|
||||
enum class Kind { Model, Texture, Scene, Material, AnimClip, Other };
|
||||
|
||||
Kind KindOf(const std::filesystem::path& p)
|
||||
{
|
||||
std::string ext = p.extension().string();
|
||||
std::transform(ext.begin(), ext.end(), ext.begin(),
|
||||
[](unsigned char c) { return std::tolower(c); });
|
||||
if (ext == ".gltf" || ext == ".glb" || ext == ".fbx" ||
|
||||
ext == ".obj")
|
||||
{
|
||||
return Kind::Model;
|
||||
}
|
||||
if (ext == ".hdr" || ext == ".png" || ext == ".jpg" ||
|
||||
ext == ".jpeg")
|
||||
{
|
||||
return Kind::Texture;
|
||||
}
|
||||
if (ext == ".scene")
|
||||
{
|
||||
return Kind::Scene;
|
||||
}
|
||||
if (ext == ".material")
|
||||
{
|
||||
return Kind::Material;
|
||||
}
|
||||
if (ext == ".animctrl")
|
||||
{
|
||||
return Kind::AnimClip;
|
||||
}
|
||||
return Kind::Other;
|
||||
}
|
||||
|
||||
bool PassFilter(int typeFilter, Kind kind)
|
||||
{
|
||||
switch (typeFilter)
|
||||
{
|
||||
case 1: return kind == Kind::Model;
|
||||
case 2: return kind == Kind::Texture;
|
||||
case 3: return kind == Kind::Scene;
|
||||
case 4: return kind == Kind::Material;
|
||||
case 5: return kind == Kind::AnimClip;
|
||||
default: return true;
|
||||
}
|
||||
}
|
||||
|
||||
// 列表条目前缀符号(ImGui 默认字体无图标字形,用 ASCII 标签)
|
||||
const char* KindTag(Kind kind)
|
||||
{
|
||||
switch (kind)
|
||||
{
|
||||
case Kind::Model: return "[M]";
|
||||
case Kind::Texture: return "[T]";
|
||||
case Kind::Scene: return "[S]";
|
||||
case Kind::Material: return "[Ma]";
|
||||
case Kind::AnimClip: return "[An]";
|
||||
default: return "[ ]";
|
||||
}
|
||||
}
|
||||
|
||||
// 相对路径规范化为正斜杠(AssetManager 键与场景文件统一 POSIX 风格)
|
||||
std::string ToAssetPath(const std::filesystem::path& p)
|
||||
{
|
||||
std::string s = p.generic_string();
|
||||
std::replace(s.begin(), s.end(), '\\', '/');
|
||||
return s;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void AssetBrowserPanel::Draw(const EditorContext& ctx)
|
||||
{
|
||||
if (!ImGui::Begin("Content Browser"))
|
||||
{
|
||||
ImGui::End();
|
||||
return;
|
||||
}
|
||||
|
||||
// ---- 顶栏:根切换 + 类型过滤 + 名字过滤 ----
|
||||
if (ImGui::BeginTabBar("##roots"))
|
||||
{
|
||||
for (int i = 0; i < 2; ++i)
|
||||
{
|
||||
if (ImGui::BeginTabItem(kRootNames[i]))
|
||||
{
|
||||
m_Root = i;
|
||||
ImGui::EndTabItem();
|
||||
}
|
||||
}
|
||||
ImGui::EndTabBar();
|
||||
}
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(110.0f);
|
||||
ImGui::Combo("##type", &m_TypeFilter, kTypeNames, 6);
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(140.0f);
|
||||
ImGui::InputTextWithHint("##name", "filter", m_NameFilter,
|
||||
sizeof(m_NameFilter));
|
||||
ImGui::Separator();
|
||||
|
||||
const std::filesystem::path root(kRootPaths[m_Root]);
|
||||
// 根切换 / 首帧:当前目录回根。前缀比较手写(path::starts_with
|
||||
// 是 libstdc++ 11 才补齐的成员,本机 GCC 没有);补 '/' 分隔校验
|
||||
// 防 "assets_x" 之类误匹配
|
||||
const std::string rootStr = root.generic_string();
|
||||
const std::string curStr = m_CurrentDir.generic_string();
|
||||
const bool under = curStr == rootStr ||
|
||||
(curStr.size() > rootStr.size() &&
|
||||
curStr.compare(0, rootStr.size(), rootStr) == 0 &&
|
||||
curStr[rootStr.size()] == '/');
|
||||
if (m_CurrentDir.empty() || !under)
|
||||
{
|
||||
m_CurrentDir = root;
|
||||
}
|
||||
|
||||
// ---- 顶栏下:面包屑(当前目录相对根路径 + 回根按钮)----
|
||||
if (ImGui::Button("Root"))
|
||||
{
|
||||
m_CurrentDir = root;
|
||||
}
|
||||
ImGui::SameLine();
|
||||
std::string rel = std::filesystem::relative(m_CurrentDir, root)
|
||||
.generic_string();
|
||||
ImGui::TextDisabled("%s/%s", kRootNames[m_Root],
|
||||
rel.empty() ? "" : rel.c_str());
|
||||
ImGui::Separator();
|
||||
|
||||
// ---- 左:目录树 | 右:当前目录文件列表 ----
|
||||
const float treeWidth = 180.0f;
|
||||
ImGui::BeginChild("##tree", ImVec2(treeWidth, 0.0f),
|
||||
ImGuiChildFlags_ResizeX | ImGuiChildFlags_Borders);
|
||||
if (std::filesystem::exists(root))
|
||||
{
|
||||
// 根节点也画进树(可点击选中)
|
||||
ImGui::SetNextItemOpen(true, ImGuiCond_Once);
|
||||
if (ImGui::TreeNodeEx(kRootNames[m_Root],
|
||||
ImGuiTreeNodeFlags_OpenOnArrow))
|
||||
{
|
||||
if (ImGui::IsItemClicked(ImGuiMouseButton_Left))
|
||||
{
|
||||
m_CurrentDir = root;
|
||||
}
|
||||
DrawDirTree(ctx, root);
|
||||
ImGui::TreePop();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui::TextDisabled("(no '%s' dir)", kRootPaths[m_Root]);
|
||||
}
|
||||
ImGui::EndChild();
|
||||
|
||||
ImGui::SameLine();
|
||||
ImGui::BeginChild("##files", ImVec2(0.0f, 0.0f),
|
||||
ImGuiChildFlags_Borders);
|
||||
if (std::filesystem::exists(m_CurrentDir))
|
||||
{
|
||||
DrawFileList(ctx, m_CurrentDir);
|
||||
}
|
||||
ImGui::EndChild();
|
||||
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
void AssetBrowserPanel::DrawDirTree(const EditorContext& ctx,
|
||||
const std::filesystem::path& dir)
|
||||
{
|
||||
for (const auto& entry : std::filesystem::directory_iterator(dir))
|
||||
{
|
||||
if (!entry.is_directory())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const std::string name = entry.path().filename().string();
|
||||
// OpenOnArrow:单击行 = 选中(列表切换),箭头 = 折叠
|
||||
const bool open = ImGui::TreeNodeEx(
|
||||
name.c_str(), ImGuiTreeNodeFlags_OpenOnArrow |
|
||||
ImGuiTreeNodeFlags_SpanFullWidth);
|
||||
if (ImGui::IsItemClicked(ImGuiMouseButton_Left))
|
||||
{
|
||||
m_CurrentDir = entry.path();
|
||||
}
|
||||
if (open)
|
||||
{
|
||||
DrawDirTree(ctx, entry.path());
|
||||
ImGui::TreePop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AssetBrowserPanel::DrawFileList(const EditorContext& ctx,
|
||||
const std::filesystem::path& dir)
|
||||
{
|
||||
// 收集 + 过滤 + 排序(名字序;目录迭代序不稳定)
|
||||
struct Item
|
||||
{
|
||||
std::filesystem::path path;
|
||||
Kind kind;
|
||||
};
|
||||
std::vector<Item> items;
|
||||
const std::string filter = m_NameFilter;
|
||||
for (const auto& entry : std::filesystem::directory_iterator(dir))
|
||||
{
|
||||
if (entry.is_directory())
|
||||
{
|
||||
// 子目录也进列表:单行点击进入(树/列表双向导航)
|
||||
const std::string dname = entry.path().filename().string();
|
||||
ImGui::PushID(dname.c_str());
|
||||
ImGui::TextUnformatted("[D]");
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Selectable(dname.c_str()))
|
||||
{
|
||||
m_CurrentDir = entry.path();
|
||||
}
|
||||
ImGui::PopID();
|
||||
continue;
|
||||
}
|
||||
const Kind kind = KindOf(entry.path());
|
||||
if (!PassFilter(m_TypeFilter, kind))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (!filter.empty())
|
||||
{
|
||||
const std::string name = entry.path().filename().string();
|
||||
if (name.find(filter) == std::string::npos)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
items.push_back({entry.path(), kind});
|
||||
}
|
||||
std::sort(items.begin(), items.end(),
|
||||
[](const Item& a, const Item& b)
|
||||
{
|
||||
return a.path.filename().string() <
|
||||
b.path.filename().string();
|
||||
});
|
||||
|
||||
for (const auto& item : items)
|
||||
{
|
||||
const std::string name = item.path.filename().string();
|
||||
const std::string assetPath = ToAssetPath(item.path);
|
||||
ImGui::PushID(name.c_str());
|
||||
|
||||
ImGui::TextUnformatted(KindTag(item.kind));
|
||||
ImGui::SameLine();
|
||||
ImGui::Selectable(name.c_str());
|
||||
|
||||
// .material 拖拽源:拖到 Details 材质槽球上指派(kMaterialDragPayload)
|
||||
if (item.kind == Kind::Material && ImGui::BeginDragDropSource())
|
||||
{
|
||||
ImGui::SetDragDropPayload(kMaterialDragPayload,
|
||||
assetPath.c_str(), assetPath.size() + 1);
|
||||
ImGui::TextUnformatted(name.c_str());
|
||||
ImGui::EndDragDropSource();
|
||||
}
|
||||
|
||||
// .animctrl 拖拽源:拖到 Details Controller 槽指派
|
||||
//(kControllerDragPayload,对齐材质槽交互)
|
||||
if (item.kind == Kind::AnimClip && ImGui::BeginDragDropSource())
|
||||
{
|
||||
ImGui::SetDragDropPayload(kControllerDragPayload,
|
||||
assetPath.c_str(), assetPath.size() + 1);
|
||||
ImGui::TextUnformatted(name.c_str());
|
||||
ImGui::EndDragDropSource();
|
||||
}
|
||||
|
||||
// 双击:.material 打开 Material Editor;.animctrl 打开
|
||||
// Controller Editor;model/HDR 进缓存预热(路径去重,重复
|
||||
// 双击零成本);场景加载含环境恢复/选中清理流程,提示走
|
||||
// File 菜单
|
||||
if (ImGui::IsItemHovered() &&
|
||||
ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left))
|
||||
{
|
||||
if (item.kind == Kind::Material && ctx.materialEditor)
|
||||
{
|
||||
ctx.materialEditor->OpenAsset(assetPath);
|
||||
}
|
||||
else if (item.kind == Kind::AnimClip && ctx.controllerEditor)
|
||||
{
|
||||
ctx.controllerEditor->Open(assetPath);
|
||||
}
|
||||
else if (item.kind == Kind::Model && ctx.assets)
|
||||
{
|
||||
const auto ref = ctx.assets->LoadModel(assetPath);
|
||||
ENGINE_INFO("AssetBrowser: model '{}' loaded (valid={})",
|
||||
assetPath, ref.Valid());
|
||||
}
|
||||
else if (item.kind == Kind::Texture && ctx.assets)
|
||||
{
|
||||
const auto ref = ctx.assets->LoadEnvironment(assetPath);
|
||||
ENGINE_INFO("AssetBrowser: env '{}' loaded (valid={})",
|
||||
assetPath, ref.Valid());
|
||||
}
|
||||
else if (item.kind == Kind::Scene)
|
||||
{
|
||||
ENGINE_INFO("AssetBrowser: load scenes via File menu ({})",
|
||||
assetPath);
|
||||
}
|
||||
}
|
||||
|
||||
// hover 信息卡(UE Content Browser 式):类型/大小/修改时间/加载状态
|
||||
if (ImGui::IsItemHovered())
|
||||
{
|
||||
ImGui::BeginTooltip();
|
||||
ImGui::TextUnformatted(name.c_str());
|
||||
ImGui::Separator();
|
||||
// 与 Kind 枚举逐位对齐(Kind::Other = 5)——漏项 = tooltip
|
||||
// 数组越界读
|
||||
static const char* kKindNames[] = {"Model", "Texture/Environment",
|
||||
"Scene", "Material",
|
||||
"Controller", "File"};
|
||||
ImGui::Text("type: %s", kKindNames[static_cast<int>(item.kind)]);
|
||||
namespace fs = std::filesystem;
|
||||
std::error_code ec;
|
||||
const auto sz = fs::file_size(item.path, ec);
|
||||
if (!ec)
|
||||
{
|
||||
char szBuf[32];
|
||||
if (sz >= 1024ull * 1024ull)
|
||||
{
|
||||
std::snprintf(szBuf, sizeof(szBuf), "%.1f MB",
|
||||
static_cast<double>(sz) / (1024.0 * 1024.0));
|
||||
}
|
||||
else
|
||||
{
|
||||
std::snprintf(szBuf, sizeof(szBuf), "%.1f KB",
|
||||
static_cast<double>(sz) / 1024.0);
|
||||
}
|
||||
ImGui::Text("size: %s", szBuf);
|
||||
}
|
||||
const auto ftime = fs::last_write_time(item.path, ec);
|
||||
if (!ec)
|
||||
{
|
||||
// file_clock → system_clock 近似转换(C++17 无官方转换)
|
||||
const auto sctp = std::chrono::time_point_cast<
|
||||
std::chrono::system_clock::duration>(
|
||||
ftime - fs::file_time_type::clock::now() +
|
||||
std::chrono::system_clock::now());
|
||||
const std::time_t tt = std::chrono::system_clock::to_time_t(sctp);
|
||||
char tmBuf[32];
|
||||
std::strftime(tmBuf, sizeof(tmBuf), "%Y-%m-%d %H:%M",
|
||||
std::localtime(&tt));
|
||||
ImGui::Text("modified: %s", tmBuf);
|
||||
}
|
||||
// 加载状态(只查询不触发加载;model/HDR 进过缓存才显示 loaded)
|
||||
if (ctx.assets)
|
||||
{
|
||||
const char* state = "not loaded";
|
||||
if (item.kind == Kind::Model &&
|
||||
ctx.assets->IsModelLoaded(assetPath))
|
||||
{
|
||||
state = "loaded";
|
||||
}
|
||||
else if (item.kind == Kind::Texture &&
|
||||
ctx.assets->IsEnvironmentLoaded(assetPath))
|
||||
{
|
||||
state = "loaded";
|
||||
}
|
||||
ImGui::Text("state: %s", state);
|
||||
// 已加载的 model 展开资产数据概览(meshes=0 即纯动画
|
||||
// 容器;未加载不显示——查询不触发加载)
|
||||
if (item.kind == Kind::Model)
|
||||
{
|
||||
if (const Engine::Model* m =
|
||||
ctx.assets->GetModelByPath(assetPath);
|
||||
m != nullptr)
|
||||
{
|
||||
ImGui::Text("meshes: %u, bones: %u, clips: %u",
|
||||
static_cast<unsigned>(m->GetMeshes().size()),
|
||||
m->GetBoneCount(),
|
||||
static_cast<unsigned>(m->GetClips().size()));
|
||||
}
|
||||
}
|
||||
}
|
||||
ImGui::TextDisabled("%s", assetPath.c_str());
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
//
|
||||
// AssetBrowserPanel:项目资产浏览器(Unity Project 窗口简化版)。
|
||||
//
|
||||
// 左侧目录树 + 右侧文件列表(类型过滤 + 名字过滤)。与 AssetManager
|
||||
// 的集成点是"双击预热":model/HDR 进缓存去重(重复双击零成本),
|
||||
// .scene 提示走 File 菜单(加载含环境恢复/选中清理等流程,不在面板
|
||||
// 重复)。拖拽到视口/Inspector 属后续迭代。
|
||||
//
|
||||
// 目录每帧扫描:项目级目录条目有限(几十量级),filesystem 迭代开销
|
||||
// 可忽略;资产规模上来后再加缓存脏标。
|
||||
//
|
||||
|
||||
#ifndef HELLOVULKAN_ASSETBROWSERPANEL_H
|
||||
#define HELLOVULKAN_ASSETBROWSERPANEL_H
|
||||
|
||||
#include "EditorContext.h"
|
||||
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
|
||||
class AssetBrowserPanel {
|
||||
public:
|
||||
void Draw(const EditorContext& ctx);
|
||||
|
||||
private:
|
||||
// 目录树节点(递归);右侧列表画 dir 下的文件
|
||||
void DrawDirTree(const EditorContext& ctx,
|
||||
const std::filesystem::path& dir);
|
||||
void DrawFileList(const EditorContext& ctx,
|
||||
const std::filesystem::path& dir);
|
||||
|
||||
// 固定根(下标即身份):项目资产 / 场景文件
|
||||
int m_Root = 0;
|
||||
// 当前浏览目录(点目录树切换;列表显示这里的文件)
|
||||
std::filesystem::path m_CurrentDir;
|
||||
// 类型过滤(0 = All;其余见 kTypeNames)
|
||||
int m_TypeFilter = 0;
|
||||
// 名字子串过滤(空 = 不过滤)
|
||||
char m_NameFilter[64] = "";
|
||||
};
|
||||
|
||||
#endif //HELLOVULKAN_ASSETBROWSERPANEL_H
|
||||
@@ -0,0 +1,786 @@
|
||||
//
|
||||
// ControllerEditorPanel 实现:见头文件注释。
|
||||
//
|
||||
|
||||
#include "ControllerEditorPanel.h"
|
||||
|
||||
#include <Engine/Animation/AnimationController.h>
|
||||
#include <Engine/ECS/Components.h>
|
||||
#include <Engine/Renderer/Model.h>
|
||||
|
||||
#include <imgui.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
|
||||
namespace {
|
||||
|
||||
ImVec4 WarnCol() { return ImVec4(1.0f, 0.75f, 0.2f, 1.0f); }
|
||||
|
||||
// 模型首个 clip 名(children/树新建时的默认引用;无模型 = 空)
|
||||
std::string FirstClipName(const Engine::Model* model)
|
||||
{
|
||||
if (model == nullptr || model->GetClips().empty()) return "";
|
||||
return model->GetClips()[0].name;
|
||||
}
|
||||
|
||||
// clip 引用编辑:选中实体有模型 → combo(模型 clip 列表);
|
||||
// 无模型 → InputText 手填(clip 缺失 = 该层 rest 兜底,不崩)
|
||||
bool ClipRefEdit(const char* label, std::string& clip,
|
||||
const Engine::Model* model)
|
||||
{
|
||||
if (model == nullptr || model->GetClips().empty())
|
||||
{
|
||||
char buf[128];
|
||||
snprintf(buf, sizeof(buf), "%s", clip.c_str());
|
||||
ImGui::SetNextItemWidth(-1.0f);
|
||||
if (ImGui::InputText(label, buf, sizeof(buf)))
|
||||
{
|
||||
clip = buf;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
std::vector<const char*> names;
|
||||
names.reserve(model->GetClips().size());
|
||||
int sel = -1;
|
||||
for (const auto& c : model->GetClips())
|
||||
{
|
||||
names.push_back(c.name.c_str());
|
||||
if (c.name == clip) sel = static_cast<int>(names.size()) - 1;
|
||||
}
|
||||
if (sel < 0) sel = 0;
|
||||
if (ImGui::Combo(label, &sel, names.data(),
|
||||
static_cast<int>(names.size())))
|
||||
{
|
||||
clip = model->GetClips()[static_cast<size_t>(sel)].name;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void ControllerEditorPanel::Draw(const EditorContext& ctx)
|
||||
{
|
||||
if (!m_Open || m_Path.empty()) return;
|
||||
|
||||
Engine::Animation::AnimationController* ctrl =
|
||||
ctx.assets->FindController(m_Path);
|
||||
if (!ImGui::Begin("Controller Editor", &m_Open))
|
||||
{
|
||||
ImGui::End();
|
||||
return;
|
||||
}
|
||||
if (ctrl == nullptr)
|
||||
{
|
||||
ImGui::TextColored(WarnCol(), "'%s' failed to load",
|
||||
m_Path.c_str());
|
||||
if (ImGui::Button("Reload"))
|
||||
{
|
||||
ctx.assets->ReloadController(m_Path);
|
||||
}
|
||||
ImGui::End();
|
||||
return;
|
||||
}
|
||||
|
||||
// ---- 头部:路径 + Save(落盘 + Reload 重编译条件)----
|
||||
ImGui::TextUnformatted(m_Path.c_str());
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Save"))
|
||||
{
|
||||
if (ctrl->Save(m_Path))
|
||||
{
|
||||
ctx.assets->ReloadController(m_Path);
|
||||
ctrl = ctx.assets->FindController(m_Path);
|
||||
}
|
||||
}
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled("edits apply live; Save writes the file");
|
||||
ImGui::Separator();
|
||||
|
||||
DrawPreview(ctx);
|
||||
ImGui::Separator();
|
||||
DrawStates(ctx, ctrl);
|
||||
ImGui::Separator();
|
||||
DrawTransitions(ctx, ctrl);
|
||||
ImGui::Separator();
|
||||
DrawParams(ctx, ctrl);
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
// ---- Preview:方向盘(拖动 = SetFloat 到选中实体,摇杆回中)----
|
||||
void ControllerEditorPanel::DrawPreview(const EditorContext& ctx)
|
||||
{
|
||||
// 选中实体的 AnimationComponent(Controller 模式才驱动)
|
||||
Engine::ECS::AnimationComponent* anim = nullptr;
|
||||
const Engine::Model* model = nullptr;
|
||||
if (ctx.selected != nullptr && ctx.scene != nullptr &&
|
||||
ctx.scene->GetRegistry().valid(*ctx.selected))
|
||||
{
|
||||
auto& reg = ctx.scene->GetRegistry();
|
||||
if (auto* a =
|
||||
reg.try_get<Engine::ECS::AnimationComponent>(*ctx.selected))
|
||||
{
|
||||
anim = a;
|
||||
if (auto* mr =
|
||||
reg.try_get<Engine::ECS::MeshRendererComponent>(
|
||||
*ctx.selected))
|
||||
{
|
||||
if (mr->model.Valid())
|
||||
{
|
||||
model = ctx.assets->GetModel(mr->model);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (anim == nullptr)
|
||||
{
|
||||
ImGui::TextDisabled("select an entity with an Animation component "
|
||||
"to preview");
|
||||
return;
|
||||
}
|
||||
|
||||
Engine::Animation::AnimationController* ctrl =
|
||||
anim->controllerPath.empty()
|
||||
? nullptr
|
||||
: ctx.assets->FindController(anim->controllerPath);
|
||||
if (ctrl == nullptr) return;
|
||||
|
||||
// 面板选中状态的 2D 树(预览读出;拖动写的是实体参数)
|
||||
const Engine::Animation::ControllerState* padState = nullptr;
|
||||
const auto& order = ctrl->GetStateOrder();
|
||||
if (m_SelState >= 0 && m_SelState < static_cast<int>(order.size()))
|
||||
{
|
||||
const auto& s = ctrl->GetStates().at(order[static_cast<size_t>(
|
||||
m_SelState)]);
|
||||
if (s.tree.type ==
|
||||
Engine::Animation::BlendTree::Type::Directional2D)
|
||||
{
|
||||
padState = &s;
|
||||
}
|
||||
}
|
||||
if (padState == nullptr)
|
||||
{
|
||||
ImGui::TextDisabled("preview: select a 2DDirectional state");
|
||||
return;
|
||||
}
|
||||
|
||||
const float S = 190.0f;
|
||||
const float rad = S * 0.5f - 14.0f;
|
||||
const ImVec2 org = ImGui::GetCursorScreenPos();
|
||||
const ImVec2 ctr(org.x + S * 0.5f, org.y + S * 0.5f);
|
||||
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||
dl->AddCircleFilled(ctr, rad, IM_COL32(30, 30, 34, 255));
|
||||
dl->AddCircle(ctr, rad, IM_COL32(90, 90, 100, 255), 48, 1.5f);
|
||||
dl->AddLine(ImVec2(ctr.x - rad, ctr.y), ImVec2(ctr.x + rad, ctr.y),
|
||||
IM_COL32(60, 60, 68, 255));
|
||||
dl->AddLine(ImVec2(ctr.x, ctr.y - rad), ImVec2(ctr.x, ctr.y + rad),
|
||||
IM_COL32(60, 60, 68, 255));
|
||||
const auto& ch = padState->tree.children;
|
||||
for (const auto& c : ch)
|
||||
{
|
||||
const bool isOrigin =
|
||||
c.pos.x * c.pos.x + c.pos.y * c.pos.y < 1e-8f;
|
||||
const ImVec2 p(ctr.x + c.pos.x * rad, ctr.y - c.pos.y * rad);
|
||||
dl->AddCircleFilled(p, isOrigin ? 5.0f : 4.0f,
|
||||
isOrigin ? IM_COL32(90, 140, 255, 255)
|
||||
: IM_COL32(220, 220, 225, 255));
|
||||
}
|
||||
// mode=Direct 的实体不跑状态机——参数写进去没有任何效果,
|
||||
// 直接禁用方向盘(Unity:Animator 组件没挂 Controller 时
|
||||
// 参数面板不可编辑,同语义)
|
||||
const bool isCtrlMode =
|
||||
anim->mode == Engine::ECS::AnimationComponent::AnimMode::Controller;
|
||||
if (!isCtrlMode)
|
||||
{
|
||||
ImGui::TextColored(WarnCol(),
|
||||
"entity is in Direct mode - switch Mode to "
|
||||
"Controller to preview");
|
||||
return;
|
||||
}
|
||||
ImGui::InvisibleButton("##dirpad", ImVec2(S, S));
|
||||
const auto getF = [&](const std::string& n) -> float {
|
||||
auto it = anim->floatParams.find(n);
|
||||
return it != anim->floatParams.end() ? it->second : 0.0f;
|
||||
};
|
||||
float cx = getF(padState->tree.xParam);
|
||||
float cz = getF(padState->tree.zParam);
|
||||
if (ImGui::IsItemActivated())
|
||||
{
|
||||
// 预览闭环(Unity Animator 窗口语义:选中状态即可预览该
|
||||
// 状态)——实体停在别的状态时拖盘自动切到面板选中的 2D
|
||||
// 树状态,否则 SetFloat 对 Single 状态完全无效
|
||||
if (anim->currentState != order[static_cast<size_t>(m_SelState)])
|
||||
{
|
||||
anim->CrossFade(order[static_cast<size_t>(m_SelState)], 0.1f);
|
||||
}
|
||||
}
|
||||
if (ImGui::IsItemActive())
|
||||
{
|
||||
const ImVec2 m = ImGui::GetIO().MousePos;
|
||||
float vx = (m.x - ctr.x) / rad;
|
||||
float vz = -(m.y - ctr.y) / rad;
|
||||
const float len = glm::length(glm::vec2(vx, vz));
|
||||
if (len > 1.0f)
|
||||
{
|
||||
vx /= len;
|
||||
vz /= len;
|
||||
}
|
||||
anim->SetFloat(padState->tree.xParam, vx);
|
||||
anim->SetFloat(padState->tree.zParam, vz);
|
||||
cx = vx;
|
||||
cz = vz;
|
||||
}
|
||||
else if (ImGui::IsItemDeactivatedAfterEdit())
|
||||
{
|
||||
anim->SetFloat(padState->tree.xParam, 0.0f);
|
||||
anim->SetFloat(padState->tree.zParam, 0.0f);
|
||||
cx = cz = 0.0f;
|
||||
}
|
||||
if (ImGui::IsItemHovered())
|
||||
{
|
||||
const ImVec2 m = ImGui::GetIO().MousePos;
|
||||
for (const auto& c : ch)
|
||||
{
|
||||
const ImVec2 p(ctr.x + c.pos.x * rad, ctr.y - c.pos.y * rad);
|
||||
const float dx = m.x - p.x;
|
||||
const float dy = m.y - p.y;
|
||||
if (dx * dx + dy * dy < 64.0f)
|
||||
{
|
||||
ImGui::SetTooltip("%s", c.clip.c_str());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
const ImVec2 cur(ctr.x + cx * rad, ctr.y - cz * rad);
|
||||
dl->AddCircleFilled(cur, 6.0f, IM_COL32(255, 170, 60, 255));
|
||||
dl->AddCircle(cur, 9.0f, IM_COL32(255, 170, 60, 160), 0, 1.5f);
|
||||
|
||||
// 右侧读出:当前参数 + 选中状态树的权重(运行时同一数学)
|
||||
ImGui::SameLine();
|
||||
ImGui::BeginGroup();
|
||||
ImGui::Text("preview: %s", anim->currentState.c_str());
|
||||
ImGui::Text("%.2f / %.2f", cx, cz);
|
||||
std::vector<std::pair<Engine::u32, float>> ws;
|
||||
Engine::Animation::EvaluateBlendTree2D(padState->tree, cx, cz, ws);
|
||||
std::sort(ws.begin(), ws.end(),
|
||||
[](const auto& a, const auto& b)
|
||||
{ return a.second > b.second; });
|
||||
for (const auto& [idx, w] : ws)
|
||||
{
|
||||
ImGui::Text("%s %.2f", ch[idx].clip.c_str(), w);
|
||||
}
|
||||
ImGui::EndGroup();
|
||||
}
|
||||
|
||||
// ---- States:列表(entry ★)+ 选中状态详情 + children 编辑 ----
|
||||
void ControllerEditorPanel::DrawStates(
|
||||
const EditorContext& ctx, Engine::Animation::AnimationController* ctrl)
|
||||
{
|
||||
if (!ImGui::TreeNodeEx("States", ImGuiTreeNodeFlags_DefaultOpen))
|
||||
{
|
||||
return;
|
||||
}
|
||||
auto& order = ctrl->StateOrderMut();
|
||||
auto& states = ctrl->StatesMut();
|
||||
|
||||
// 选中实体模型(children 的 clip 下拉来源)
|
||||
const Engine::Model* model = nullptr;
|
||||
if (ctx.selected != nullptr && ctx.scene != nullptr &&
|
||||
ctx.scene->GetRegistry().valid(*ctx.selected))
|
||||
{
|
||||
if (auto* mr =
|
||||
ctx.scene->GetRegistry()
|
||||
.try_get<Engine::ECS::MeshRendererComponent>(
|
||||
*ctx.selected))
|
||||
{
|
||||
if (mr->model.Valid())
|
||||
{
|
||||
model = ctx.assets->GetModel(mr->model);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 状态列表 + Add
|
||||
int delIdx = -1;
|
||||
for (int i = 0; i < static_cast<int>(order.size()); ++i)
|
||||
{
|
||||
const bool isEntry = order[static_cast<size_t>(i)] ==
|
||||
ctrl->GetEntryState();
|
||||
ImGui::PushID(i);
|
||||
if (ImGui::Selectable(
|
||||
(order[static_cast<size_t>(i)] +
|
||||
std::string(isEntry ? " [entry]" : ""))
|
||||
.c_str(),
|
||||
i == m_SelState))
|
||||
{
|
||||
m_SelState = i;
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
{
|
||||
static char s_NewName[64] = "";
|
||||
ImGui::SetNextItemWidth(120.0f);
|
||||
ImGui::InputText("##new_state", s_NewName, sizeof(s_NewName));
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("+ Add") && s_NewName[0] != '\0' &&
|
||||
states.find(s_NewName) == states.end())
|
||||
{
|
||||
const std::string n = s_NewName;
|
||||
states.emplace(n, Engine::Animation::ControllerState{n});
|
||||
order.push_back(n);
|
||||
m_SelState = static_cast<int>(order.size()) - 1;
|
||||
s_NewName[0] = '\0';
|
||||
}
|
||||
}
|
||||
if (m_SelState >= static_cast<int>(order.size()))
|
||||
{
|
||||
m_SelState = static_cast<int>(order.size()) - 1;
|
||||
}
|
||||
if (m_SelState < 0 || order.empty())
|
||||
{
|
||||
ImGui::TreePop();
|
||||
return;
|
||||
}
|
||||
|
||||
// ---- 选中状态详情 ----
|
||||
const std::string selName = order[static_cast<size_t>(m_SelState)];
|
||||
auto it = states.find(selName);
|
||||
if (it == states.end())
|
||||
{
|
||||
ImGui::TreePop();
|
||||
return;
|
||||
}
|
||||
auto& st = it->second;
|
||||
ImGui::Indent();
|
||||
|
||||
ImGui::TextUnformatted("editing:");
|
||||
ImGui::SameLine();
|
||||
ImGui::TextUnformatted(selName.c_str());
|
||||
if (selName != ctrl->GetEntryState() &&
|
||||
ImGui::SmallButton("Set as Entry"))
|
||||
{
|
||||
ctrl->SetEntry(selName);
|
||||
}
|
||||
ImGui::SameLine();
|
||||
ImGui::PushID("delstate");
|
||||
if (ImGui::SmallButton("x Delete"))
|
||||
{
|
||||
delIdx = m_SelState;
|
||||
}
|
||||
ImGui::PopID();
|
||||
// 重命名(级联:transitions / transitionsOnEnd / entry 引用同步)
|
||||
{
|
||||
static char s_Rename[64] = "";
|
||||
ImGui::SetNextItemWidth(120.0f);
|
||||
ImGui::InputText("##rename", s_Rename, sizeof(s_Rename));
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("Rename") && s_Rename[0] != '\0' &&
|
||||
states.find(s_Rename) == states.end())
|
||||
{
|
||||
const std::string oldN = selName;
|
||||
const std::string newN = s_Rename;
|
||||
auto node = states.extract(oldN);
|
||||
if (node)
|
||||
{
|
||||
node.key() = newN;
|
||||
states.insert(std::move(node));
|
||||
order[static_cast<size_t>(m_SelState)] = newN;
|
||||
for (auto& t : ctrl->TransitionsMut())
|
||||
{
|
||||
if (t.from == oldN) t.from = newN;
|
||||
if (t.to == oldN) t.to = newN;
|
||||
}
|
||||
for (auto& [n, s] : states)
|
||||
{
|
||||
for (auto& e : s.transitionsOnEnd)
|
||||
{
|
||||
if (e == oldN) e = newN;
|
||||
}
|
||||
}
|
||||
if (ctrl->GetEntryState() == oldN)
|
||||
{
|
||||
ctrl->SetEntry(newN);
|
||||
}
|
||||
s_Rename[0] = '\0';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 播放分组(speed/loop/播完出口)----
|
||||
if (ImGui::TreeNode("Playback"))
|
||||
{
|
||||
ImGui::DragFloat("speed", &st.speed, 0.05f, 0.1f, 10.0f, "%.2f");
|
||||
ImGui::Checkbox("loop", &st.loop);
|
||||
if (!st.loop)
|
||||
{
|
||||
// 播完出口列表(按序首个可用)+ 下拉追加
|
||||
for (size_t ei = 0; ei < st.transitionsOnEnd.size(); ++ei)
|
||||
{
|
||||
ImGui::PushID(static_cast<int>(ei) + 1000);
|
||||
ImGui::BulletText("%s",
|
||||
st.transitionsOnEnd[ei].c_str());
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("x"))
|
||||
{
|
||||
st.transitionsOnEnd.erase(
|
||||
st.transitionsOnEnd.begin() +
|
||||
static_cast<long>(ei));
|
||||
ImGui::PopID();
|
||||
break;
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
if (!order.empty())
|
||||
{
|
||||
static int s_OnEndSel = 0;
|
||||
s_OnEndSel = std::clamp(
|
||||
s_OnEndSel, 0,
|
||||
static_cast<int>(order.size()) - 1);
|
||||
ImGui::SetNextItemWidth(120.0f);
|
||||
ImGui::Combo(
|
||||
"##onend", &s_OnEndSel,
|
||||
[](void* d, int i) -> const char* {
|
||||
return (*static_cast<std::vector<std::string>*>(
|
||||
d))[static_cast<size_t>(i)]
|
||||
.c_str();
|
||||
},
|
||||
&order, static_cast<int>(order.size()));
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("+ exit"))
|
||||
{
|
||||
st.transitionsOnEnd.push_back(
|
||||
order[static_cast<size_t>(s_OnEndSel)]);
|
||||
}
|
||||
}
|
||||
}
|
||||
ImGui::TreePop();
|
||||
}
|
||||
|
||||
// ---- 混合树分组 ----
|
||||
if (ImGui::TreeNode("Blend Tree"))
|
||||
{
|
||||
using BTType = Engine::Animation::BlendTree::Type;
|
||||
// 树类型:Single 单 clip / 1D 阈值 / 2D 平面方向
|
||||
int tt = static_cast<int>(st.tree.type);
|
||||
if (ImGui::Combo("type", &tt,
|
||||
"Single\0Directional 1D\0Directional 2D\0"))
|
||||
{
|
||||
st.tree.type = static_cast<BTType>(tt);
|
||||
st.tree.children.clear();
|
||||
if (st.tree.type == BTType::Single)
|
||||
{
|
||||
if (st.tree.clip.empty())
|
||||
{
|
||||
st.tree.clip = FirstClipName(model);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 默认一个原点 child(clip 取模型首个;无模型 = 手填)
|
||||
st.tree.children.push_back(
|
||||
{FirstClipName(model), glm::vec2{0.0f, 0.0f}});
|
||||
}
|
||||
}
|
||||
// x/z 参数绑定(float 参数下拉)
|
||||
{
|
||||
std::vector<const char*> pnames;
|
||||
for (const auto& p : ctrl->GetParams())
|
||||
{
|
||||
if (p.type ==
|
||||
Engine::Animation::ControllerParam::Type::Float)
|
||||
{
|
||||
pnames.push_back(p.name.c_str());
|
||||
}
|
||||
}
|
||||
if (!pnames.empty())
|
||||
{
|
||||
if (st.tree.type != BTType::Single)
|
||||
{
|
||||
int xi = 0;
|
||||
for (int k = 0; k < static_cast<int>(pnames.size());
|
||||
++k)
|
||||
{
|
||||
if (st.tree.xParam ==
|
||||
pnames[static_cast<size_t>(k)])
|
||||
xi = k;
|
||||
}
|
||||
ImGui::SetNextItemWidth(140.0f);
|
||||
if (ImGui::Combo("x param", &xi, pnames.data(),
|
||||
static_cast<int>(pnames.size())))
|
||||
{
|
||||
st.tree.xParam = pnames[static_cast<size_t>(xi)];
|
||||
}
|
||||
}
|
||||
if (st.tree.type == BTType::Directional2D)
|
||||
{
|
||||
int zi = 0;
|
||||
for (int k = 0; k < static_cast<int>(pnames.size());
|
||||
++k)
|
||||
{
|
||||
if (st.tree.zParam ==
|
||||
pnames[static_cast<size_t>(k)])
|
||||
zi = k;
|
||||
}
|
||||
ImGui::SetNextItemWidth(140.0f);
|
||||
if (ImGui::Combo("z param", &zi, pnames.data(),
|
||||
static_cast<int>(pnames.size())))
|
||||
{
|
||||
st.tree.zParam = pnames[static_cast<size_t>(zi)];
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (st.tree.type != BTType::Single)
|
||||
{
|
||||
ImGui::TextColored(
|
||||
WarnCol(), "need a float param for the blend axis");
|
||||
}
|
||||
}
|
||||
if (st.tree.type == BTType::Single)
|
||||
{
|
||||
ClipRefEdit("clip", st.tree.clip, model);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 原点点缺失警告:v 回到中心时方向层权重全被最小阈值滤
|
||||
// 掉,层空 → 引擎侧 fallback 到首个有效 child 单层(不是
|
||||
// T-pose),但"中心=默认动作"的直觉语义要靠原点点承载
|
||||
if (st.tree.type == BTType::Directional2D)
|
||||
{
|
||||
bool hasOrigin = false;
|
||||
for (const auto& c : st.tree.children)
|
||||
{
|
||||
if (c.pos.x * c.pos.x + c.pos.y * c.pos.y < 1e-8f)
|
||||
hasOrigin = true;
|
||||
}
|
||||
if (!hasOrigin)
|
||||
{
|
||||
ImGui::TextColored(
|
||||
WarnCol(), "! no origin child (at 0,0) - center "
|
||||
"falls back to first child");
|
||||
}
|
||||
}
|
||||
// children 表:clip + 定位 + 删除(2D = (x,y) 平面定位点;
|
||||
// 1D = x 即阈值)。编辑即时生效(System 下帧直读缓存实例)
|
||||
int delChild = -1;
|
||||
for (int ci = 0;
|
||||
ci < static_cast<int>(st.tree.children.size()); ++ci)
|
||||
{
|
||||
auto& c = st.tree.children[static_cast<size_t>(ci)];
|
||||
ImGui::PushID(ci);
|
||||
ClipRefEdit("##clip", c.clip, model);
|
||||
if (st.tree.type == BTType::Directional2D)
|
||||
{
|
||||
ImGui::SetNextItemWidth(70.0f);
|
||||
ImGui::DragFloat("##atx", &c.pos.x, 0.01f, -1.0f,
|
||||
1.0f, "x %.2f");
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(70.0f);
|
||||
ImGui::DragFloat("##aty", &c.pos.y, 0.01f, -1.0f,
|
||||
1.0f, "y %.2f");
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui::SetNextItemWidth(120.0f);
|
||||
ImGui::DragFloat("##thr", &c.pos.x, 0.05f, -10.0f,
|
||||
10.0f, "thr %.2f");
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("x")) delChild = ci;
|
||||
ImGui::PopID();
|
||||
}
|
||||
if (delChild >= 0)
|
||||
{
|
||||
st.tree.children.erase(
|
||||
st.tree.children.begin() + delChild);
|
||||
}
|
||||
if (ImGui::SmallButton("+ Add Child"))
|
||||
{
|
||||
st.tree.children.push_back(
|
||||
{FirstClipName(model),
|
||||
st.tree.type == BTType::Directional2D
|
||||
? glm::vec2{1.0f, 0.0f}
|
||||
: glm::vec2{0.0f, 0.0f}});
|
||||
}
|
||||
}
|
||||
ImGui::TreePop(); // Blend Tree
|
||||
}
|
||||
ImGui::Unindent();
|
||||
|
||||
if (delIdx >= 0)
|
||||
{
|
||||
const std::string name = order[static_cast<size_t>(delIdx)];
|
||||
states.erase(name);
|
||||
order.erase(order.begin() + delIdx);
|
||||
auto& trs = ctrl->TransitionsMut();
|
||||
for (int i = static_cast<int>(trs.size()) - 1; i >= 0; --i)
|
||||
{
|
||||
if (trs[static_cast<size_t>(i)].from == name ||
|
||||
trs[static_cast<size_t>(i)].to == name)
|
||||
{
|
||||
trs.erase(trs.begin() + i);
|
||||
}
|
||||
}
|
||||
for (auto& [n, s] : states)
|
||||
{
|
||||
std::erase_if(s.transitionsOnEnd,
|
||||
[&](const std::string& e)
|
||||
{ return e == name; });
|
||||
}
|
||||
if (ctrl->GetEntryState() == name)
|
||||
{
|
||||
ctrl->SetEntry(order.empty() ? "" : order[0]);
|
||||
}
|
||||
m_SelState = 0;
|
||||
}
|
||||
ImGui::TreePop();
|
||||
}
|
||||
|
||||
// ---- Transitions:行编辑(from/to 下拉 + cond 字符串 Enter 编译)----
|
||||
void ControllerEditorPanel::DrawTransitions(
|
||||
const EditorContext& ctx, Engine::Animation::AnimationController* ctrl)
|
||||
{
|
||||
(void)ctx;
|
||||
if (!ImGui::TreeNode("Transitions")) return;
|
||||
const auto& order = ctrl->GetStateOrder();
|
||||
std::vector<const char*> snames;
|
||||
snames.reserve(order.size() + 1);
|
||||
snames.push_back("*"); // AnyState
|
||||
for (const auto& n : order) snames.push_back(n.c_str());
|
||||
auto stateCombo = [&](const char* label, std::string& cur) {
|
||||
int sel = 0;
|
||||
for (int k = 0; k < static_cast<int>(snames.size()); ++k)
|
||||
{
|
||||
if (cur == snames[static_cast<size_t>(k)]) sel = k;
|
||||
}
|
||||
ImGui::SetNextItemWidth(110.0f);
|
||||
if (ImGui::Combo(label, &sel, snames.data(),
|
||||
static_cast<int>(snames.size())))
|
||||
{
|
||||
cur = snames[static_cast<size_t>(sel)];
|
||||
}
|
||||
};
|
||||
|
||||
int delIdx = -1;
|
||||
int ti = 0;
|
||||
for (auto& t : ctrl->TransitionsMut())
|
||||
{
|
||||
ImGui::PushID(ti++);
|
||||
// 行 1:来源 -> 目标(+ 删除)
|
||||
stateCombo("##from", t.from);
|
||||
ImGui::SameLine();
|
||||
ImGui::TextUnformatted("->");
|
||||
ImGui::SameLine();
|
||||
stateCombo("##to", t.to);
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("x")) delIdx = ti - 1;
|
||||
// 行 2:条件 + fade + exitTime(缩进——同一过渡的属性组)
|
||||
ImGui::Indent(18.0f);
|
||||
// cond:反推字符串 → Enter(DeactivatedAfterEdit)就地重编译
|
||||
{
|
||||
const std::string s =
|
||||
t.cond.always ? "" :
|
||||
Engine::Animation::AnimationController::CondToString(
|
||||
t.cond);
|
||||
char buf[96];
|
||||
snprintf(buf, sizeof(buf), "%s", s.c_str());
|
||||
ImGui::SetNextItemWidth(170.0f);
|
||||
if (ImGui::InputText("##cond", buf, sizeof(buf),
|
||||
ImGuiInputTextFlags_EnterReturnsTrue) ||
|
||||
ImGui::IsItemDeactivatedAfterEdit())
|
||||
{
|
||||
t.cond = Engine::Animation::AnimationController::ParseCond(
|
||||
buf);
|
||||
}
|
||||
if (!t.cond.always && !t.cond.valid)
|
||||
{
|
||||
ImGui::SameLine();
|
||||
ImGui::TextColored(WarnCol(), "!");
|
||||
}
|
||||
}
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(60.0f);
|
||||
ImGui::DragFloat("##fade", &t.fade, 0.02f, 0.0f, 5.0f, "%.2f");
|
||||
ImGui::SameLine();
|
||||
ImGui::Checkbox("##exit", &t.hasExitTime);
|
||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("exit time");
|
||||
ImGui::Unindent(18.0f);
|
||||
ImGui::PopID();
|
||||
}
|
||||
if (delIdx >= 0)
|
||||
{
|
||||
auto& trs = ctrl->TransitionsMut();
|
||||
trs.erase(trs.begin() + delIdx);
|
||||
}
|
||||
if (ImGui::SmallButton("+ Add") && !order.empty())
|
||||
{
|
||||
Engine::Animation::Transition tr;
|
||||
tr.from = order[0];
|
||||
tr.to = order[order.size() > 1 ? 1 : 0];
|
||||
ctrl->TransitionsMut().push_back(tr);
|
||||
}
|
||||
ImGui::TreePop();
|
||||
}
|
||||
|
||||
// ---- Params:参数声明编辑(禁改名——被 tree/cond 引用,改了断链)----
|
||||
void ControllerEditorPanel::DrawParams(
|
||||
const EditorContext& ctx, Engine::Animation::AnimationController* ctrl)
|
||||
{
|
||||
(void)ctx;
|
||||
if (!ImGui::TreeNode("Params")) return;
|
||||
auto& ps = ctrl->ParamsMut();
|
||||
int delIdx = -1;
|
||||
int pi = 0;
|
||||
for (auto& p : ps)
|
||||
{
|
||||
ImGui::PushID(pi++);
|
||||
ImGui::TextUnformatted(p.name.c_str());
|
||||
ImGui::SameLine();
|
||||
int ty = static_cast<int>(p.type);
|
||||
ImGui::SetNextItemWidth(80.0f);
|
||||
if (ImGui::Combo("##ty", &ty, "float\0bool\0trigger\0"))
|
||||
{
|
||||
p.type =
|
||||
static_cast<Engine::Animation::ControllerParam::Type>(ty);
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (p.type ==
|
||||
Engine::Animation::ControllerParam::Type::Float)
|
||||
{
|
||||
ImGui::SetNextItemWidth(90.0f);
|
||||
ImGui::DragFloat("##def", &p.defFloat, 0.05f);
|
||||
}
|
||||
else if (p.type ==
|
||||
Engine::Animation::ControllerParam::Type::Bool)
|
||||
{
|
||||
ImGui::Checkbox("##def", &p.defBool);
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("x")) delIdx = pi - 1;
|
||||
ImGui::PopID();
|
||||
}
|
||||
if (delIdx >= 0) ps.erase(ps.begin() + delIdx);
|
||||
{
|
||||
static char s_NewName[64] = "";
|
||||
ImGui::SetNextItemWidth(120.0f);
|
||||
ImGui::InputText("##new_param", s_NewName, sizeof(s_NewName));
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("+ Add") && s_NewName[0] != '\0')
|
||||
{
|
||||
bool dup = false;
|
||||
for (const auto& p : ps)
|
||||
{
|
||||
if (p.name == s_NewName) dup = true;
|
||||
}
|
||||
if (!dup)
|
||||
{
|
||||
Engine::Animation::ControllerParam d;
|
||||
d.name = s_NewName;
|
||||
ps.push_back(d);
|
||||
s_NewName[0] = '\0';
|
||||
}
|
||||
}
|
||||
}
|
||||
ImGui::TreePop();
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
//
|
||||
// ControllerEditorPanel:动画控制器编辑器窗口(Unity Animator 状态机
|
||||
// 面板范式,交互对齐 MaterialEditorPanel 的"指派/编辑分离")。
|
||||
//
|
||||
// Details 的 Controller 区只做指派(槽/Create/双击打开本面板);
|
||||
// 结构编辑全在这里:
|
||||
// - Preview:方向盘(2D 混合树可视化驱动)+ 权重实时读出,拖动
|
||||
// SetFloat 到当前选中实体(Controller 模式)实时预览
|
||||
// - States:状态列表(增删/重命名/entry),选中状态详情 = 播放
|
||||
// 参数 + 混合树类型 + children 逐行编辑(clip 指定/定位/删除)
|
||||
// - Transitions / Params:列表编辑,cond 字符串 Enter 就地编译
|
||||
//
|
||||
// 编辑直接写缓存资产实例(System 下一帧生效,无需保存);Save 落盘
|
||||
// + ReloadController 重编译(实例换新,本面板每帧重解析不持指针)。
|
||||
//
|
||||
|
||||
#ifndef HELLOVULKAN_CONTROLLEREDITORPANEL_H
|
||||
#define HELLOVULKAN_CONTROLLEREDITORPANEL_H
|
||||
|
||||
#include "EditorContext.h"
|
||||
|
||||
#include <Engine/Core/Types.h>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
// .animctrl 拖拽 payload(AssetBrowser → Details Controller 槽指派)
|
||||
inline constexpr char kControllerDragPayload[] = "CTRL_PATH";
|
||||
|
||||
class ControllerEditorPanel {
|
||||
public:
|
||||
// 打开 .animctrl 资产(Details 槽位 Edit 按钮 / Content Browser
|
||||
// 双击入口)
|
||||
void Open(const std::string& path)
|
||||
{
|
||||
m_Path = path;
|
||||
m_Open = true;
|
||||
m_SelState = 0;
|
||||
}
|
||||
bool IsOpen() const { return m_Open; }
|
||||
void Draw(const EditorContext& ctx);
|
||||
|
||||
private:
|
||||
void DrawPreview(const EditorContext& ctx);
|
||||
void DrawStates(const EditorContext& ctx,
|
||||
Engine::Animation::AnimationController* ctrl);
|
||||
void DrawTransitions(const EditorContext& ctx,
|
||||
Engine::Animation::AnimationController* ctrl);
|
||||
void DrawParams(const EditorContext& ctx,
|
||||
Engine::Animation::AnimationController* ctrl);
|
||||
|
||||
bool m_Open = false;
|
||||
std::string m_Path;
|
||||
int m_SelState = 0; // 选中状态(StateOrder 下标)
|
||||
std::vector<char> m_CondBuf; // cond 输入缓冲(按行复用)
|
||||
};
|
||||
|
||||
#endif //HELLOVULKAN_CONTROLLEREDITORPANEL_H
|
||||
@@ -0,0 +1,214 @@
|
||||
//
|
||||
// HierarchyPanel.cpp:实体层级树实现(UE Outliner 语义)。
|
||||
//
|
||||
// 树形展示(根 = 无有效父链接的实体,子 = HierarchyComponent 挂载序)、
|
||||
// 点击选中、拖拽改父/拖到空白区挂根(keepWorld = 挂载前后世界位姿
|
||||
// 不变)、右键 Create Child / Detach / Delete(级联随葬,延迟销毁)。
|
||||
//
|
||||
|
||||
#include "HierarchyPanel.h"
|
||||
#include "Engine/ECS/Components.h"
|
||||
#include "Engine/ECS/EntityHandle.h" // CreateEntity 返回完整 EntityHandle
|
||||
|
||||
#include <imgui.h>
|
||||
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
using namespace Engine;
|
||||
|
||||
namespace {
|
||||
|
||||
// 拖拽 payload 类型名(Editor 内部约定;实体位模式 4 字节)
|
||||
constexpr const char* kHierPayload = "HIER_ENTITY";
|
||||
|
||||
// 实体显示名
|
||||
std::string DisplayName(const entt::registry& reg, entt::entity e)
|
||||
{
|
||||
if (const auto* n = reg.try_get<Engine::ECS::NameComponent>(e);
|
||||
n && !n->name.empty()) {
|
||||
return n->name;
|
||||
}
|
||||
return "(unnamed)";
|
||||
}
|
||||
|
||||
// 递归绘制单节点(含子树)。返回值无意义,选中写 selected
|
||||
void DrawEntityNode(const EditorContext& ctx, entt::entity e,
|
||||
entt::entity& selected)
|
||||
{
|
||||
auto& reg = ctx.scene->GetRegistry();
|
||||
|
||||
// 稳定行 ID(UUID):折叠态跨帧保持 + 与 Inspector PushID 隔离
|
||||
u64 rowId = static_cast<u64>(static_cast<u32>(e));
|
||||
if (const auto* id = reg.try_get<Engine::ECS::IdComponent>(e)) {
|
||||
rowId = id->uuid.Value();
|
||||
}
|
||||
ImGui::PushID(reinterpret_cast<const void*>(rowId));
|
||||
|
||||
// 子表(过滤悬空 + 无 Name 的防御性兜底)
|
||||
const auto* h = reg.try_get<Engine::ECS::HierarchyComponent>(e);
|
||||
std::vector<entt::entity> kids;
|
||||
if (h) {
|
||||
for (entt::entity c : h->children) {
|
||||
if (reg.valid(c) && reg.all_of<Engine::ECS::NameComponent>(c)) {
|
||||
kids.push_back(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const std::string label = DisplayName(reg, e);
|
||||
const ImGuiTreeNodeFlags flags =
|
||||
ImGuiTreeNodeFlags_OpenOnArrow |
|
||||
ImGuiTreeNodeFlags_SpanAvailWidth |
|
||||
(kids.empty() ? ImGuiTreeNodeFlags_Leaf : 0) |
|
||||
(selected == e ? ImGuiTreeNodeFlags_Selected : 0);
|
||||
|
||||
const bool opened = ImGui::TreeNodeEx(label.c_str(), flags);
|
||||
const bool toggled = ImGui::IsItemToggledOpen();
|
||||
if (ImGui::IsItemClicked(ImGuiMouseButton_Left) && !toggled) {
|
||||
selected = e;
|
||||
}
|
||||
|
||||
// ---- 拖拽源:拖动本实体去改父 ----
|
||||
if (ImGui::BeginDragDropSource()) {
|
||||
const u32 bits = static_cast<u32>(entt::to_integral(e));
|
||||
ImGui::SetDragDropPayload(kHierPayload, &bits, sizeof(bits));
|
||||
ImGui::TextUnformatted(label.c_str());
|
||||
ImGui::EndDragDropSource();
|
||||
}
|
||||
|
||||
// ---- 拖放目标:挂到本实体下(keepWorld,世界位姿不变)----
|
||||
if (ImGui::BeginDragDropTarget()) {
|
||||
if (const ImGuiPayload* pl =
|
||||
ImGui::AcceptDragDropPayload(kHierPayload)) {
|
||||
u32 bits = 0;
|
||||
std::memcpy(&bits, pl->Data, sizeof(bits));
|
||||
const entt::entity dragged = static_cast<entt::entity>(bits);
|
||||
// 拒挂自身/自身后代(SetParent 内部同样防环,这里提前
|
||||
// 静默拦截,避免"看起来挂上了"的错觉)
|
||||
if (dragged != e && !ctx.scene->IsDescendantOf(dragged, e)) {
|
||||
ctx.scene->SetParent(dragged, e);
|
||||
}
|
||||
}
|
||||
ImGui::EndDragDropTarget();
|
||||
}
|
||||
|
||||
// ---- 右键菜单 ----
|
||||
if (ImGui::BeginPopupContextItem("hier_ctx")) {
|
||||
if (ImGui::MenuItem("Create Child")) {
|
||||
// keepWorld=true:新实体出现在父的位置(UE attach 语义)
|
||||
const auto child = ctx.scene->CreateEntity("child entity");
|
||||
ctx.scene->SetParent(child, e);
|
||||
selected = child;
|
||||
}
|
||||
if (ctx.scene->GetParent(e) != entt::null &&
|
||||
ImGui::MenuItem("Detach to Root")) {
|
||||
ctx.scene->SetParent(e, entt::null);
|
||||
}
|
||||
if (ImGui::MenuItem("Delete")) {
|
||||
// 级联随葬:选中若是被删子树的一部分,一并清除
|
||||
const bool selInSubtree = ctx.scene->IsDescendantOf(selected, e);
|
||||
ctx.scene->DestroyEntity(e);
|
||||
if (selInSubtree) {
|
||||
selected = entt::null;
|
||||
}
|
||||
}
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
|
||||
// ---- 组件徽标(与行内同级显示)----
|
||||
std::string badge;
|
||||
if (reg.all_of<Engine::ECS::MeshRendererComponent>(e)) badge += "[M]";
|
||||
if (reg.all_of<Engine::ECS::AnimationComponent>(e)) badge += "[A]";
|
||||
if (reg.all_of<Engine::ECS::CameraComponent>(e)) badge += "[C]";
|
||||
if (reg.all_of<Engine::ECS::LightComponent>(e)) badge += "[L]";
|
||||
if (reg.all_of<Engine::ECS::RigidBodyComponent>(e)) badge += "[P]";
|
||||
if (!badge.empty()) {
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled("%s", badge.c_str());
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip(
|
||||
"M=Mesh A=Animation C=Camera L=Light P=Physics");
|
||||
}
|
||||
}
|
||||
|
||||
if (opened) {
|
||||
for (entt::entity c : kids) {
|
||||
DrawEntityNode(ctx, c, selected);
|
||||
}
|
||||
ImGui::TreePop();
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void HierarchyPanel::Draw(const EditorContext& ctx, entt::entity& selected)
|
||||
{
|
||||
ImGui::Begin("Outliner");
|
||||
|
||||
// 工具栏:创建实体(在遍历之前,不修改遍历中的注册表结构)。
|
||||
// Place Actors 简化语义:常用类型一键带组件,其余 Add Component 补
|
||||
if (ImGui::Button("+ Create"))
|
||||
{
|
||||
ImGui::OpenPopup("create_entity");
|
||||
}
|
||||
if (ImGui::BeginPopup("create_entity"))
|
||||
{
|
||||
if (ImGui::MenuItem("Empty"))
|
||||
{
|
||||
selected = ctx.scene->CreateEntity("Entity");
|
||||
}
|
||||
if (ImGui::MenuItem("Light"))
|
||||
{
|
||||
auto e = ctx.scene->CreateEntity("Light");
|
||||
e.AddComponent<Engine::ECS::LightComponent>();
|
||||
selected = e;
|
||||
}
|
||||
if (ImGui::MenuItem("Camera"))
|
||||
{
|
||||
auto e = ctx.scene->CreateEntity("Camera");
|
||||
e.AddComponent<Engine::ECS::CameraComponent>();
|
||||
selected = e;
|
||||
}
|
||||
if (ImGui::MenuItem("Environment"))
|
||||
{
|
||||
// UE 新关卡环境集简化:Skybox(背景)+ SkyLight(环境光)一体
|
||||
auto e = ctx.scene->CreateEntity("Environment");
|
||||
e.AddComponent<Engine::ECS::SkyboxComponent>();
|
||||
e.AddComponent<Engine::ECS::SkyLightComponent>();
|
||||
selected = e;
|
||||
}
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
ImGui::Separator();
|
||||
|
||||
auto& reg = ctx.scene->GetRegistry();
|
||||
|
||||
// 树形遍历:根 = 无有效父链接的实体(creation 序),子按挂载序递归
|
||||
reg.view<Engine::ECS::NameComponent>().each(
|
||||
[&](const entt::entity e, const Engine::ECS::NameComponent&)
|
||||
{
|
||||
if (ctx.scene->GetParent(e) != entt::null) return; // 非根
|
||||
DrawEntityNode(ctx, e, selected);
|
||||
});
|
||||
|
||||
// ---- 空白区 = 挂根落点(UE Outliner 拖到空白 = 顶层)----
|
||||
ImVec2 rem = ImGui::GetContentRegionAvail();
|
||||
if (rem.y > 4.0f) {
|
||||
ImGui::InvisibleButton("##hier_drop_root", rem);
|
||||
if (ImGui::BeginDragDropTarget()) {
|
||||
if (const ImGuiPayload* pl =
|
||||
ImGui::AcceptDragDropPayload(kHierPayload)) {
|
||||
u32 bits = 0;
|
||||
std::memcpy(&bits, pl->Data, sizeof(bits));
|
||||
ctx.scene->SetParent(static_cast<entt::entity>(bits),
|
||||
entt::null);
|
||||
}
|
||||
ImGui::EndDragDropTarget();
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::End();
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
//
|
||||
// HierarchyPanel:实体层级树面板。
|
||||
//
|
||||
// 树形展示父子层级(UE Outliner)、点击选中、拖拽改父/拖到空白挂根
|
||||
//(keepWorld)、右键 Create Child / Detach / Delete(级联随葬,帧尾
|
||||
// flush,遍历安全)。每项尾缀显示组件构成徽标。
|
||||
//
|
||||
|
||||
#ifndef HELLOVULKAN_HIERARCHYPANEL_H
|
||||
#define HELLOVULKAN_HIERARCHYPANEL_H
|
||||
|
||||
#include "EditorContext.h"
|
||||
|
||||
#include <entt/entt.hpp>
|
||||
|
||||
class HierarchyPanel {
|
||||
public:
|
||||
void Draw(const EditorContext& ctx, entt::entity& selected);
|
||||
};
|
||||
|
||||
#endif //HELLOVULKAN_HIERARCHYPANEL_H
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,22 @@
|
||||
//
|
||||
// InspectorPanel:选中实体的组件检查/编辑面板。
|
||||
//
|
||||
// 泛化编辑:任何实体(不是沙盒特例)的 Name / Transform /
|
||||
// MeshRenderer / Light / Camera / Animation 全部在此编辑。
|
||||
// 沙盒原 Scene Debug 里的 KeyLight、动画 clip、gun scale 控件
|
||||
// 全部由这里的通用控件替代。
|
||||
//
|
||||
|
||||
#ifndef HELLOVULKAN_INSPECTORPANEL_H
|
||||
#define HELLOVULKAN_INSPECTORPANEL_H
|
||||
|
||||
#include "EditorContext.h"
|
||||
|
||||
#include <entt/entt.hpp>
|
||||
|
||||
class InspectorPanel {
|
||||
public:
|
||||
void Draw(const EditorContext& ctx, entt::entity selected);
|
||||
};
|
||||
|
||||
#endif //HELLOVULKAN_INSPECTORPANEL_H
|
||||
@@ -0,0 +1,321 @@
|
||||
//
|
||||
// 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();
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
//
|
||||
// MaterialEditorPanel:独立材质编辑器窗口(UE Material Editor 语义)。
|
||||
//
|
||||
// 与 Details 面板职责分离:Details 材质槽行只做"指派"(换绑/还原/拖拽
|
||||
// 覆盖资产),双击槽位预览球或 Content Browser 的 .material 条目 →
|
||||
// 本面板打开做"编辑"(参数全平铺 + 纹理槽)。
|
||||
//
|
||||
// 两种模式:
|
||||
// - 资产模式(可编辑):编辑 .material 资产。参数走
|
||||
// EditParams 直写 UBO(松手落盘),纹理槽变化走 ReloadMaterialAsset
|
||||
// (waitIdle 全停重建,低频路径)——与原 Inspector 内嵌编辑完全
|
||||
// 同一套语义,此处原样搬运
|
||||
// - 内建模式(只读):Model 内建材质无 CPU 参数副本(直写 GPU UBO),
|
||||
// 只读展示 MeshMaterialInfo 快照 + 站内一键 Create Override(生成
|
||||
// 资产后自动切换到资产模式编辑)
|
||||
//
|
||||
// 打开目标每帧重解析(实体可销毁 / 模型可卸载 / 资产可 Reload 换实例),
|
||||
// 面板不跨帧持有任何指针。
|
||||
//
|
||||
|
||||
#ifndef HELLOVULKAN_MATERIALEDITORPANEL_H
|
||||
#define HELLOVULKAN_MATERIALEDITORPANEL_H
|
||||
|
||||
#include "EditorContext.h"
|
||||
|
||||
#include <Engine/Core/Types.h>
|
||||
|
||||
#include <entt/entt.hpp>
|
||||
#include <string>
|
||||
|
||||
// 材质资产拖拽 payload(Content Browser 拖 .material → Details 槽位)
|
||||
inline constexpr const char* kMaterialDragPayload = "MAT_PATH";
|
||||
|
||||
class MaterialEditorPanel {
|
||||
public:
|
||||
// 打开 .material 资产(可编辑模式)
|
||||
void OpenAsset(const std::string& path);
|
||||
// 打开内建材质(只读模式):owner 实体 + mesh 槽位每帧重解析
|
||||
void OpenBuiltin(entt::entity owner, Engine::u32 meshIdx,
|
||||
const std::string& displayName);
|
||||
|
||||
void Draw(const EditorContext& ctx);
|
||||
|
||||
private:
|
||||
void DrawPreview(const EditorContext& ctx, Engine::Material* mat);
|
||||
void DrawAssetMode(const EditorContext& ctx);
|
||||
void DrawBuiltinMode(const EditorContext& ctx);
|
||||
|
||||
bool m_Open = false;
|
||||
bool m_AssetMode = false;
|
||||
// 资产模式目标
|
||||
std::string m_Path;
|
||||
bool m_LoadTried = false; // 未加载时自动 Load 一次(防每帧重试)
|
||||
// 内建模式目标
|
||||
entt::entity m_Owner{entt::null};
|
||||
Engine::u32 m_MeshIdx = 0;
|
||||
std::string m_DisplayName;
|
||||
};
|
||||
|
||||
#endif //HELLOVULKAN_MATERIALEDITORPANEL_H
|
||||
@@ -0,0 +1,214 @@
|
||||
//
|
||||
// StatsPanel.cpp:遥测与全局设置实现(原 main.cpp Scene Debug 搬迁)。
|
||||
// 面板按 View / Scene / Rendering / Physics / Performance 分组——
|
||||
// 环境贴图/环境光已实体化(Skybox/SkyLight 组件,Details 配置),
|
||||
// 此处只留场景级 ClearColor。
|
||||
//
|
||||
|
||||
#include "StatsPanel.h"
|
||||
#include "Engine/ECS/Components.h"
|
||||
#include "Engine/Physics/PhysicsWorld.h"
|
||||
#include "Engine/Core/Time.h"
|
||||
|
||||
#include <imgui.h>
|
||||
#include <glm/gtc/type_ptr.hpp>
|
||||
|
||||
using namespace Engine; // f32(Engine/Core/Types.h)
|
||||
|
||||
void StatsPanel::Draw(const EditorContext& ctx)
|
||||
{
|
||||
ImGui::Begin("Stats");
|
||||
|
||||
auto& renderer = *ctx.renderer;
|
||||
auto& app = *ctx.app;
|
||||
|
||||
// ---- View:看哪里(渲染目标)+ 相机位姿(跟随渲染源分派)----
|
||||
if (ImGui::CollapsingHeader("View", ImGuiTreeNodeFlags_DefaultOpen))
|
||||
{
|
||||
// 渲染源分派与 SceneRenderer::BuildFrame 同规则:PIE 内嵌播放
|
||||
// 或 kDisplayDirect 全屏预览 = primary 相机,否则编辑器相机
|
||||
const bool playing = ctx.playState && *ctx.playState;
|
||||
static const char* displayTarget[] = {
|
||||
"Fullscreen",
|
||||
"Viewport",
|
||||
};
|
||||
int mode = renderer.GetDisplayMode();
|
||||
if (ImGui::Combo("Display Target", &mode, displayTarget,
|
||||
IM_ARRAYSIZE(displayTarget)))
|
||||
{
|
||||
renderer.SetDisplayMode(mode);
|
||||
}
|
||||
if ((playing || renderer.GetDisplayMode() == kDisplayDirect))
|
||||
{
|
||||
Engine::ECS::TransformComponent* camT = nullptr;
|
||||
if (ctx.scene->GetPrimaryCamera(&camT) != nullptr && camT != nullptr)
|
||||
{
|
||||
// world 位姿(相机实体可能挂父层级下)
|
||||
const glm::vec3& pos = camT->GetWorldPosition();
|
||||
ImGui::Text("Game Camera Pos");
|
||||
ImGui::Text("(%.3f, %.3f, %.3f)", pos.x, pos.y, pos.z);
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui::TextDisabled("(no primary camera)");
|
||||
}
|
||||
}
|
||||
else if (ctx.editorCamera)
|
||||
{
|
||||
const glm::vec3& pos = ctx.editorCamera->GetTransform().GetPosition();
|
||||
ImGui::Text("Editor Camera Pos");
|
||||
ImGui::Text("(%.3f, %.3f, %.3f)", pos.x, pos.y, pos.z);
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui::TextDisabled("(no camera)");
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Scene:模拟控制 + 场景级设置。PIE 暂停在主工具栏(UE 语义)----
|
||||
if (ImGui::CollapsingHeader("Scene", ImGuiTreeNodeFlags_DefaultOpen))
|
||||
{
|
||||
ImGui::ColorEdit4("Clear Color",
|
||||
glm::value_ptr(ctx.scene->GetSettings().ClearColor));
|
||||
}
|
||||
|
||||
// ---- Rendering:渲染管线参数与开关 ----
|
||||
if (ImGui::CollapsingHeader("Rendering", ImGuiTreeNodeFlags_DefaultOpen))
|
||||
{
|
||||
f32 exposure = renderer.GetExposure();
|
||||
if (ImGui::SliderFloat("Exposure", &exposure, 0.1f, 4.0f))
|
||||
{
|
||||
renderer.SetExposure(exposure);
|
||||
}
|
||||
bool aces = renderer.GetUseACES();
|
||||
if (ImGui::Checkbox("ACES", &aces))
|
||||
{
|
||||
renderer.SetUseACES(aces);
|
||||
}
|
||||
|
||||
// Bloom:开关即显隐——勾上才显示参数
|
||||
bool bloom = renderer.GetBloomEnabled();
|
||||
if (ImGui::Checkbox("Bloom", &bloom))
|
||||
{
|
||||
renderer.SetBloomEnabled(bloom);
|
||||
}
|
||||
if (bloom)
|
||||
{
|
||||
f32 threshold = renderer.GetBloomThreshold();
|
||||
if (ImGui::SliderFloat("Threshold", &threshold, 0.0f, 8.0f,
|
||||
"%.1f", ImGuiSliderFlags_Logarithmic))
|
||||
{
|
||||
renderer.SetBloomThreshold(threshold);
|
||||
}
|
||||
f32 strength = renderer.GetBloomStrength();
|
||||
if (ImGui::SliderFloat("Strength", &strength, 0.0f, 2.0f))
|
||||
{
|
||||
renderer.SetBloomStrength(strength);
|
||||
}
|
||||
}
|
||||
|
||||
// 无参数的纯开关:横排平铺
|
||||
bool shadows = renderer.GetShadowsEnabled();
|
||||
if (ImGui::Checkbox("Shadows", &shadows))
|
||||
{
|
||||
renderer.SetShadowsEnabled(shadows);
|
||||
}
|
||||
ImGui::SameLine();
|
||||
bool taa = renderer.GetTAAEnabled();
|
||||
if (ImGui::Checkbox("TAA", &taa))
|
||||
{
|
||||
renderer.SetTAAEnabled(taa);
|
||||
}
|
||||
ImGui::SameLine();
|
||||
bool lod = renderer.GetLodEnabled();
|
||||
if (ImGui::Checkbox("LOD", &lod))
|
||||
{
|
||||
renderer.SetLodEnabled(lod); // 关 = 恒绘原网格
|
||||
}
|
||||
|
||||
// 调试视图:单看某个 pass 的中间输出(类 UE pass debug)
|
||||
const auto& viewNames = renderer.GetDebugViewNames();
|
||||
int view = renderer.GetDebugView();
|
||||
if (ImGui::Combo("Debug View", &view, viewNames.data(),
|
||||
static_cast<int>(viewNames.size())))
|
||||
{
|
||||
renderer.SetDebugView(view);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Physics:调试可视化(collider/约束/重力/速度/加速度)----
|
||||
if (ImGui::CollapsingHeader("Physics"))
|
||||
{
|
||||
auto& physics = app.GetPhysics();
|
||||
u32 bits = physics.GetDebugDrawBits();
|
||||
bool master = bits != 0;
|
||||
if (ImGui::Checkbox("Debug Draw##Phys", &master))
|
||||
{
|
||||
bits = master ? kPhyDbgDefault : 0;
|
||||
}
|
||||
if (master)
|
||||
{
|
||||
ImGui::Indent();
|
||||
auto toggle = [&bits](const char* label, u32 bit) {
|
||||
bool on = (bits & bit) != 0;
|
||||
if (ImGui::Checkbox(label, &on))
|
||||
{
|
||||
bits = on ? (bits | bit) : (bits & ~bit);
|
||||
}
|
||||
};
|
||||
toggle("Colliders", kPhyDbgColliders);
|
||||
toggle("Constraints", kPhyDbgConstraints);
|
||||
toggle("Gravity", kPhyDbgGravity);
|
||||
toggle("Velocity", kPhyDbgVelocity);
|
||||
toggle("Acceleration", kPhyDbgAccel);
|
||||
ImGui::Unindent();
|
||||
}
|
||||
physics.SetDebugDrawBits(bits);
|
||||
// 碰撞事件计数(最近一次步进;掉落物触地时应 > 0)
|
||||
ImGui::Text("contacts (frame): %zu", physics.GetEvents().size());
|
||||
}
|
||||
|
||||
// ---- Performance:帧率控制 + GPU 遥测(默认折叠,不占面板高度)----
|
||||
if (ImGui::CollapsingHeader("Performance"))
|
||||
{
|
||||
bool vsync = app.GetVSync();
|
||||
if (ImGui::Checkbox("VSync", &vsync))
|
||||
{
|
||||
app.SetVSync(vsync);
|
||||
if (!vsync)
|
||||
{
|
||||
const f32 target = app.GetTargetFps();
|
||||
app.SetTargetFps(target);
|
||||
}
|
||||
}
|
||||
if (!vsync)
|
||||
{
|
||||
f32 target = app.GetTargetFps();
|
||||
if (ImGui::SliderFloat("Target FPS", &target, 24.0f, 360.0f, "%.0f"))
|
||||
{
|
||||
app.SetTargetFps(target);
|
||||
}
|
||||
}
|
||||
ImGui::Text("fps: %.1f frame: %llu",
|
||||
1.0f / Time::Current().UnscaledDt(),
|
||||
static_cast<unsigned long long>(app.GetFrameCount()));
|
||||
|
||||
ImGui::Separator();
|
||||
ImGui::Text("instances: %u main / %u shadow",
|
||||
renderer.GetMainInstanceCount(),
|
||||
renderer.GetShadowInstanceCount());
|
||||
if (renderer.GetInstancesTruncated())
|
||||
{
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f),
|
||||
"TRUNCATED: instance buffer full!");
|
||||
}
|
||||
const auto& names = renderer.GetPassNames();
|
||||
const auto& times = renderer.GetPassGpuMs();
|
||||
for (size_t i = 0; i < names.size() && i < times.size(); ++i)
|
||||
{
|
||||
ImGui::Text(" %-12s %.3f ms", names[i].c_str(), times[i]);
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::End();
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
//
|
||||
// StatsPanel:遥测与全局设置面板(原 Scene Debug 拆入)。
|
||||
//
|
||||
// Camera 位姿 / Rendering 开关 / Environment(IBL+DebugView+ClearColor) /
|
||||
// GPU Timings / Frame(VSync、fps)。KeyLight、动画 clip、gun scale
|
||||
// 等沙盒特例编辑已泛化进 Inspector,此处不再重复。
|
||||
//
|
||||
|
||||
#ifndef HELLOVULKAN_STATSPANEL_H
|
||||
#define HELLOVULKAN_STATSPANEL_H
|
||||
|
||||
#include "EditorContext.h"
|
||||
|
||||
class StatsPanel {
|
||||
public:
|
||||
void Draw(const EditorContext& ctx);
|
||||
};
|
||||
|
||||
#endif //HELLOVULKAN_STATSPANEL_H
|
||||
@@ -0,0 +1,473 @@
|
||||
//
|
||||
// ViewportPanel:场景渲染输出面板。
|
||||
//
|
||||
// tonemap 输出经 ImGui::Image 显示(Editor 模式),并提供
|
||||
// Editor Viewport / Fullscreen 显示模式切换。面板可用区域
|
||||
// (物理像素)上报为渲染分辨率 → 画面精确填满面板。
|
||||
//
|
||||
// 点击拾取(UE 式 GPU HitProxy):左键单击 → 鼠标 NDC 经
|
||||
// Renderer::RequestPick 请求下一帧 id 渲染 + 像素拷贝;Draw 每帧
|
||||
// 轮询 TakePickResult 取走结果。像素级精度,与视觉画面一致
|
||||
//(视觉几何语义:与 Collider 无关,无 collider 实体同样可选中)。
|
||||
//
|
||||
|
||||
#include "ViewportPanel.h"
|
||||
|
||||
#include <imgui.h>
|
||||
#include <ImGuizmo.h>
|
||||
|
||||
#include <Engine/ECS/Components.h> // MeshRenderer / Name / RigidBody(日志对照)
|
||||
#include <Engine/Renderer/Renderer.h> // RequestPick / TakePickResult 门面
|
||||
#include <Engine/Renderer/SceneTypes.h> // kPickMiss
|
||||
#include <Engine/Core/Log.h> // 命中打印
|
||||
|
||||
#include <glm/glm.hpp>
|
||||
#include <glm/gtc/matrix_transform.hpp>
|
||||
|
||||
#include <cstdio>
|
||||
|
||||
// GTX 扩展(实验性)需显式开启:matrix_decompose(矩阵→SRT 分解,
|
||||
// 旋转以四元数输出,直接写回 TransformComponent)
|
||||
#define GLM_ENABLE_EXPERIMENTAL
|
||||
#include <glm/gtx/matrix_decompose.hpp>
|
||||
|
||||
using namespace Engine; // u32 / f32(Engine/Core/Types.h)
|
||||
|
||||
namespace
|
||||
{
|
||||
// Ctrl 吸附步长(UE Grid Snap 语义:按住 Ctrl 拖拽才吸附)
|
||||
constexpr float kSnapTranslate = 0.25f; // 米/格
|
||||
constexpr float kSnapRotateDeg = 15.0f; // 度
|
||||
constexpr float kSnapScale = 0.1f; // 缩放步进
|
||||
|
||||
// 平移拖拽灵敏度下限:sinθ(轴与"相机→物体"连线夹角)。低于该值
|
||||
// 时拖拽投影系数 <0.25 且激活平面与鼠标射线近共面——视觉上
|
||||
// "看得见、抓了没反应"。≈14.5°,略宽于库默认可见角(≈11.5°),
|
||||
// 只藏真正病态的轴,不动可用轴
|
||||
constexpr float kMoveMinSin = 0.25f;
|
||||
|
||||
// 缩放退化轴裁剪(防误拖动)阈值。库内缩放 hover/拖拽依赖
|
||||
// "法线=轴"平面求交,两种病态几何:
|
||||
// - 轴⊥"相机→物体"连线(正对视角无横偏):交点分子 numer =
|
||||
// -dist·dot(camToObj, 轴) ≡ 0 → 杆抓不到、数值随机猛跳
|
||||
// - 轴∥视线(轴指向相机):屏幕零位移 → 拖了数值不变
|
||||
constexpr float kScaleMinDot = 0.1f; // ≈84.3°
|
||||
constexpr float kScaleMaxFwdDot = 0.98f; // ≈11.5°,与库藏杆角一致
|
||||
|
||||
// Vulkan proj → ImGuizmo 期望的 OpenGL 形式(Y-up、Z∈[-1,1]):
|
||||
// Y 行翻回(抵消 Vulkan clip Y 向下翻转);z 行 = 2*z - w 即
|
||||
// 深度 [0,1] → [-1,1]。glm 列主序 m[col][row],逐列改 row1/row2
|
||||
glm::mat4 ProjVulkanToGL(const glm::mat4& projVulkan)
|
||||
{
|
||||
glm::mat4 p = projVulkan;
|
||||
for (int c = 0; c < 4; ++c) {
|
||||
p[c][1] *= -1.0f;
|
||||
p[c][2] = 2.0f * p[c][2] - p[c][3];
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
// 视图相机分派(必须与 SceneRenderer::BuildFrame 渲染源同一规则,
|
||||
// 否则 Gizmo 杆与画面错位):编辑器形态 = EditorCamera(Scene view
|
||||
// 专用,与相机实体无关);游戏形态(kDisplayDirect 全屏预览或 PIE
|
||||
// 内嵌播放)= primary 相机实体。
|
||||
// 返回 nullptr = 游戏形态且无 primary 相机(本帧无 Gizmo)
|
||||
Engine::Camera* ViewCamera(const EditorContext& ctx,
|
||||
ECS::TransformComponent*& outT)
|
||||
{
|
||||
const bool playing = ctx.playState && *ctx.playState;
|
||||
if (!playing && ctx.renderer->GetDisplayMode() == kDisplayViewport)
|
||||
{
|
||||
outT = &ctx.editorCamera->GetTransform();
|
||||
return &ctx.editorCamera->GetController();
|
||||
}
|
||||
auto* cc = ctx.scene->GetPrimaryCamera(&outT);
|
||||
return cc ? &cc->camera : nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<entt::entity> ViewportPanel::Draw(const EditorContext &ctx,
|
||||
ViewportInputState &out,
|
||||
GizmoState &gizmo,
|
||||
const entt::entity selected) const {
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f));
|
||||
std::optional<entt::entity> picked;
|
||||
{
|
||||
ImGui::Begin("Viewport", nullptr, ImGuiWindowFlags_NoCollapse);
|
||||
|
||||
// 输入焦点上报(悬停计入遮挡:模态框盖住时不误报)
|
||||
out.hovered = ImGui::IsWindowHovered();
|
||||
out.focused = ImGui::IsWindowFocused();
|
||||
|
||||
auto &renderer = *ctx.renderer;
|
||||
const int mode = renderer.GetDisplayMode();
|
||||
|
||||
// ---- 拾取结果轮询(先于新请求:先取走上帧结果再发新请求)。
|
||||
// 拷贝随上一帧命令缓冲提交,TakePickResult 内 waitIdle 保证
|
||||
// 暂存可见(仅拷贝就绪帧阻塞,点击低频不可感知)----
|
||||
if (auto bits = renderer.TakePickResult()) {
|
||||
auto ® = ctx.scene->GetRegistry();
|
||||
const entt::entity hit = static_cast<entt::entity>(*bits);
|
||||
if (*bits != kPickMiss && reg.valid(hit)) {
|
||||
picked = hit;
|
||||
LogPick(ctx, hit);
|
||||
} else {
|
||||
// 空白(kPickMiss)或悬空位模式:清选中(与旧语义一致)
|
||||
picked = entt::null;
|
||||
if (*bits == kPickMiss) {
|
||||
ENGINE_INFO("[pick] miss (sky/empty)");
|
||||
} else {
|
||||
ENGINE_WARN("[pick] dangling entity bits {:#x}", *bits);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const void *sceneTex = renderer.GetSceneTexture();
|
||||
if (sceneTex != nullptr && mode == kDisplayViewport) {
|
||||
// 面板可用区域(物理像素)上报为渲染分辨率 → 画面精确填满
|
||||
// 面板;下一帧帧首 BeginFrameSync 生效(一帧延迟,不可见)
|
||||
const ImVec2 avail = ImGui::GetContentRegionAvail();
|
||||
const ImVec2 fb = ImGui::GetIO().DisplayFramebufferScale;
|
||||
const u32 wantW = static_cast<u32>(avail.x * fb.x + 0.5f);
|
||||
const u32 wantH = static_cast<u32>(avail.y * fb.y + 0.5f);
|
||||
if (wantW >= 16 && wantH >= 16) {
|
||||
renderer.SetViewportSize(wantW, wantH);
|
||||
}
|
||||
|
||||
// 纹理 = 渲染分辨率:等比适配面板可用区域
|
||||
const vk::Extent2D ext = renderer.GetRenderExtent();
|
||||
const f32 aspect =
|
||||
ext.height != 0 ? f32(ext.width) / f32(ext.height) : 1.0f;
|
||||
ImVec2 size = avail;
|
||||
if (avail.x / avail.y > aspect) size.x = avail.y * aspect;
|
||||
else size.y = avail.x / aspect;
|
||||
ImGui::Image((ImTextureID) (intptr_t) sceneTex, size);
|
||||
|
||||
// ---- 骨骼关节 overlay(Endfield-Poser 范式):3D 骨架线
|
||||
// 给结构,屏幕圆点给可读性。世界→屏幕与渲染同相机,
|
||||
// Vulkan NDC y 向下与 ImGui 屏幕坐标同构 → 线性映射
|
||||
// image 矩形即可。hover 关节 = 白圈高亮 + 骨骼名
|
||||
{
|
||||
const auto &frameData = renderer.GetFrameSceneData();
|
||||
if (frameData.showBoneWires && !frameData.pieActive &&
|
||||
!frameData.boneJoints.empty() &&
|
||||
ctx.editorCamera != nullptr) {
|
||||
const ImVec2 rmin = ImGui::GetItemRectMin();
|
||||
const ImVec2 rmax = ImGui::GetItemRectMax();
|
||||
const glm::mat4 vp =
|
||||
ctx.editorCamera->GetController().GetProjection(aspect) *
|
||||
ctx.editorCamera->GetView();
|
||||
ImDrawList *dl = ImGui::GetWindowDrawList();
|
||||
dl->PushClipRect(rmin, rmax, true);
|
||||
const f32 r = frameData.boneJointPx;
|
||||
const ImGuiIO &io = ImGui::GetIO();
|
||||
const ImVec2 mouse = io.MousePos;
|
||||
int hovered = -1;
|
||||
float hoveredD = 0.0f;
|
||||
for (size_t ji = 0; ji < frameData.boneJoints.size(); ++ji) {
|
||||
const auto &jm = frameData.boneJoints[ji];
|
||||
const glm::vec4 c = vp * glm::vec4(jm.pos, 1.0f);
|
||||
if (c.w <= 0.0f) continue; // 相机身后
|
||||
const ImVec2 p(
|
||||
rmin.x + (c.x / c.w * 0.5f + 0.5f) * size.x,
|
||||
rmin.y + (c.y / c.w * 0.5f + 0.5f) * size.y);
|
||||
// 骨骼 = 蓝(Endfield humanoid 色),普通节点 = 青
|
||||
const ImU32 fill = jm.isBone
|
||||
? IM_COL32(70, 160, 255, 230)
|
||||
: IM_COL32(70, 190, 190, 190);
|
||||
dl->AddCircleFilled(p, r, fill);
|
||||
dl->AddCircle(p, r + 1.5f,
|
||||
IM_COL32(255, 255, 255, 140), 0, 1.2f);
|
||||
// hover 命中(半径 +3px 容差,取最近)
|
||||
const float dx = p.x - mouse.x, dy = p.y - mouse.y;
|
||||
const float d = dx * dx + dy * dy;
|
||||
const float hit = (r + 3.0f) * (r + 3.0f);
|
||||
if (d <= hit && (hovered < 0 || d < hoveredD)) {
|
||||
hovered = static_cast<int>(ji);
|
||||
hoveredD = d;
|
||||
}
|
||||
}
|
||||
// hover 高亮圈 + 骨骼名 tooltip(名字查选中实体模型
|
||||
// 节点表;查不到 = 显示节点下标)
|
||||
if (hovered >= 0 && ImGui::IsItemHovered()) {
|
||||
const auto &jm = frameData.boneJoints[hovered];
|
||||
const glm::vec4 c = vp * glm::vec4(jm.pos, 1.0f);
|
||||
const ImVec2 p(
|
||||
rmin.x + (c.x / c.w * 0.5f + 0.5f) * size.x,
|
||||
rmin.y + (c.y / c.w * 0.5f + 0.5f) * size.y);
|
||||
dl->AddCircle(p, r + 3.5f,
|
||||
IM_COL32(255, 255, 255, 255), 0, 2.0f);
|
||||
const char *name = nullptr;
|
||||
if (selected != entt::null) {
|
||||
// 本段在 reg 声明之前,就地取 registry
|
||||
const auto *mrSel =
|
||||
ctx.scene->GetRegistry().try_get<
|
||||
Engine::ECS::MeshRendererComponent>(
|
||||
selected);
|
||||
if (mrSel && mrSel->model.Valid()) {
|
||||
const Model *mdl =
|
||||
ctx.assets->GetModel(mrSel->model);
|
||||
if (mdl && jm.nodeIdx < mdl->GetNodes().size())
|
||||
name = mdl->GetNodes()[jm.nodeIdx]
|
||||
.name.c_str();
|
||||
}
|
||||
}
|
||||
ImGui::SetTooltip(
|
||||
"%s%s", name ? name : "(node)",
|
||||
jm.isBone ? "" : " [non-bone]");
|
||||
}
|
||||
dl->PopClipRect();
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 工具热键(UE 语义):W/E/R 切 Gizmo 模式、T 切
|
||||
// local/world、F 焦点选中实体。仅 Viewport 持焦时生效——
|
||||
// 输入框打字(WantTextInput)由 BlockingKeyboard 拦游戏侧,
|
||||
// 但 ImGui 自身仍收键,故必须限定持焦面板。RMB 按住 = 相机
|
||||
// 飞行模式(键盘归相机),工具热键全部抑制——W 前进与
|
||||
// W 平移 Gizmo 的键位冲突就此解耦 ----
|
||||
auto ® = ctx.scene->GetRegistry();
|
||||
ECS::TransformComponent *tr =
|
||||
selected != entt::null
|
||||
? reg.try_get<ECS::TransformComponent>(selected)
|
||||
: nullptr;
|
||||
// PIE 运行态:输入归游戏(脚本读 GLFW 轮询),Gizmo/编辑器
|
||||
// 热键全部让位(UE Play 语义:Scene view 编辑操作冻结)
|
||||
const bool playing = ctx.playState && *ctx.playState;
|
||||
if (!playing && out.focused && !ImGui::GetIO().WantTextInput &&
|
||||
!ImGui::IsMouseDown(ImGuiMouseButton_Right)) {
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_W)) gizmo.op = ImGuizmo::TRANSLATE;
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_E)) gizmo.op = ImGuizmo::ROTATE;
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_R)) gizmo.op = ImGuizmo::SCALE;
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_T)) gizmo.localSpace = !gizmo.localSpace;
|
||||
const bool fCombo = ImGui::GetIO().KeyCtrl || ImGui::GetIO().KeyShift;
|
||||
// F = 焦点选中实体(frame-selection):编辑器相机
|
||||
// 环绕 pivot 设为选中实体位置,沿视线平移对准(环
|
||||
// 绕距离沿用当前值,滚轮可调)。世界位姿(层级
|
||||
// 实体 = 父链组合结果)。裸 F 才触发(组合键排他)
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_F) && !fCombo && tr != nullptr) {
|
||||
ctx.editorCamera->FocusOn(tr->GetWorldPosition());
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Gizmo 操纵杆(ImGui drawlist 叠加,无 GPU pass)----
|
||||
// 选中实体 + 有 Transform 才出杆;分解 world 矩阵经
|
||||
// SetWorldPose 写回(与 Inspector 同源,下一帧数值自动跟随)
|
||||
// PIE 运行态不出杆(编辑操作冻结)
|
||||
if (tr != nullptr && !playing) {
|
||||
// 相机矩阵:跟随渲染源(编辑器形态 = EditorCamera,游戏
|
||||
// 形态 = primary 相机实体;与 SceneRenderer::BuildFrame
|
||||
// 同一分派),投影参数由相机自身承载(与渲染侧一致)
|
||||
ECS::TransformComponent *camT = nullptr;
|
||||
if (Engine::Camera *cam = ViewCamera(ctx, camT);
|
||||
cam != nullptr && camT != nullptr) {
|
||||
const glm::mat4 view = cam->GetView(*camT);
|
||||
const glm::mat4 projGL = ProjVulkanToGL(
|
||||
cam->GetProjection(aspect));
|
||||
|
||||
ImGuizmo::SetDrawlist(ImGui::GetWindowDrawList());
|
||||
// 杆画在 Image 区域内(等比适配后的 item rect)
|
||||
const ImVec2 mn = ImGui::GetItemRectMin();
|
||||
ImGuizmo::SetRect(mn.x, mn.y, size.x, size.y);
|
||||
|
||||
// 不调 SetPlaneLimit/SetAxisLimit——可见度保持库默认
|
||||
|
||||
glm::mat4 m = tr->World();
|
||||
// 操作空间:local(模型轴,旋转/缩放随物体朝向直观)/
|
||||
// world(T 键切换)。无父层级时两种模式 world 矩阵同源,
|
||||
// decompose 写回链路不变
|
||||
const ImGuizmo::MODE mode =
|
||||
gizmo.localSpace ? ImGuizmo::LOCAL : ImGuizmo::WORLD;
|
||||
const glm::mat3 rotM(m);
|
||||
|
||||
// 退化轴裁剪(防误拖动):被裁轴不可见、不可激活,
|
||||
// 转个视角即可用。三操作病态几何不同,按 op 分派:
|
||||
// - 平移:拖拽灵敏度 = sinθ(轴与相机连线夹角),θ→0
|
||||
// 时鼠标位移在轴上投影→0("数值不变")
|
||||
// - 缩放:库内 hover/拖拽依赖"法线=轴"平面求交——
|
||||
// 轴⊥相机连线(正对无横偏)时交点分子 numer≡0
|
||||
// (抓不到/数值乱跳);轴∥视线时屏幕零位移(数值
|
||||
// 不变)。两判据并集裁剪
|
||||
// - 旋转:屏幕空间角度数学,无退化,不裁
|
||||
const bool isTranslate = (gizmo.op
|
||||
& int(ImGuizmo::TRANSLATE)) == int(ImGuizmo::TRANSLATE);
|
||||
const bool isScale = (gizmo.op
|
||||
& int(ImGuizmo::SCALE)) == int(ImGuizmo::SCALE);
|
||||
if (isTranslate || isScale) {
|
||||
bool maskAxis[3] = {false, false, false};
|
||||
const glm::vec3 camPos(glm::inverse(view)[3]);
|
||||
glm::vec3 camToObj = glm::vec3(m[3]) - camPos;
|
||||
const float camDist = glm::length(camToObj);
|
||||
if (camDist > 1e-4f) {
|
||||
camToObj /= camDist;
|
||||
const glm::vec3 viewFwd = -glm::normalize(
|
||||
glm::vec3(view[0][2], view[1][2],
|
||||
view[2][2]));
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
const glm::vec3 axis = gizmo.localSpace
|
||||
? glm::normalize(rotM[i])
|
||||
: glm::vec3(float(i == 0), float(i == 1),
|
||||
float(i == 2));
|
||||
maskAxis[i] = isScale
|
||||
? glm::abs(glm::dot(camToObj, axis))
|
||||
< kScaleMinDot ||
|
||||
glm::abs(glm::dot(viewFwd, axis))
|
||||
> kScaleMaxFwdDot
|
||||
: glm::length(glm::cross(camToObj, axis))
|
||||
< kMoveMinSin;
|
||||
}
|
||||
}
|
||||
ImGuizmo::SetAxisMask(maskAxis[0], maskAxis[1],
|
||||
maskAxis[2]);
|
||||
}
|
||||
|
||||
// Ctrl 吸附(UE Grid Snap 语义)。库的约定
|
||||
//(docs/documentation.md):平移 = 世界单位 float[3]
|
||||
// 逐轴;旋转 = snap[0] 按度(库内自行 DEG2RAD,传弧
|
||||
// 度会被解释成极小步进);缩放 = 只取 snap[0]
|
||||
float snap[3] = {0.0f, 0.0f, 0.0f};
|
||||
const bool snapping = ImGui::GetIO().KeyCtrl;
|
||||
if (snapping) {
|
||||
if (gizmo.op == ImGuizmo::TRANSLATE) {
|
||||
snap[0] = snap[1] = snap[2] = kSnapTranslate;
|
||||
} else if (gizmo.op == ImGuizmo::ROTATE) {
|
||||
snap[0] = kSnapRotateDeg; // 度,非弧度
|
||||
} else if (gizmo.op == ImGuizmo::SCALE) {
|
||||
snap[0] = kSnapScale;
|
||||
}
|
||||
}
|
||||
|
||||
ImGuizmo::Manipulate(&view[0][0], &projGL[0][0],
|
||||
static_cast<ImGuizmo::OPERATION>(gizmo.op),
|
||||
mode, &m[0][0], nullptr,
|
||||
snapping ? snap : nullptr);
|
||||
gizmo.dragging = ImGuizmo::IsUsing();
|
||||
|
||||
// 拖拽生效:分解 world 矩阵 → world SRT → 沿父链反解
|
||||
// local 写回(SetWorldPose;根实体 = 直写)。旋转用
|
||||
// decompose 出的 quat 直写(UE FTransform 语义)——
|
||||
// 不经欧拉往返,拖拽零漂移、无万向锁
|
||||
if (ImGuizmo::IsUsing()) {
|
||||
glm::vec3 scale, transl, skew;
|
||||
glm::quat rot;
|
||||
glm::vec4 persp;
|
||||
if (glm::decompose(m, scale, rot, transl, skew, persp)) {
|
||||
ctx.scene->SetWorldPose(selected, transl, rot,
|
||||
scale);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Gizmo 状态提示(左上角 drawlist 叠加,原因同下:不占布局行)
|
||||
// PIE 运行态 = 黄色 PLAYING/PAUSED 提示替代编辑状态
|
||||
{
|
||||
char tip[128];
|
||||
if (playing) {
|
||||
const bool paused = ctx.pausedState && *ctx.pausedState;
|
||||
std::snprintf(tip, sizeof(tip),
|
||||
paused ? "PAUSED - Ctrl+P to stop"
|
||||
: "PLAYING - Ctrl+P to stop");
|
||||
} else {
|
||||
std::snprintf(tip, sizeof(tip),
|
||||
"Space: %s [T] Snap: Ctrl F: Focus "
|
||||
"Ctrl+Shift+F: To Selection",
|
||||
gizmo.localSpace ? "Local" : "World");
|
||||
}
|
||||
const ImVec2 imgMin = ImGui::GetItemRectMin();
|
||||
ImGui::GetWindowDrawList()->AddText(
|
||||
ImVec2(imgMin.x + 8.0f, imgMin.y + 8.0f),
|
||||
playing ? IM_COL32(255, 184, 46, 220)
|
||||
: IM_COL32(255, 255, 255, 150),
|
||||
tip);
|
||||
}
|
||||
|
||||
// Dynamic 刚体提示:物理每帧把姿态写回 Transform,拖拽即刻
|
||||
// 被覆盖(Static/Kinematic 编辑器权威,可直接拖)。
|
||||
// 用 drawlist 叠加而非 ImGui::TextDisabled:后者在 Image 下方
|
||||
// 占一行布局 → 滚动条出现/消失循环 → avail 抖动 → 渲染分辨率
|
||||
// 每帧重报(视口疯狂顶动);且默认字体无中文字形(显示 ?)
|
||||
if (playing) {
|
||||
// 运行态无 primary 相机实体:渲染回退编辑器视角(保持帧
|
||||
// 管线节拍),提示补相机
|
||||
ECS::TransformComponent *camT = nullptr;
|
||||
if (ViewCamera(ctx, camT) == nullptr) {
|
||||
const ImVec2 imgMin = ImGui::GetItemRectMin();
|
||||
ImGui::GetWindowDrawList()->AddText(
|
||||
ImVec2(imgMin.x + 8.0f,
|
||||
imgMin.y + size.y * 0.5f),
|
||||
IM_COL32(255, 120, 80, 230),
|
||||
"No primary camera entity - showing editor "
|
||||
"view. Add a Camera component and check "
|
||||
"'Primary'");
|
||||
}
|
||||
}
|
||||
|
||||
// Dynamic 刚体提示(drawlist 叠加,理由同上)
|
||||
if (selected != entt::null) {
|
||||
if (const auto *rb = reg.try_get<ECS::RigidBodyComponent>(selected);
|
||||
rb != nullptr &&
|
||||
rb->type == ECS::RigidBodyComponent::BodyType::Dynamic) {
|
||||
const ImVec2 imgMin = ImGui::GetItemRectMin();
|
||||
ImGui::GetWindowDrawList()->AddText(
|
||||
ImVec2(imgMin.x + 8.0f, imgMin.y + size.y - 20.0f),
|
||||
IM_COL32(255, 255, 255, 160),
|
||||
"(Dynamic body: transform driven by physics)");
|
||||
}
|
||||
}
|
||||
|
||||
// 点击拾取:单击 Image 且未命中 UI 遮挡(IsItemHovered 已含)。
|
||||
// 抓杆时不出 pick(IsOver 悬停杆/IsUsing 拖拽中)——否则按在
|
||||
// 操纵杆上的点击会穿透发拾取请求。PIE 运行态不出拾取:
|
||||
// 点击 = 游戏输入(UE Play 语义,编辑操作冻结)
|
||||
if (!playing && ImGui::IsItemHovered() && ImGui::IsMouseClicked(0) &&
|
||||
!ImGuizmo::IsOver() && !ImGuizmo::IsUsing()) {
|
||||
RequestPickAt(ctx, size);
|
||||
}
|
||||
} else {
|
||||
ImGui::TextDisabled("(scene texture not ready)");
|
||||
}
|
||||
|
||||
ImGui::End();
|
||||
}
|
||||
ImGui::PopStyleVar();
|
||||
return picked;
|
||||
}
|
||||
|
||||
void ViewportPanel::RequestPickAt(const EditorContext &ctx,
|
||||
const ImVec2 &size) const {
|
||||
// 鼠标在 Image 内的位置 → NDC。Vulkan NDC y 与屏幕 v 同向
|
||||
//(向下为正,与渲染同构;skybox.frag 反投影交叉验证过方向)
|
||||
const ImVec2 mouse = ImGui::GetMousePos();
|
||||
const ImVec2 mn = ImGui::GetItemRectMin();
|
||||
const glm::vec2 ndc{
|
||||
(mouse.x - mn.x) / size.x * 2.0f - 1.0f,
|
||||
(mouse.y - mn.y) / size.y * 2.0f - 1.0f,
|
||||
};
|
||||
|
||||
// UE 式 GPU HitProxy:下一帧 id 渲染 + 请求像素 1×1 拷贝
|
||||
ctx.renderer->RequestPick(ndc);
|
||||
}
|
||||
|
||||
void ViewportPanel::LogPick(const EditorContext &ctx, entt::entity e) {
|
||||
// 命中详情:实体名;有物理体则附 collider 对照(诊断"物理盒 ≠
|
||||
// 视觉"的错位,如 M1911 的默认 0.5 半尺寸盒)
|
||||
auto ® = ctx.scene->GetRegistry();
|
||||
const auto &name = reg.get<ECS::NameComponent>(e).name;
|
||||
if (const auto *rb = reg.try_get<ECS::RigidBodyComponent>(e)) {
|
||||
const auto &tr = reg.get<ECS::TransformComponent>(e);
|
||||
ENGINE_INFO("[pick] '{}' (hitproxy) | collider={} type={} "
|
||||
"boxHalf=({:.2f},{:.2f},{:.2f}) sphereR={:.2f} "
|
||||
"bodyPos=({:.2f},{:.2f},{:.2f})",
|
||||
name,
|
||||
rb->collider == ECS::RigidBodyComponent::ColliderType::Box
|
||||
? "box" : "sphere",
|
||||
rb->type == ECS::RigidBodyComponent::BodyType::Static
|
||||
? "static" : "dynamic",
|
||||
rb->boxHalfExtents.x, rb->boxHalfExtents.y,
|
||||
rb->boxHalfExtents.z, rb->sphereRadius,
|
||||
tr.GetWorldPosition().x, tr.GetWorldPosition().y, tr.GetWorldPosition().z);
|
||||
} else {
|
||||
ENGINE_INFO("[pick] '{}' (hitproxy) | (no collider)", name);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
//
|
||||
// ViewportPanel:场景渲染输出面板。
|
||||
//
|
||||
// tonemap 输出经 ImGui::Image 显示(Editor 模式),并提供
|
||||
// Editor Viewport / Fullscreen 显示模式切换。面板可用区域
|
||||
// (物理像素)上报为渲染分辨率 → 画面精确填满面板。
|
||||
//
|
||||
// 点击拾取(UE 式 GPU HitProxy):单击 → NDC 请求 id 渲染;结果
|
||||
// 由 Draw 每帧轮询取走(像素级,与视觉画面一致)。
|
||||
//
|
||||
|
||||
#ifndef HELLOVULKAN_VIEWPORTPANEL_H
|
||||
#define HELLOVULKAN_VIEWPORTPANEL_H
|
||||
|
||||
#include "EditorContext.h"
|
||||
|
||||
#include <imgui.h> // ImVec2(RequestPickAt 签名)
|
||||
#include <entt/entt.hpp>
|
||||
#include <optional>
|
||||
|
||||
// Viewport 面板每帧上报的输入焦点状态(Hazel 模式:编辑器据此决定
|
||||
// 游戏侧输入是否放行——悬停/持焦 Viewport 才拥有 WASD/鼠标)
|
||||
struct ViewportInputState {
|
||||
bool hovered = false; // 鼠标悬停在 Viewport 面板上
|
||||
bool focused = false; // Viewport 面板持有键盘焦点
|
||||
};
|
||||
|
||||
// Gizmo 交互状态(Editor 持有、ViewportPanel 写):
|
||||
// op 取 ImGuizmo::OPERATION 位掩码(TRANSLATE=7=三轴位或运算,非 0
|
||||
// 起始枚举;0 = 无任何轴位 → Gizmo 完全不绘制),int 存储避免本
|
||||
// 头文件 include ImGuizmo.h(拖入 imgui 宏依赖顺序)
|
||||
// W/E/R 切换 op、T 切换 local/world 空间(UE 语义);dragging 拉起
|
||||
// Editor::BlockingMouse 门控
|
||||
struct GizmoState {
|
||||
int op = 7; // ImGuizmo::TRANSLATE(TRANSLATE_X|Y|Z)
|
||||
bool localSpace = true; // 操纵空间:true=模型轴(旋转/缩放语义),
|
||||
// T 键切 world
|
||||
bool dragging = false; // 拖拽中(IsUsing)→ 鼠标归 Gizmo 不给游戏侧
|
||||
};
|
||||
|
||||
class ViewportPanel {
|
||||
public:
|
||||
// 返回值:本帧拾取命中的实体(无点击/未命中 = nullopt 或 null);
|
||||
// Editor 据此 SelectEntity(面板不持有选中状态)
|
||||
std::optional<entt::entity> Draw(const EditorContext& ctx,
|
||||
ViewportInputState& out,
|
||||
GizmoState& gizmo,
|
||||
entt::entity selected) const;
|
||||
|
||||
private:
|
||||
// Image 区域内的鼠标位置 → NDC → Renderer::RequestPick
|
||||
//(结果下一帧由 Draw 轮询 TakePickResult 取走)
|
||||
void RequestPickAt(const EditorContext& ctx, const ImVec2& size) const;
|
||||
|
||||
// 命中打印:实体名 + collider 参数对照(诊断物理盒 ≠ 视觉用)
|
||||
static void LogPick(const EditorContext& ctx, entt::entity e);
|
||||
};
|
||||
|
||||
#endif //HELLOVULKAN_VIEWPORTPANEL_H
|
||||
Reference in New Issue
Block a user