From 3ec96acc0f17d77a52793a11c97435e244b39e5f Mon Sep 17 00:00:00 2001 From: atdunbg Date: Fri, 2 Oct 2026 23:16:52 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20Editor=20Gizmo=E2=80=94=E2=80=94ImGuizm?= =?UTF-8?q?o=20=E5=B9=B3=E7=A7=BB/=E6=97=8B=E8=BD=AC/=E7=BC=A9=E6=94=BE?= =?UTF-8?q?=E6=93=8D=E7=BA=B5=E6=9D=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ImGuizmo 接入:Lib/ImGuizmo(src/ 布局)静态库,IMGUI_DEFINE_MATH_OPERATORS - ViewportPanel:Vulkan→GL 投影转换(Y 翻回 + 深度 [-1,1])、 SetDrawlist/SetRect 叠加绘制、decompose 写回 TransformComponent - W/E/R 切换(UE 语义);拖拽中 BlockingMouse 门控(输入归 Gizmo) - Dynamic 刚体选中提示:物理每帧写回,编辑被覆盖 - 修复:GizmoState.op 初值 0 无轴位(ImGuizmo::TRANSLATE=7)导致杆不绘制 - 点击拾取门控:抓杆时(IsOver/IsUsing)不发 pick 请求 --- .gitmodules | 3 + App/editor/Editor.cpp | 6 +- App/editor/Editor.h | 5 +- App/editor/ViewportPanel.cpp | 106 ++++++++++++++++++++++++++++++++++- App/editor/ViewportPanel.h | 15 ++++- Engine/CMakeLists.txt | 11 ++++ Lib/ImGuizmo | 1 + 7 files changed, 140 insertions(+), 7 deletions(-) create mode 160000 Lib/ImGuizmo diff --git a/.gitmodules b/.gitmodules index db1c060..4bcc889 100644 --- a/.gitmodules +++ b/.gitmodules @@ -29,3 +29,6 @@ [submodule "Lib/JoltPhysics"] path = Lib/JoltPhysics url = https://github.com/jrouwe/JoltPhysics.git +[submodule "Lib/ImGuizmo"] + path = Lib/ImGuizmo + url = https://github.com/CedricGuillemet/ImGuizmo.git diff --git a/App/editor/Editor.cpp b/App/editor/Editor.cpp index 5b2d832..f1d2d3c 100644 --- a/App/editor/Editor.cpp +++ b/App/editor/Editor.cpp @@ -65,7 +65,8 @@ void Editor::Draw() if (m_ShowInspector) m_Inspector->Draw(m_Ctx, m_Selected); if (m_ShowViewport) { - if (auto picked = m_Viewport->Draw(m_Ctx, m_VpInput)) { + if (auto picked = m_Viewport->Draw(m_Ctx, m_VpInput, m_Gizmo, + m_Selected)) { SelectEntity(*picked); // Viewport 点击拾取 → 选中联动 Inspector/Hierarchy } } @@ -96,6 +97,9 @@ bool Editor::BlockingKeyboard() const bool Editor::BlockingMouse() const { + // Gizmo 拖拽中:鼠标归操纵杆,不给游戏侧相机(松手自动恢复) + if (m_Gizmo.dragging) return true; + // 游戏形态:沿用 ImGui 全局判定(UI 隐藏时 WantCaptureMouse=false) if (m_Ctx.renderer && m_Ctx.renderer->GetDisplayMode() == kDisplayDirect) diff --git a/App/editor/Editor.h b/App/editor/Editor.h index 2b17a09..0e4171c 100644 --- a/App/editor/Editor.h +++ b/App/editor/Editor.h @@ -47,8 +47,8 @@ public: // 键盘:编辑器形态 = Viewport 持焦且不在文本输入中;游戏形态 // (Fullscreen)只在文本输入中让位(打字不触发快捷键) bool BlockingKeyboard() const; - // 鼠标:编辑器形态 = 悬停 Viewport 才放行;游戏形态沿用 - // WantCaptureMouse(UI 隐藏时自动放行) + // 鼠标:编辑器形态 = 悬停 Viewport 才放行;Gizmo 拖拽中一律拦截 + // (抓杆时相机不能吃鼠标);游戏形态沿用 WantCaptureMouse bool BlockingMouse() const; // 请求退出(弹确认框;窗口关闭事件走这里) @@ -77,6 +77,7 @@ private: EditorContext m_Ctx{}; ViewportInputState m_VpInput{}; // Viewport 面板 hover/focus(每帧刷新) + GizmoState m_Gizmo{}; // 视口操纵杆状态(op/拖拽中) std::unique_ptr m_Hierarchy; std::unique_ptr m_Inspector; diff --git a/App/editor/ViewportPanel.cpp b/App/editor/ViewportPanel.cpp index 1460377..997ed71 100644 --- a/App/editor/ViewportPanel.cpp +++ b/App/editor/ViewportPanel.cpp @@ -14,6 +14,7 @@ #include "ViewportPanel.h" #include +#include #include // MeshRenderer / Name / RigidBody(日志对照) #include // RequestPick / TakePickResult 门面 @@ -21,11 +22,36 @@ #include // 命中打印 #include +#include + +// GTX 扩展(实验性)需显式开启:matrix_decompose(矩阵→SRT 分解)、 +// quaternion(eulerAngles 四元数→欧拉角) +#define GLM_ENABLE_EXPERIMENTAL +#include +#include using namespace Engine; // u32 / f32(Engine/Core/Types.h) +namespace +{ + // 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; + } +} + std::optional ViewportPanel::Draw(const EditorContext &ctx, - ViewportInputState &out) const { + ViewportInputState &out, + GizmoState &gizmo, + const entt::entity selected) const { ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f)); std::optional picked; { @@ -79,9 +105,83 @@ std::optional ViewportPanel::Draw(const EditorContext &ctx, else size.y = avail.x / aspect; ImGui::Image((ImTextureID) (intptr_t) sceneTex, size); + // ---- W/E/R 切换 Gizmo 模式(UE 语义:W 平移 / E 旋转 / + // R 缩放)。仅 Viewport 持焦时生效——输入框打字(WantTextInput) + // 由 BlockingKeyboard 拦游戏侧,但 ImGui 自身仍收键,故必须 + // 限定持焦面板,避免在 Inspector 拖数值时误切 ---- + if (out.focused && !ImGui::GetIO().WantTextInput) { + 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; + } + + // ---- Gizmo 操纵杆(ImGui drawlist 叠加,无 GPU pass)---- + // 选中实体 + 有 Transform 才出杆;矩阵分解直接写回 + // TransformComponent(与 Inspector 同源,下一帧数值自动跟随) + auto ® = ctx.scene->GetRegistry(); + ECS::TransformComponent *tr = + selected != entt::null + ? reg.try_get(selected) + : nullptr; + if (tr != nullptr) { + // 相机矩阵:primary 相机 view + Vulkan proj(转 GL 形式), + // 参数与渲染侧一致(aspect = 渲染分辨率、near/far 0.1/100) + ECS::TransformComponent *camT = nullptr; + if (const auto *camComp = ctx.scene->GetPrimaryCamera(&camT); + camComp != nullptr && camT != nullptr) { + const glm::mat4 view = camComp->camera.GetView(*camT); + const glm::mat4 projGL = ProjVulkanToGL( + camComp->camera.GetProjection(aspect, 0.1f, 100.0f)); + + ImGuizmo::SetDrawlist(ImGui::GetWindowDrawList()); + // 杆画在 Image 区域内(等比适配后的 item rect) + const ImVec2 mn = ImGui::GetItemRectMin(); + ImGuizmo::SetRect(mn.x, mn.y, size.x, size.y); + + glm::mat4 m = tr->World(); + // 注:操作空间为 WORLD(UE 默认);snap / local-world + // 切换留 TODO + ImGuizmo::Manipulate(&view[0][0], &projGL[0][0], + static_cast(gizmo.op), + ImGuizmo::WORLD, &m[0][0]); + gizmo.dragging = ImGuizmo::IsUsing(); + + // 拖拽生效:分解 world 矩阵写回 SRT(Set* 标脏 → + // UpdateTransforms 重算 → 渲染下帧生效)。 + // eulerAngles 与 Compose 的 YXZ 旋转序在复合旋转下 + // 可能微差——每帧从 World 重读不累积;表现不对再换 + // ImGuizmo::DecomposeMatrixToComponents(TODO) + if (ImGuizmo::IsUsing()) { + glm::vec3 scale, transl, skew; + glm::quat rot; + glm::vec4 persp; + if (glm::decompose(m, scale, rot, transl, skew, persp)) { + const glm::vec3 euler = + degrees(eulerAngles(rot)); + tr->SetPosition(transl) + .SetRotation(euler) + .SetScale(scale); + } + } + } + } + + // Dynamic 刚体提示:物理每帧把姿态写回 Transform,拖拽即刻 + // 被覆盖(Static/Kinematic 编辑器权威,可直接拖) + if (selected != entt::null) { + if (const auto *rb = reg.try_get(selected); + rb != nullptr && + rb->type == ECS::RigidBodyComponent::BodyType::Dynamic) { + ImGui::TextDisabled( + "(Dynamic body: transform 由物理每帧覆盖)"); + } + } + // 点击拾取:单击 Image 且未命中 UI 遮挡(IsItemHovered 已含)。 - // 结果下一帧经轮询到达(本帧不改变选中) - if (ImGui::IsItemHovered() && ImGui::IsMouseClicked(0)) { + // 抓杆时不出 pick(IsOver 悬停杆/IsUsing 拖拽中)——否则按在 + // 操纵杆上的点击会穿透发拾取请求 + if (ImGui::IsItemHovered() && ImGui::IsMouseClicked(0) && + !ImGuizmo::IsOver() && !ImGuizmo::IsUsing()) { RequestPickAt(ctx, size); } } else { diff --git a/App/editor/ViewportPanel.h b/App/editor/ViewportPanel.h index 6019843..8dc59dd 100644 --- a/App/editor/ViewportPanel.h +++ b/App/editor/ViewportPanel.h @@ -25,11 +25,24 @@ struct ViewportInputState { bool focused = false; // Viewport 面板持有键盘焦点 }; +// Gizmo 交互状态(Editor 持有、ViewportPanel 写): +// op 取 ImGuizmo::OPERATION 位掩码(TRANSLATE=7=三轴位或运算,非 0 +// 起始枚举;0 = 无任何轴位 → Gizmo 完全不绘制),int 存储避免本 +// 头文件 include ImGuizmo.h(拖入 imgui 宏依赖顺序) +// W/E/R 切换(UE 语义);dragging 拉起 Editor::BlockingMouse 门控 +struct GizmoState { + int op = 7; // ImGuizmo::TRANSLATE(TRANSLATE_X|Y|Z) + bool dragging = false; // 拖拽中(IsUsing)→ 鼠标归 Gizmo 不给游戏侧 +}; + class ViewportPanel { public: // 返回值:本帧拾取命中的实体(无点击/未命中 = nullopt 或 null); // Editor 据此 SelectEntity(面板不持有选中状态) - std::optional Draw(const EditorContext& ctx, ViewportInputState& out) const; + std::optional Draw(const EditorContext& ctx, + ViewportInputState& out, + GizmoState& gizmo, + entt::entity selected) const; private: // Image 区域内的鼠标位置 → NDC → Renderer::RequestPick diff --git a/Engine/CMakeLists.txt b/Engine/CMakeLists.txt index 7d08390..7371ce6 100644 --- a/Engine/CMakeLists.txt +++ b/Engine/CMakeLists.txt @@ -17,6 +17,16 @@ target_include_directories(imgui PUBLIC # 两个 backend 头文件分别依赖 GLFW / Vulkan 头 target_link_libraries(imgui PUBLIC glfw Vulkan::Vulkan) +# ImGuizmo(视口操纵杆,vendored submodule;新版仓库源码在 src/, +# 只编译 ImGuizmo.cpp,GraphEditor/ImCurveEdit 等示例模块不用) +# IMGUI_DEFINE_MATH_OPERATORS 必须 PUBLIC:ImGuizmo.cpp 与任何 +# include 其头文件的 TU 都要在 imgui.h 之前看到该宏(ImVec2 运算符) +set(IMGUIZMO_DIR ${CMAKE_SOURCE_DIR}/Lib/ImGuizmo/src) +add_library(ImGuizmo STATIC ${IMGUIZMO_DIR}/ImGuizmo.cpp) +target_include_directories(ImGuizmo PUBLIC ${IMGUIZMO_DIR}) +target_compile_definitions(ImGuizmo PUBLIC IMGUI_DEFINE_MATH_OPERATORS) +target_link_libraries(ImGuizmo PUBLIC imgui) + # Tracy(CPU/GPU 帧分析) add_library(tracy STATIC ${CMAKE_SOURCE_DIR}/Lib/tracy/public/TracyClient.cpp) target_include_directories(tracy PUBLIC ${CMAKE_SOURCE_DIR}/Lib/tracy/public) @@ -64,6 +74,7 @@ target_link_libraries(Engine PUBLIC meshoptimizer Jolt::Jolt imgui + ImGuizmo tracy) target_compile_definitions(Engine PRIVATE VULKAN_HPP_DISPATCH_LOADER_DYNAMIC=1) diff --git a/Lib/ImGuizmo b/Lib/ImGuizmo new file mode 160000 index 0000000..18cef5e --- /dev/null +++ b/Lib/ImGuizmo @@ -0,0 +1 @@ +Subproject commit 18cef5e031d8c6973d80284c67f60549fafd78c1