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
+103 -3
View File
@@ -14,6 +14,7 @@
#include "ViewportPanel.h"
#include <imgui.h>
#include <ImGuizmo.h>
#include <Engine/ECS/Components.h> // MeshRenderer / Name / RigidBody(日志对照)
#include <Engine/Renderer/Renderer.h> // RequestPick / TakePickResult 门面
@@ -21,11 +22,36 @@
#include <Engine/Core/Log.h> // 命中打印
#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)
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,
ViewportInputState &out) const {
ViewportInputState &out,
GizmoState &gizmo,
const entt::entity selected) const {
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f));
std::optional<entt::entity> picked;
{
@@ -79,9 +105,83 @@ std::optional<entt::entity> 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 &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 已含)。
// 结果下一帧经轮询到达(本帧不改变选中)
if (ImGui::IsItemHovered() && ImGui::IsMouseClicked(0)) {
// 抓杆时不出 pick(IsOver 悬停杆/IsUsing 拖拽中)——否则按在
// 操纵杆上的点击会穿透发拾取请求
if (ImGui::IsItemHovered() && ImGui::IsMouseClicked(0) &&
!ImGuizmo::IsOver() && !ImGuizmo::IsUsing()) {
RequestPickAt(ctx, size);
}
} else {