feat: ImGui docking Editor + 显示链/帧同步/输入语义重构
- App/editor: Hierarchy/Inspector/Viewport/Stats 四面板 + DockBuilder 默认布局,main.cpp 瘦身为场景搭建与生命周期 - 显示链: Tonemap→PresentTarget 经 ImGui::Image 呈现(零色偏),backbuffer 仅承载 ImGui;Direct/Viewport 双显示模式 + 面板分辨率上报 - 帧首同步 BeginFrameSync: resize/vsync/尺寸/模式意图统一帧首落定(修描述符悬垂);修复 vsync 意图未落 swapchain 导致的每帧重建交换链 - 输入: Editor 每帧判定键盘/鼠标归属(打字让位、Viewport hover/focus 放行、Fullscreen 全放行) - 材质: roughness 独立槽位 + 非 glTF 金属/粗糙贴图映射;透明队列 depthWrite=FALSE + translucent 独立 shader + scratch 清帧 - FBX: 非蒙皮网格烘焙节点链全局变换(Z-up 旋转/单位换算落顶点) - Hierarchy 行 ImGui ID 以 UUID 作用域(重名不冲突、改名不丢选中)
This commit is contained in:
@@ -0,0 +1,202 @@
|
||||
//
|
||||
// Editor.cpp:docking 骨架实现。
|
||||
//
|
||||
// 布局方案(首帧 DockBuilder 一次,之后 imgui.ini 接管):
|
||||
// ┌──────────────────────────────┬───────────────┐
|
||||
// │ Hierarchy │ │
|
||||
// ├──────────────────────────────┤ Inspector │
|
||||
// │ Stats │ (28%) │
|
||||
// └──────────────────────────────┴───────────────┘
|
||||
//
|
||||
|
||||
#include "Editor.h"
|
||||
#include "HierarchyPanel.h"
|
||||
#include "InspectorPanel.h"
|
||||
#include "ViewportPanel.h"
|
||||
#include "StatsPanel.h"
|
||||
|
||||
#include <imgui.h>
|
||||
#include <imgui_internal.h> // DockBuilder*(内部 API)
|
||||
|
||||
#include <GLFW/glfw3.h>
|
||||
|
||||
using namespace Engine; // kDisplayViewport / kDisplayDirect(SceneTypes.h)
|
||||
|
||||
Editor::Editor() = default;
|
||||
|
||||
Editor::~Editor() = default;
|
||||
|
||||
void Editor::Init(const EditorContext& ctx)
|
||||
{
|
||||
m_Ctx = ctx;
|
||||
|
||||
m_Hierarchy = std::make_unique<HierarchyPanel>();
|
||||
m_Inspector = std::make_unique<InspectorPanel>();
|
||||
m_Viewport = std::make_unique<ViewportPanel>();
|
||||
m_Stats = std::make_unique<StatsPanel>();
|
||||
}
|
||||
|
||||
void Editor::Draw()
|
||||
{
|
||||
// host 窗口:铺满主视口,承载菜单栏 + DockSpace
|
||||
ImGuiViewport* viewport = ImGui::GetMainViewport();
|
||||
ImGui::SetNextWindowPos(viewport->Pos);
|
||||
ImGui::SetNextWindowSize(viewport->Size);
|
||||
ImGui::SetNextWindowViewport(viewport->ID);
|
||||
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f));
|
||||
ImGui::Begin("##EditorDockHost", nullptr,
|
||||
ImGuiWindowFlags_MenuBar | ImGuiWindowFlags_NoDocking |
|
||||
ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoCollapse |
|
||||
ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove |
|
||||
ImGuiWindowFlags_NoBringToFrontOnFocus |
|
||||
ImGuiWindowFlags_NoNavFocus);
|
||||
ImGui::PopStyleVar(3);
|
||||
|
||||
DrawMenuBar();
|
||||
DrawDockSpace();
|
||||
|
||||
ImGui::End();
|
||||
|
||||
// 常规面板窗口(窗口名 = docking 身份)
|
||||
if (m_ShowHierarchy) m_Hierarchy->Draw(m_Ctx, m_Selected);
|
||||
if (m_ShowInspector) m_Inspector->Draw(m_Ctx, m_Selected);
|
||||
if (m_ShowViewport) m_Viewport->Draw(m_Ctx, m_VpInput);
|
||||
if (m_ShowStats) m_Stats->Draw(m_Ctx);
|
||||
|
||||
DrawQuitModal();
|
||||
}
|
||||
|
||||
// 输入归属判定:ImGui 侧经自身后端链式回调永远正常收输入,这里
|
||||
// 只决定"剩余输入给不给游戏侧"。UI 被隐藏(反引号)时不会走到
|
||||
// Draw → m_VpInput 停在旧值,谓词调用方(SandboxApp)须先查
|
||||
// GetImGuiEnabled() 再问这里。
|
||||
bool Editor::BlockingKeyboard() const
|
||||
{
|
||||
const ImGuiIO& io = ImGui::GetIO();
|
||||
if (io.WantTextInput) return true; // 打字中(改名等)不让位快捷键
|
||||
|
||||
// 游戏形态(Fullscreen):WASD 全窗口生效,键盘只让位文本输入
|
||||
if (m_Ctx.renderer &&
|
||||
m_Ctx.renderer->GetDisplayMode() == kDisplayDirect)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 编辑器形态:Viewport 面板持焦才拥有键盘(UE Scene view 语义)
|
||||
return !m_VpInput.focused;
|
||||
}
|
||||
|
||||
bool Editor::BlockingMouse() const
|
||||
{
|
||||
// 游戏形态:沿用 ImGui 全局判定(UI 隐藏时 WantCaptureMouse=false)
|
||||
if (m_Ctx.renderer &&
|
||||
m_Ctx.renderer->GetDisplayMode() == kDisplayDirect)
|
||||
{
|
||||
return ImGui::GetIO().WantCaptureMouse;
|
||||
}
|
||||
|
||||
// 编辑器形态:悬停 Viewport 才放行(模态框遮挡时 hovered=false)
|
||||
return !m_VpInput.hovered;
|
||||
}
|
||||
|
||||
void Editor::DrawMenuBar()
|
||||
{
|
||||
if (!ImGui::BeginMenuBar())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (ImGui::BeginMenu("File"))
|
||||
{
|
||||
if (ImGui::MenuItem("Quit", "Alt+F4"))
|
||||
{
|
||||
RequestQuit();
|
||||
}
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
|
||||
if (ImGui::BeginMenu("View"))
|
||||
{
|
||||
ImGui::MenuItem("Hierarchy", nullptr, &m_ShowHierarchy);
|
||||
ImGui::MenuItem("Inspector", nullptr, &m_ShowInspector);
|
||||
ImGui::MenuItem("Viewport", nullptr, &m_ShowViewport);
|
||||
ImGui::MenuItem("Stats", nullptr, &m_ShowStats);
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
|
||||
ImGui::EndMenuBar();
|
||||
}
|
||||
|
||||
void Editor::DrawDockSpace()
|
||||
{
|
||||
const ImGuiID dockspaceId = ImGui::GetID("EditorDockSpace");
|
||||
|
||||
// 默认布局:仅当 dock node 不存在时构建一次
|
||||
//(imgui.ini 已保存过布局 → DockBuilderGetNode 非空 → 跳过)
|
||||
if (!m_LayoutBuilt && ImGui::DockBuilderGetNode(dockspaceId) == nullptr)
|
||||
{
|
||||
m_LayoutBuilt = true;
|
||||
|
||||
ImGui::DockBuilderRemoveNode(dockspaceId);
|
||||
ImGui::DockBuilderAddNode(dockspaceId, ImGuiDockNodeFlags_DockSpace);
|
||||
ImGui::DockBuilderSetNodeSize(dockspaceId, ImGui::GetMainViewport()->Size);
|
||||
|
||||
ImGuiID dockLeft = 0, dockRight = 0, dockCenter = 0, dockStats = 0;
|
||||
ImGui::DockBuilderSplitNode(dockspaceId, ImGuiDir_Left, 0.20f,
|
||||
&dockLeft, &dockCenter);
|
||||
ImGui::DockBuilderSplitNode(dockCenter, ImGuiDir_Right, 0.28f,
|
||||
&dockRight, &dockCenter);
|
||||
ImGui::DockBuilderSplitNode(dockLeft, ImGuiDir_Down, 0.38f,
|
||||
&dockStats, &dockLeft);
|
||||
|
||||
ImGui::DockBuilderDockWindow("Hierarchy", dockLeft);
|
||||
ImGui::DockBuilderDockWindow("Stats", dockStats);
|
||||
ImGui::DockBuilderDockWindow("Inspector", dockRight);
|
||||
ImGui::DockBuilderDockWindow("Viewport", dockCenter);
|
||||
|
||||
ImGui::DockBuilderFinish(dockspaceId);
|
||||
}
|
||||
|
||||
ImGui::DockSpace(dockspaceId, ImVec2(0.0f, 0.0f),
|
||||
ImGuiDockNodeFlags_None);
|
||||
}
|
||||
|
||||
void Editor::DrawQuitModal()
|
||||
{
|
||||
if (m_ShowQuitConfirm)
|
||||
{
|
||||
ImGui::OpenPopup("Should quit?");
|
||||
m_ShowQuitConfirm = false;
|
||||
}
|
||||
|
||||
ImGui::SetNextWindowPos(
|
||||
ImGui::GetMainViewport()->GetCenter(),
|
||||
ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
|
||||
|
||||
if (ImGui::BeginPopupModal("Should quit?", nullptr,
|
||||
ImGuiWindowFlags_AlwaysAutoResize))
|
||||
{
|
||||
ImGui::TextUnformatted("Quit the application?");
|
||||
ImGui::Separator();
|
||||
if (ImGui::Button("Yes", ImVec2(120, 0)))
|
||||
{
|
||||
m_QuitConfirmed = true;
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("No", ImVec2(120, 0)))
|
||||
{
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
|
||||
if (m_QuitConfirmed)
|
||||
{
|
||||
m_QuitConfirmed = false;
|
||||
m_Ctx.app->Close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
//
|
||||
// Editor:docking 编辑器骨架。
|
||||
//
|
||||
// 职责:
|
||||
// - 顶栏菜单(File/View)+ 全屏 DockSpace + 首帧默认布局(DockBuilder)
|
||||
// - 实体选中状态(entt::entity,Hierarchy 写 / Inspector 读)
|
||||
// - 退出确认模态框(窗口关闭 → RequestQuit → Yes 时 Close)
|
||||
//
|
||||
// 面板窗口名(Hierarchy/Inspector/Viewport/Stats)即 docking 身份,
|
||||
// imgui.ini 持久化布局;默认布局只在 dock node 为空时构建一次。
|
||||
//
|
||||
|
||||
#ifndef HELLOVULKAN_EDITOR_H
|
||||
#define HELLOVULKAN_EDITOR_H
|
||||
|
||||
#include "EditorContext.h"
|
||||
#include "ViewportPanel.h" // ViewportInputState(成员按值持有)
|
||||
|
||||
#include <entt/entt.hpp>
|
||||
|
||||
#include <memory>
|
||||
|
||||
class HierarchyPanel;
|
||||
class InspectorPanel;
|
||||
class ViewportPanel;
|
||||
class StatsPanel;
|
||||
|
||||
class Editor {
|
||||
public:
|
||||
Editor();
|
||||
~Editor();
|
||||
|
||||
Editor(const Editor&) = delete;
|
||||
Editor& operator=(const Editor&) = delete;
|
||||
|
||||
// 注入上下文(OnInit 阶段,指针必须全部有效)
|
||||
void Init(const EditorContext& ctx);
|
||||
|
||||
// 每帧 UI 构建(OnImGuiRender 唯一入口)
|
||||
void Draw();
|
||||
|
||||
// ---- 输入归属判定(Window 层输入捕获谓词;UE/Unity 的
|
||||
// "viewport 持焦才拥有输入" 语义,ImGui 侧经自身后端
|
||||
// 链式回调永远正常收输入)----
|
||||
// 键盘:编辑器形态 = Viewport 持焦且不在文本输入中;游戏形态
|
||||
// (Fullscreen)只在文本输入中让位(打字不触发快捷键)
|
||||
bool BlockingKeyboard() const;
|
||||
// 鼠标:编辑器形态 = 悬停 Viewport 才放行;游戏形态沿用
|
||||
// WantCaptureMouse(UI 隐藏时自动放行)
|
||||
bool BlockingMouse() const;
|
||||
|
||||
// 请求退出(弹确认框;窗口关闭事件走这里)
|
||||
void RequestQuit() { m_ShowQuitConfirm = true; }
|
||||
|
||||
// ---- 选中状态 ----
|
||||
void SelectEntity(entt::entity e) { m_Selected = e; }
|
||||
entt::entity GetSelected() const { return m_Selected; }
|
||||
|
||||
private:
|
||||
void DrawMenuBar(); // host 窗口内顶栏
|
||||
void DrawDockSpace(); // 全屏 host + dockspace + 默认布局
|
||||
void DrawQuitModal(); // 退出确认模态框
|
||||
|
||||
EditorContext m_Ctx{};
|
||||
|
||||
ViewportInputState m_VpInput{}; // Viewport 面板 hover/focus(每帧刷新)
|
||||
|
||||
std::unique_ptr<HierarchyPanel> m_Hierarchy;
|
||||
std::unique_ptr<InspectorPanel> m_Inspector;
|
||||
std::unique_ptr<ViewportPanel> m_Viewport;
|
||||
std::unique_ptr<StatsPanel> m_Stats;
|
||||
|
||||
entt::entity m_Selected{entt::null};
|
||||
|
||||
// 面板显隐(View 菜单)
|
||||
bool m_ShowHierarchy = true;
|
||||
bool m_ShowInspector = true;
|
||||
bool m_ShowViewport = true;
|
||||
bool m_ShowStats = true;
|
||||
|
||||
// 默认布局只建一次(imgui.ini 已有布局则跳过)
|
||||
bool m_LayoutBuilt = false;
|
||||
|
||||
// 退出确认链
|
||||
bool m_ShowQuitConfirm = false;
|
||||
bool m_QuitConfirmed = false;
|
||||
};
|
||||
|
||||
#endif //HELLOVULKAN_EDITOR_H
|
||||
@@ -0,0 +1,48 @@
|
||||
//
|
||||
// EditorContext:Editor 层与 SandboxApp 之间的桥接上下文。
|
||||
//
|
||||
// Editor 是 App 层代码(不属于引擎),它需要的引擎门面与沙盒能力
|
||||
// 全部经此聚合注入——面板只认识 EditorContext,不反向依赖 main.cpp。
|
||||
//
|
||||
|
||||
#ifndef HELLOVULKAN_EDITORCONTEXT_H
|
||||
#define HELLOVULKAN_EDITORCONTEXT_H
|
||||
|
||||
#include <Engine/Core/Application.h>
|
||||
#include <Engine/ECS/Scene.h>
|
||||
#include <Engine/Assets/AssetManager.h>
|
||||
#include <Engine/Renderer/Renderer.h>
|
||||
#include <Engine/Renderer/SceneTypes.h> // kDisplayViewport / kDisplayDirect
|
||||
|
||||
#include <functional>
|
||||
|
||||
// 可切换环境(IBL 动态切换):StatsPanel 的 Combo 与 SandboxApp 的
|
||||
// 加载逻辑共用这一张表(下标即身份)。
|
||||
inline constexpr const char *kEnvPaths[] = {
|
||||
"assets/textures/venice_dawn_1_4k.hdr",
|
||||
"assets/textures/rooitou_park_4k.hdr",
|
||||
"assets/textures/pink_sunrise_4k.hdr",
|
||||
"assets/textures/birchwood_4k.hdr",
|
||||
};
|
||||
inline constexpr const char *kEnvNames[] = {
|
||||
"Venice Dawn", "Rooitou Park", "Pink Sunrise", "Birchwood",
|
||||
};
|
||||
inline constexpr int kEnvCount = 4;
|
||||
inline constexpr int kDefaultEnv = 0;
|
||||
|
||||
// 沙盒特有状态(面板可选读取,空指针 = 该面板区块不显示)
|
||||
struct EditorContext {
|
||||
// 引擎门面
|
||||
Engine::Renderer *renderer = nullptr;
|
||||
Engine::ECS::Scene *scene = nullptr;
|
||||
Engine::Assets::AssetManager *assets = nullptr;
|
||||
Engine::Application *app = nullptr;
|
||||
|
||||
std::function<int()> getEnvIndex; // 当前环境表下标
|
||||
std::function<void(int)> setEnvIndex;
|
||||
|
||||
// 冻结动画时钟(沙盒 spinners 用;保留原 Scene Debug 行为)
|
||||
bool *pauseAnim = nullptr;
|
||||
};
|
||||
|
||||
#endif //HELLOVULKAN_EDITORCONTEXT_H
|
||||
@@ -0,0 +1,86 @@
|
||||
//
|
||||
// HierarchyPanel.cpp:实体列表实现。
|
||||
//
|
||||
|
||||
#include "HierarchyPanel.h"
|
||||
#include "Engine/ECS/Components.h"
|
||||
#include "Engine/ECS/EntityHandle.h" // CreateEntity 返回完整 EntityHandle
|
||||
|
||||
#include <imgui.h>
|
||||
|
||||
using namespace Engine;
|
||||
|
||||
namespace {
|
||||
struct ScopedID {
|
||||
explicit ScopedID(const u64 uuid) {
|
||||
ImGui::PushID(reinterpret_cast<const void*>(uuid));
|
||||
}
|
||||
~ScopedID() {ImGui::PopID(); }
|
||||
};
|
||||
}
|
||||
|
||||
void HierarchyPanel::Draw(const EditorContext& ctx, entt::entity& selected)
|
||||
{
|
||||
ImGui::Begin("Hierarchy");
|
||||
|
||||
// 工具栏:创建实体(在遍历之前,不修改遍历中的注册表结构)
|
||||
if (ImGui::Button("+ Create Entity"))
|
||||
{
|
||||
const auto e = ctx.scene->CreateEntity("Entity");
|
||||
selected = e;
|
||||
}
|
||||
ImGui::Separator();
|
||||
|
||||
auto& reg = ctx.scene->GetRegistry();
|
||||
|
||||
reg.view<Engine::ECS::NameComponent>().each(
|
||||
[&](const entt::entity e, const Engine::ECS::NameComponent& name)
|
||||
{
|
||||
const bool isSelected = (selected == e);
|
||||
|
||||
u64 rowId = static_cast < uint64_t >( static_cast < uint32_t >(e));
|
||||
if (const auto * id = reg.try_get<Engine::ECS::IdComponent>(e)) {
|
||||
rowId = id->uuid.Value();
|
||||
}
|
||||
const ScopedID scope (rowId);
|
||||
|
||||
const std::string label = name.name.empty() ? "(unnamed)" : name.name;
|
||||
if (ImGui::Selectable(label.c_str(), isSelected))
|
||||
{
|
||||
selected = e;
|
||||
}
|
||||
|
||||
// 右键菜单:删除(延迟销毁,帧尾统一 flush)
|
||||
if (ImGui::BeginPopupContextItem("entity_ctx"))
|
||||
{
|
||||
if (ImGui::MenuItem("Delete"))
|
||||
{
|
||||
ctx.scene->DestroyEntity(e);
|
||||
if (selected == e)
|
||||
{
|
||||
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 (!badge.empty())
|
||||
{
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled("%s", badge.c_str());
|
||||
if (ImGui::IsItemHovered())
|
||||
{
|
||||
ImGui::SetTooltip(
|
||||
"M=Mesh A=Animation C=Camera L=Light");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
ImGui::End();
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
//
|
||||
// HierarchyPanel:实体列表面板。
|
||||
//
|
||||
// 点击选中(写 Editor 的选中状态)、工具栏创建实体、右键删除
|
||||
//(延迟销毁,帧尾 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
|
||||
@@ -0,0 +1,220 @@
|
||||
//
|
||||
// InspectorPanel.cpp:组件编辑实现。
|
||||
//
|
||||
// Transform 编辑必须走 Set* 入口(标脏 → 帧循环统一 Compose 重算
|
||||
// world),直接改私有成员是编译错误——面板天然遵守引擎约束。
|
||||
//
|
||||
|
||||
#include "InspectorPanel.h"
|
||||
#include "Engine/ECS/Components.h"
|
||||
#include "Engine/Renderer/Model.h"
|
||||
|
||||
#include <imgui.h>
|
||||
|
||||
#include <cstdio>
|
||||
#include <vector>
|
||||
|
||||
namespace
|
||||
{
|
||||
// InputText 需要 char 缓冲;提交时写回 std::string
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
|
||||
{
|
||||
ImGui::Begin("Inspector");
|
||||
|
||||
auto& reg = ctx.scene->GetRegistry();
|
||||
if (selected == entt::null || !reg.valid(selected))
|
||||
{
|
||||
ImGui::TextDisabled("(no entity selected)");
|
||||
ImGui::End();
|
||||
return;
|
||||
}
|
||||
|
||||
// ---- Name ----
|
||||
if (auto* name = reg.try_get<Engine::ECS::NameComponent>(selected))
|
||||
{
|
||||
InputTextString("Name", name->name);
|
||||
}
|
||||
ImGui::Separator();
|
||||
|
||||
// ---- Add Component ----
|
||||
if (ImGui::Button("Add Component"))
|
||||
{
|
||||
ImGui::OpenPopup("add_component");
|
||||
}
|
||||
if (ImGui::BeginPopup("add_component"))
|
||||
{
|
||||
if (!reg.all_of<Engine::ECS::LightComponent>(selected) &&
|
||||
ImGui::MenuItem("Light"))
|
||||
{
|
||||
reg.emplace<Engine::ECS::LightComponent>(selected);
|
||||
}
|
||||
if (!reg.all_of<Engine::ECS::CameraComponent>(selected) &&
|
||||
ImGui::MenuItem("Camera"))
|
||||
{
|
||||
reg.emplace<Engine::ECS::CameraComponent>(selected);
|
||||
}
|
||||
if (!reg.all_of<Engine::ECS::AnimationComponent>(selected) &&
|
||||
ImGui::MenuItem("Animation"))
|
||||
{
|
||||
reg.emplace<Engine::ECS::AnimationComponent>(selected);
|
||||
}
|
||||
if (!reg.all_of<Engine::ECS::NameComponent>(selected) &&
|
||||
ImGui::MenuItem("Name"))
|
||||
{
|
||||
reg.emplace<Engine::ECS::NameComponent>(selected);
|
||||
}
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
ImGui::Separator();
|
||||
|
||||
// ---- Transform ----
|
||||
if (auto* t = reg.try_get<Engine::ECS::TransformComponent>(selected))
|
||||
{
|
||||
if (ImGui::CollapsingHeader("Transform", ImGuiTreeNodeFlags_DefaultOpen))
|
||||
{
|
||||
glm::vec3 pos = t->GetPosition();
|
||||
if (ImGui::DragFloat3("position", &pos.x, 0.05f))
|
||||
{
|
||||
t->SetPosition(pos);
|
||||
}
|
||||
glm::vec3 rot = t->GetRotation(); // 欧拉角(度)
|
||||
if (ImGui::DragFloat3("rotation", &rot.x, 0.5f))
|
||||
{
|
||||
t->SetRotation(rot);
|
||||
}
|
||||
glm::vec3 scl = t->GetScale();
|
||||
if (ImGui::DragFloat3("scale", &scl.x, 0.02f, 0.001f, 100.0f))
|
||||
{
|
||||
t->SetScale(scl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- MeshRenderer ----
|
||||
if (auto* mr = reg.try_get<Engine::ECS::MeshRendererComponent>(selected))
|
||||
{
|
||||
if (ImGui::CollapsingHeader("MeshRenderer",
|
||||
ImGuiTreeNodeFlags_DefaultOpen))
|
||||
{
|
||||
if (const Engine::Model* model = ctx.assets->GetModel(mr->model);
|
||||
model != nullptr)
|
||||
{
|
||||
const auto& clips = model->GetClips();
|
||||
ImGui::Text("model: valid (%u clips, %u bones)",
|
||||
static_cast<unsigned>(clips.size()),
|
||||
static_cast<unsigned>(model->GetBoneCount()));
|
||||
ImGui::TextDisabled("(mesh asset is read-only here)");
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f),
|
||||
"model: INVALID (dangling ref)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Light ----
|
||||
if (auto* light = reg.try_get<Engine::ECS::LightComponent>(selected))
|
||||
{
|
||||
if (ImGui::CollapsingHeader("Light", ImGuiTreeNodeFlags_DefaultOpen))
|
||||
{
|
||||
int type = static_cast<int>(light->type);
|
||||
if (ImGui::Combo("type", &type, "Directional\0Point\0"))
|
||||
{
|
||||
light->type = static_cast<Engine::ECS::LightComponent::Type>(type);
|
||||
}
|
||||
ImGui::ColorEdit3("color", &light->color.x);
|
||||
ImGui::DragFloat("intensity", &light->color.a, 0.05f, 0.0f, 100.0f);
|
||||
if (light->type == Engine::ECS::LightComponent::Type::Directional)
|
||||
{
|
||||
ImGui::SliderFloat3("direction", &light->direction.x, -1.0f, 1.0f);
|
||||
ImGui::Checkbox("castShadow", &light->castShadow);
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui::DragFloat("radius", &light->radius, 0.1f, 0.0f, 100.0f);
|
||||
ImGui::TextDisabled("(position = entity Transform)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Camera ----
|
||||
if (auto* cam = reg.try_get<Engine::ECS::CameraComponent>(selected))
|
||||
{
|
||||
if (ImGui::CollapsingHeader("Camera", ImGuiTreeNodeFlags_DefaultOpen))
|
||||
{
|
||||
if (ImGui::Checkbox("primary", &cam->primary) && cam->primary)
|
||||
{
|
||||
// primary 互斥:启用本相机时清掉其余实体的 primary
|
||||
//(引擎取第一个 primary 作为渲染视角)
|
||||
reg.view<Engine::ECS::CameraComponent>().each(
|
||||
[&](entt::entity e, Engine::ECS::CameraComponent& c)
|
||||
{
|
||||
if (e != selected)
|
||||
{
|
||||
c.primary = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Animation ----
|
||||
if (auto* anim = reg.try_get<Engine::ECS::AnimationComponent>(selected))
|
||||
{
|
||||
if (ImGui::CollapsingHeader("Animation",
|
||||
ImGuiTreeNodeFlags_DefaultOpen))
|
||||
{
|
||||
const auto* mr = reg.try_get<Engine::ECS::MeshRendererComponent>(selected);
|
||||
const Engine::Model* model =
|
||||
mr ? ctx.assets->GetModel(mr->model) : nullptr;
|
||||
|
||||
if (model != nullptr && model->HasAnimations())
|
||||
{
|
||||
const auto& clips = model->GetClips();
|
||||
std::vector<const char*> clipNames;
|
||||
clipNames.reserve(clips.size());
|
||||
for (const auto& c : clips)
|
||||
{
|
||||
clipNames.push_back(c.name.c_str());
|
||||
}
|
||||
int cur = anim->clipIndex;
|
||||
if (ImGui::Combo("clip", &cur, clipNames.data(),
|
||||
static_cast<int>(clipNames.size())))
|
||||
{
|
||||
anim->Play(cur); // 触发 crossfade(fade 秒)
|
||||
}
|
||||
ImGui::Checkbox("playing", &anim->playing);
|
||||
ImGui::SameLine();
|
||||
ImGui::Checkbox("loop", &anim->loop);
|
||||
ImGui::SliderFloat("speed", &anim->speed, -2.0f, 2.0f);
|
||||
ImGui::SliderFloat("fade", &anim->fadeDuration, 0.0f, 1.0f);
|
||||
if (anim->clipIndex >= 0 &&
|
||||
anim->clipIndex < static_cast<int>(clips.size()))
|
||||
{
|
||||
ImGui::Text("t: %.2f / %.2f s", anim->time,
|
||||
clips[anim->clipIndex].duration);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui::TextDisabled("(no animation clips on model)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::End();
|
||||
}
|
||||
@@ -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,173 @@
|
||||
//
|
||||
// StatsPanel.cpp:遥测与全局设置实现(原 main.cpp Scene Debug 搬迁)。
|
||||
//
|
||||
|
||||
#include "StatsPanel.h"
|
||||
#include "Engine/ECS/Components.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;
|
||||
|
||||
// ---- Camera:主相机位姿(只读遥测;位姿编辑走 Inspector)----
|
||||
{
|
||||
Engine::ECS::TransformComponent* camT = nullptr;
|
||||
if (ctx.scene->GetPrimaryCamera(&camT) != nullptr && camT != nullptr)
|
||||
{
|
||||
const glm::vec3& pos = camT->GetPosition();
|
||||
ImGui::Text("Camera pos");
|
||||
ImGui::Text("(%.3f, %.3f, %.3f)", pos.x, pos.y, pos.z);
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui::TextDisabled("(no primary camera)");
|
||||
}
|
||||
ImGui::Separator();
|
||||
}
|
||||
|
||||
// ---- Scene:动画时钟冻结(沙盒 spinners 用)----
|
||||
if (ctx.pauseAnim != nullptr)
|
||||
{
|
||||
ImGui::Checkbox("Pause Animation", ctx.pauseAnim);
|
||||
}
|
||||
|
||||
// ---- 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, 2.0f))
|
||||
{
|
||||
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); // 关 = 恒绘原网格
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Environment:IBL 环境 + 调试视图(决定"看什么")----
|
||||
if (ImGui::CollapsingHeader("Environment", ImGuiTreeNodeFlags_DefaultOpen))
|
||||
{
|
||||
bool& drawSkybox = ctx.scene->GetSettings().DrawSkybox;
|
||||
ImGui::Checkbox("DrawSkybox", &drawSkybox);
|
||||
if (drawSkybox && ctx.getEnvIndex)
|
||||
{
|
||||
int envIdx = ctx.getEnvIndex();
|
||||
if (ImGui::Combo("env", &envIdx, kEnvNames, kEnvCount))
|
||||
{
|
||||
ctx.setEnvIndex(envIdx);
|
||||
const auto env = ctx.assets->LoadEnvironment(kEnvPaths[envIdx]);
|
||||
ctx.renderer->SetEnvironment(ctx.assets->GetEnvironment(env));
|
||||
}
|
||||
|
||||
// 调试视图:单看某个 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);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui::ColorEdit4("ClearColor",
|
||||
glm::value_ptr(ctx.scene->GetSettings().ClearColor));
|
||||
}
|
||||
}
|
||||
|
||||
// ---- GPU Timings:只读遥测(默认折叠,不占面板高度)----
|
||||
if (ImGui::CollapsingHeader("GPU Timings"))
|
||||
{
|
||||
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]);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Frame:帧率控制与显示 ----
|
||||
if (ImGui::CollapsingHeader("Frame", ImGuiTreeNodeFlags_DefaultOpen))
|
||||
{
|
||||
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 / app.GetDeltaTime(),
|
||||
static_cast<unsigned long long>(app.GetFrameCount()));
|
||||
}
|
||||
|
||||
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,53 @@
|
||||
//
|
||||
// ViewportPanel.cpp:显示模式单选 + ImGui::Image + 分辨率上报。
|
||||
// 逻辑照搬原 main.cpp 的 Viewport 窗口(行为不变)。
|
||||
//
|
||||
|
||||
#include "ViewportPanel.h"
|
||||
|
||||
#include <imgui.h>
|
||||
|
||||
#include <vulkan/vulkan.h> // vk::Extent2D 经 Renderer 头已带入,显式无妨
|
||||
|
||||
using namespace Engine; // u32 / f32(Engine/Core/Types.h)
|
||||
|
||||
void ViewportPanel::Draw(const EditorContext &ctx, ViewportInputState &out) {
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f));
|
||||
{
|
||||
ImGui::Begin("Viewport", nullptr, ImGuiWindowFlags_NoCollapse);
|
||||
|
||||
// 输入焦点上报(悬停计入遮挡:模态框盖住时不误报)
|
||||
out.hovered = ImGui::IsWindowHovered();
|
||||
out.focused = ImGui::IsWindowFocused();
|
||||
|
||||
auto &renderer = *ctx.renderer;
|
||||
const int mode = renderer.GetDisplayMode();
|
||||
|
||||
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);
|
||||
} else {
|
||||
ImGui::TextDisabled("(scene texture not ready)");
|
||||
}
|
||||
|
||||
ImGui::End();
|
||||
}
|
||||
ImGui::PopStyleVar();
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
//
|
||||
// ViewportPanel:场景渲染输出面板。
|
||||
//
|
||||
// tonemap 输出经 ImGui::Image 显示(Editor 模式),并提供
|
||||
// Editor Viewport / Fullscreen 显示模式切换。面板可用区域
|
||||
// (物理像素)上报为渲染分辨率 → 画面精确填满面板。
|
||||
//
|
||||
|
||||
#ifndef HELLOVULKAN_VIEWPORTPANEL_H
|
||||
#define HELLOVULKAN_VIEWPORTPANEL_H
|
||||
|
||||
#include "EditorContext.h"
|
||||
|
||||
// Viewport 面板每帧上报的输入焦点状态(Hazel 模式:编辑器据此决定
|
||||
// 游戏侧输入是否放行——悬停/持焦 Viewport 才拥有 WASD/鼠标)
|
||||
struct ViewportInputState {
|
||||
bool hovered = false; // 鼠标悬停在 Viewport 面板上
|
||||
bool focused = false; // Viewport 面板持有键盘焦点
|
||||
};
|
||||
|
||||
class ViewportPanel {
|
||||
public:
|
||||
void Draw(const EditorContext& ctx, ViewportInputState& out);
|
||||
};
|
||||
|
||||
#endif //HELLOVULKAN_VIEWPORTPANEL_H
|
||||
Reference in New Issue
Block a user