feat: Editor Gizmo——ImGuizmo 平移/旋转/缩放操纵杆

- 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 请求
This commit is contained in:
2026-10-02 23:16:52 +08:00
parent 56bb31638e
commit 3ec96acc0f
7 changed files with 140 additions and 7 deletions
+3
View File
@@ -29,3 +29,6 @@
[submodule "Lib/JoltPhysics"] [submodule "Lib/JoltPhysics"]
path = Lib/JoltPhysics path = Lib/JoltPhysics
url = https://github.com/jrouwe/JoltPhysics.git url = https://github.com/jrouwe/JoltPhysics.git
[submodule "Lib/ImGuizmo"]
path = Lib/ImGuizmo
url = https://github.com/CedricGuillemet/ImGuizmo.git
+5 -1
View File
@@ -65,7 +65,8 @@ void Editor::Draw()
if (m_ShowInspector) m_Inspector->Draw(m_Ctx, m_Selected); if (m_ShowInspector) m_Inspector->Draw(m_Ctx, m_Selected);
if (m_ShowViewport) 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 SelectEntity(*picked); // Viewport 点击拾取 → 选中联动 Inspector/Hierarchy
} }
} }
@@ -96,6 +97,9 @@ bool Editor::BlockingKeyboard() const
bool Editor::BlockingMouse() const bool Editor::BlockingMouse() const
{ {
// Gizmo 拖拽中:鼠标归操纵杆,不给游戏侧相机(松手自动恢复)
if (m_Gizmo.dragging) return true;
// 游戏形态:沿用 ImGui 全局判定(UI 隐藏时 WantCaptureMouse=false) // 游戏形态:沿用 ImGui 全局判定(UI 隐藏时 WantCaptureMouse=false)
if (m_Ctx.renderer && if (m_Ctx.renderer &&
m_Ctx.renderer->GetDisplayMode() == kDisplayDirect) m_Ctx.renderer->GetDisplayMode() == kDisplayDirect)
+3 -2
View File
@@ -47,8 +47,8 @@ public:
// 键盘:编辑器形态 = Viewport 持焦且不在文本输入中;游戏形态 // 键盘:编辑器形态 = Viewport 持焦且不在文本输入中;游戏形态
// (Fullscreen)只在文本输入中让位(打字不触发快捷键) // (Fullscreen)只在文本输入中让位(打字不触发快捷键)
bool BlockingKeyboard() const; bool BlockingKeyboard() const;
// 鼠标:编辑器形态 = 悬停 Viewport 才放行;游戏形态沿用 // 鼠标:编辑器形态 = 悬停 Viewport 才放行;Gizmo 拖拽中一律拦截
// WantCaptureMouse(UI 隐藏时自动放行) // (抓杆时相机不能吃鼠标);游戏形态沿用 WantCaptureMouse
bool BlockingMouse() const; bool BlockingMouse() const;
// 请求退出(弹确认框;窗口关闭事件走这里) // 请求退出(弹确认框;窗口关闭事件走这里)
@@ -77,6 +77,7 @@ private:
EditorContext m_Ctx{}; EditorContext m_Ctx{};
ViewportInputState m_VpInput{}; // Viewport 面板 hover/focus(每帧刷新) ViewportInputState m_VpInput{}; // Viewport 面板 hover/focus(每帧刷新)
GizmoState m_Gizmo{}; // 视口操纵杆状态(op/拖拽中)
std::unique_ptr<HierarchyPanel> m_Hierarchy; std::unique_ptr<HierarchyPanel> m_Hierarchy;
std::unique_ptr<InspectorPanel> m_Inspector; std::unique_ptr<InspectorPanel> m_Inspector;
+103 -3
View File
@@ -14,6 +14,7 @@
#include "ViewportPanel.h" #include "ViewportPanel.h"
#include <imgui.h> #include <imgui.h>
#include <ImGuizmo.h>
#include <Engine/ECS/Components.h> // MeshRenderer / Name / RigidBody(日志对照) #include <Engine/ECS/Components.h> // MeshRenderer / Name / RigidBody(日志对照)
#include <Engine/Renderer/Renderer.h> // RequestPick / TakePickResult 门面 #include <Engine/Renderer/Renderer.h> // RequestPick / TakePickResult 门面
@@ -21,11 +22,36 @@
#include <Engine/Core/Log.h> // 命中打印 #include <Engine/Core/Log.h> // 命中打印
#include <glm/glm.hpp> #include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
// GTX 扩展(实验性)需显式开启:matrix_decompose(矩阵→SRT 分解)、
// quaternion(eulerAngles 四元数→欧拉角)
#define GLM_ENABLE_EXPERIMENTAL
#include <glm/gtx/matrix_decompose.hpp>
#include <glm/gtx/quaternion.hpp>
using namespace Engine; // u32 / f32(Engine/Core/Types.h) 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<entt::entity> ViewportPanel::Draw(const EditorContext &ctx, std::optional<entt::entity> 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)); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f));
std::optional<entt::entity> picked; std::optional<entt::entity> picked;
{ {
@@ -79,9 +105,83 @@ std::optional<entt::entity> ViewportPanel::Draw(const EditorContext &ctx,
else size.y = avail.x / aspect; else size.y = avail.x / aspect;
ImGui::Image((ImTextureID) (intptr_t) sceneTex, size); 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 &reg = ctx.scene->GetRegistry();
ECS::TransformComponent *tr =
selected != entt::null
? reg.try_get<ECS::TransformComponent>(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<ImGuizmo::OPERATION>(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<ECS::RigidBodyComponent>(selected);
rb != nullptr &&
rb->type == ECS::RigidBodyComponent::BodyType::Dynamic) {
ImGui::TextDisabled(
"(Dynamic body: transform 由物理每帧覆盖)");
}
}
// 点击拾取:单击 Image 且未命中 UI 遮挡(IsItemHovered 已含)。 // 点击拾取:单击 Image 且未命中 UI 遮挡(IsItemHovered 已含)。
// 结果下一帧经轮询到达(本帧不改变选中) // 抓杆时不出 pick(IsOver 悬停杆/IsUsing 拖拽中)——否则按在
if (ImGui::IsItemHovered() && ImGui::IsMouseClicked(0)) { // 操纵杆上的点击会穿透发拾取请求
if (ImGui::IsItemHovered() && ImGui::IsMouseClicked(0) &&
!ImGuizmo::IsOver() && !ImGuizmo::IsUsing()) {
RequestPickAt(ctx, size); RequestPickAt(ctx, size);
} }
} else { } else {
+14 -1
View File
@@ -25,11 +25,24 @@ struct ViewportInputState {
bool focused = 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 切换(UE 语义);dragging 拉起 Editor::BlockingMouse 门控
struct GizmoState {
int op = 7; // ImGuizmo::TRANSLATE(TRANSLATE_X|Y|Z)
bool dragging = false; // 拖拽中(IsUsing)→ 鼠标归 Gizmo 不给游戏侧
};
class ViewportPanel { class ViewportPanel {
public: public:
// 返回值:本帧拾取命中的实体(无点击/未命中 = nullopt 或 null); // 返回值:本帧拾取命中的实体(无点击/未命中 = nullopt 或 null);
// Editor 据此 SelectEntity(面板不持有选中状态) // Editor 据此 SelectEntity(面板不持有选中状态)
std::optional<entt::entity> Draw(const EditorContext& ctx, ViewportInputState& out) const; std::optional<entt::entity> Draw(const EditorContext& ctx,
ViewportInputState& out,
GizmoState& gizmo,
entt::entity selected) const;
private: private:
// Image 区域内的鼠标位置 → NDC → Renderer::RequestPick // Image 区域内的鼠标位置 → NDC → Renderer::RequestPick
+11
View File
@@ -17,6 +17,16 @@ target_include_directories(imgui PUBLIC
# 两个 backend 头文件分别依赖 GLFW / Vulkan 头 # 两个 backend 头文件分别依赖 GLFW / Vulkan 头
target_link_libraries(imgui PUBLIC glfw Vulkan::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 帧分析) # Tracy(CPU/GPU 帧分析)
add_library(tracy STATIC ${CMAKE_SOURCE_DIR}/Lib/tracy/public/TracyClient.cpp) add_library(tracy STATIC ${CMAKE_SOURCE_DIR}/Lib/tracy/public/TracyClient.cpp)
target_include_directories(tracy PUBLIC ${CMAKE_SOURCE_DIR}/Lib/tracy/public) target_include_directories(tracy PUBLIC ${CMAKE_SOURCE_DIR}/Lib/tracy/public)
@@ -64,6 +74,7 @@ target_link_libraries(Engine PUBLIC
meshoptimizer meshoptimizer
Jolt::Jolt Jolt::Jolt
imgui imgui
ImGuizmo
tracy) tracy)
target_compile_definitions(Engine PRIVATE VULKAN_HPP_DISPATCH_LOADER_DYNAMIC=1) target_compile_definitions(Engine PRIVATE VULKAN_HPP_DISPATCH_LOADER_DYNAMIC=1)
Submodule
+1
Submodule Lib/ImGuizmo added at 18cef5e031