feat: Gizmo local/world 空间切换 + Ctrl 吸附 + Inspector 轴色手输行

- GizmoState.localSpace(默认 local,模型轴旋转直观),T 键切换
  Manipulate 的 LOCAL/WORLD 模式;无父层级两模式 world 矩阵同源,
  decompose 写回链路不变
- Ctrl 按住吸附(UE Grid Snap 语义):平移 0.25m / 旋转 15° /
  缩放 0.1x;ImGuizmo 约定旋转 snap 单位为弧度
- 视口左上角 drawlist 叠加状态提示(Space/Snap,不占布局行)
- Inspector Transform 三行升级 UE 风轴色输入行(对齐 Gizmo 轴色):
  拖拽粗调 + Ctrl+Click 键入精确值
This commit is contained in:
2026-10-03 00:56:27 +08:00
parent e5161439dd
commit 83c1600947
3 changed files with 87 additions and 11 deletions
+38 -4
View File
@@ -28,6 +28,40 @@ namespace
}
return false;
}
// UE 风变换行:label + 三轴色 DragFloat(轴色对齐 Gizmo:X 红 /
// Y 绿 / Z 蓝)。拖拽粗调;Ctrl+Click 进输入框键入精确值。
// 任一轴变化返回 true
bool DragVec3Row(const char* label, glm::vec3& v, float speed,
float mn = -3.4e38f, float mx = 3.4e38f)
{
ImGui::TextUnformatted(label);
ImGui::SameLine(72.0f); // 固定标签列宽 → 三行输入框对齐
ImGui::PushID(label);
constexpr ImVec4 axisCol[3] = {
{0.72f, 0.24f, 0.24f, 1.00f}, // X 红
{0.30f, 0.68f, 0.34f, 1.00f}, // Y 绿
{0.24f, 0.40f, 0.88f, 1.00f}, // Z 蓝
};
const float boxW =
(ImGui::GetContentRegionAvail().x
- 2.0f * ImGui::GetStyle().ItemInnerSpacing.x) / 3.0f;
bool changed = false;
for (int i = 0; i < 3; ++i)
{
ImGui::PushStyleColor(ImGuiCol_FrameBg, axisCol[i]);
ImGui::SetNextItemWidth(boxW);
changed |= ImGui::DragFloat(
i == 0 ? "##x" : i == 1 ? "##y" : "##z",
&v[i], speed, mn, mx, "%.2f");
ImGui::PopStyleColor();
if (i < 2) ImGui::SameLine();
}
ImGui::PopID();
return changed;
}
}
void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
@@ -91,17 +125,17 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
if (ImGui::CollapsingHeader("Transform", ImGuiTreeNodeFlags_DefaultOpen))
{
glm::vec3 pos = t->GetPosition();
if (ImGui::DragFloat3("position", &pos.x, 0.05f))
if (DragVec3Row("position", pos, 0.05f))
{
t->SetPosition(pos);
}
glm::vec3 rot = t->GetRotation(); // 欧拉角(度)
if (ImGui::DragFloat3("rotation", &rot.x, 0.5f))
glm::vec3 rot = t->GetRotation(); // 派生欧拉(度)
if (DragVec3Row("rotation", rot, 0.5f))
{
t->SetRotation(rot);
}
glm::vec3 scl = t->GetScale();
if (ImGui::DragFloat3("scale", &scl.x, 0.02f, 0.001f, 100.0f))
if (DragVec3Row("scale", scl, 0.02f, 0.001f, 100.0f))
{
t->SetScale(scl);
}
+45 -6
View File
@@ -24,6 +24,8 @@
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <cstdio>
// GTX 扩展(实验性)需显式开启:matrix_decompose(矩阵→SRT 分解,
// 旋转以四元数输出,直接写回 TransformComponent)
#define GLM_ENABLE_EXPERIMENTAL
@@ -33,6 +35,11 @@ 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; // 缩放步进
// 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
@@ -105,13 +112,15 @@ std::optional<entt::entity> ViewportPanel::Draw(const EditorContext &ctx,
ImGui::Image((ImTextureID) (intptr_t) sceneTex, size);
// ---- W/E/R 切换 Gizmo 模式(UE 语义:W 平移 / E 旋转 /
// R 缩放)。仅 Viewport 持焦时生效——输入框打字(WantTextInput)
// 由 BlockingKeyboard 拦游戏侧,但 ImGui 自身仍收键,故必须
// 限定持焦面板,避免在 Inspector 拖数值时误切 ----
// R 缩放),T 切换 local/world 操纵空间。仅 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;
if (ImGui::IsKeyPressed(ImGuiKey_T)) gizmo.localSpace = !gizmo.localSpace;
}
// ---- Gizmo 操纵杆(ImGui drawlist 叠加,无 GPU pass)----
@@ -138,11 +147,30 @@ std::optional<entt::entity> ViewportPanel::Draw(const EditorContext &ctx,
ImGuizmo::SetRect(mn.x, mn.y, size.x, size.y);
glm::mat4 m = tr->World();
// 注:操作空间为 WORLD(UE 默认);snap / local-world
// 切换留 TODO
// 操作空间:local(模型轴,旋转/缩放随物体朝向直观)/
// world(T 键切换)。无父层级时两种模式 world 矩阵同源,
// decompose 写回链路不变
const ImGuizmo::MODE mode =
gizmo.localSpace ? ImGuizmo::LOCAL : ImGuizmo::WORLD;
// Ctrl 按住 → 吸附(UE Grid Snap 语义)。ImGuizmo 约定:
// 平移 snap = 世界单位(逐轴)、旋转 = 弧度、缩放 = 步进
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] = glm::radians(kSnapRotateDeg);
} else if (gizmo.op == ImGuizmo::SCALE) {
snap[0] = snap[1] = snap[2] = kSnapScale;
}
}
ImGuizmo::Manipulate(&view[0][0], &projGL[0][0],
static_cast<ImGuizmo::OPERATION>(gizmo.op),
ImGuizmo::WORLD, &m[0][0]);
mode, &m[0][0], nullptr,
snapping ? snap : nullptr);
gizmo.dragging = ImGuizmo::IsUsing();
// 拖拽生效:分解 world 矩阵写回 SRT(Set* 标脏 →
@@ -162,6 +190,17 @@ std::optional<entt::entity> ViewportPanel::Draw(const EditorContext &ctx,
}
}
// Gizmo 状态提示(左上角 drawlist 叠加,原因同下:不占布局行)
{
char tip[64];
std::snprintf(tip, sizeof(tip), "Space: %s [T] Snap: Ctrl",
gizmo.localSpace ? "Local" : "World");
const ImVec2 imgMin = ImGui::GetItemRectMin();
ImGui::GetWindowDrawList()->AddText(
ImVec2(imgMin.x + 8.0f, imgMin.y + 8.0f),
IM_COL32(255, 255, 255, 150), tip);
}
// Dynamic 刚体提示:物理每帧把姿态写回 Transform,拖拽即刻
// 被覆盖(Static/Kinematic 编辑器权威,可直接拖)。
// 用 drawlist 叠加而非 ImGui::TextDisabled:后者在 Image 下方
+4 -1
View File
@@ -29,9 +29,12 @@ struct ViewportInputState {
// op 取 ImGuizmo::OPERATION 位掩码(TRANSLATE=7=三轴位或运算,非 0
// 起始枚举;0 = 无任何轴位 → Gizmo 完全不绘制),int 存储避免本
// 头文件 include ImGuizmo.h(拖入 imgui 宏依赖顺序)
// W/E/R 切换(UE 语义);dragging 拉起 Editor::BlockingMouse 门控
// 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 不给游戏侧
};