From 9ff08651f02d92c9c60c147b79a72d909538a966 Mon Sep 17 00:00:00 2001 From: atdunbg Date: Sat, 3 Oct 2026 12:46:07 +0800 Subject: [PATCH] editor: revert self-drawn scale gizmo, clip degenerate axes instead User feedback: the self-drawn scale handles clash visually with the ImGuizmo translate/rotate rods (different size/style, clearly not one widget). Keep the library widget for all three ops for visual consistency and prevent mis-drags by masking degenerate axes via SetAxisMask: - translate: axis ~ parallel to camera-to-object line (sin < 0.25) - scale: axis ~ perpendicular to camera-to-object line (|dot| < 0.1, intersection numerator identically zero -> grab fails/values jump) OR axis ~ parallel to view direction (|dot| > 0.98, zero screen displacement -> value never changes) - rotate: screen-space math, no degeneracy, no mask Masked axes are neither drawn nor activatable; a slight view change restores them. Center uniform-scale square (MT_SCALE_XYZ, pure screen space) stays available in any view. --- App/editor/ViewportPanel.cpp | 372 ++++++----------------------------- App/editor/ViewportPanel.h | 16 +- 2 files changed, 66 insertions(+), 322 deletions(-) diff --git a/App/editor/ViewportPanel.cpp b/App/editor/ViewportPanel.cpp index 96ee131..3a5b2e2 100644 --- a/App/editor/ViewportPanel.cpp +++ b/App/editor/ViewportPanel.cpp @@ -25,8 +25,6 @@ #include #include -#include -#include // GTX 扩展(实验性)需显式开启:matrix_decompose(矩阵→SRT 分解, // 旋转以四元数输出,直接写回 TransformComponent) @@ -48,19 +46,13 @@ namespace // 只藏真正病态的轴,不动可用轴 constexpr float kMoveMinSin = 0.25f; - // ---- 自绘缩放 gizmo 常量(SCALE op 整体自绘,不走 ImGuizmo; - // 库内缩放数学在正对视角结构性退化,submodule 禁改)---- - constexpr float kScaleAxisLenFrac = 0.11f; // 杆长 = 视口高比例 - //(库 gizmoSize 视觉近似) - constexpr float kScaleDegenerateCos = 0.98f; // 轴与视线 |cos|>此值 = - // 指向相机(≈11.5°,与 - // 库藏杆角一致) - constexpr float kScaleAxisHitPx = 6.0f; // 杆线命中半径 px - constexpr float kScaleGrabHalfPx = 6.0f; // 端点方块半边 px - constexpr float kScaleCenterHalfPx = 9.0f; // 中心等比方块半边 px - constexpr float kScaleDragPerPx = 0.01f; // 灵敏度 1/px(库中心 - // 块同款) - constexpr float kScaleMinScale = 0.01f; // scale 下限(防 0/负) + // 缩放退化轴裁剪(防误拖动)阈值。库内缩放 hover/拖拽依赖 + // "法线=轴"平面求交,两种病态几何: + // - 轴⊥"相机→物体"连线(正对视角无横偏):交点分子 numer = + // -dist·dot(camToObj, 轴) ≡ 0 → 杆抓不到、数值随机猛跳 + // - 轴∥视线(轴指向相机):屏幕零位移 → 拖了数值不变 + constexpr float kScaleMinDot = 0.1f; // ≈84.3° + constexpr float kScaleMaxFwdDot = 0.98f; // ≈11.5°,与库藏杆角一致 // Vulkan proj → ImGuizmo 期望的 OpenGL 形式(Y-up、Z∈[-1,1]): // Y 行翻回(抵消 Vulkan clip Y 向下翻转);z 行 = 2*z - w 即 @@ -74,232 +66,6 @@ namespace } return p; } - - // ImGuizmo directionColorRGB 同款轴色(X 红 / Y 黄 / Z 青), - // hot = 悬停/拖拽中 → 向白提亮 - ImU32 ScaleAxisColor(const int i, const bool hot) - { - static constexpr float kCol[3][3] = { - {0.666f, 0.000f, 0.000f}, - {0.533f, 0.533f, 0.000f}, - {0.000f, 0.666f, 0.666f}}; - const auto ch = [&](const float c) { - const float v = hot ? c + (1.0f - c) * 0.35f : c; - return static_cast(v * 255.0f + 0.5f); - }; - return IM_COL32(ch(kCol[i][0]), ch(kCol[i][1]), ch(kCol[i][2]), 255); - } - - // ---- 自绘缩放 gizmo(SCALE op 专用,替代 ImGuizmo 缩放路径)---- - // 为什么自绘:库内缩放的 hover(GetScaleType) 与拖拽(HandleScale - // ratio 除法)依赖"法线=轴"的射线-平面求交,正对视角(轴⊥相机→ - // 物体连线)时求交分子 numer = -dist·dot(camToObj, 轴) ≡ 0 → - // 杆 hover 不到、数值随机猛变;轴∥视线时屏幕零位移,任何投影 - // 式交互都退化。库层不可改(submodule 红线),AxisMask 只能"藏" - // 不能"补"(用户要求缩放真正可用)→ 整体自绘,纯屏幕空间数学: - // - 三轴杆(线+端点方块)+ 中心等比方块,杆长恒定屏幕尺寸 - // - 普通轴:鼠标位移在轴屏幕方向上的投影 → scale 增量(0.01/px, - // 与库中心块同灵敏度) - // - 轴指向相机(|cos(axis, 视线)|>0.98):径向映射——拖离杆心 = - // 放大、拖近 = 缩小(沿视线零屏幕位移的几何必然,UE 同款处理: - // 该轴退化为径向可拖而非不可用) - // - 中心方块:等比缩放(库 MT_SCALE_XYZ 同款 x 位移数学) - // - Ctrl 吸附 = 结果值按 kSnapScale 量化 - void ScaleGizmoSelf(ImDrawList* dl, const ImVec2& org, - const ImVec2& vsize, const glm::mat4& view, - const glm::mat4& projGL, const glm::mat4& world, - ECS::TransformComponent* tr, GizmoState& gizmo) - { - const ImGuiIO& io = ImGui::GetIO(); - - // 世界 → 面板屏幕坐标(GL clip y 向上,ImGui y 向下 → 翻转; - // 与 ProjVulkanToGL 后的 projGL 配套) - const glm::mat4 vp = projGL * view; - const auto toScreen = [&vp, &org, &vsize](const glm::vec3& w, - ImVec2& out) { - const glm::vec4 c = vp * glm::vec4(w, 1.0f); - if (c.w <= 1e-5f) return false; // 相机后 - out = ImVec2(org.x + (c.x / c.w * 0.5f + 0.5f) * vsize.x, - org.y + (0.5f - c.y / c.w * 0.5f) * vsize.y); - return true; - }; - - const glm::vec3 posW(world[3]); - const glm::vec3 viewFwd = -glm::normalize( - glm::vec3(view[0][2], view[1][2], view[2][2])); - ImVec2 cScr; - if (!toScreen(posW, cScr)) { - // 杆心在相机后:收拖拽防卡死,本帧不画不响应 - gizmo.scaleDragActive = false; - gizmo.scaleDragAxis = -1; - gizmo.scaleHover = false; - return; - } - - const float axisLen = kScaleAxisLenFrac * vsize.y; - const glm::mat3 rotM(world); - - // 每轴屏幕几何:轴方向 / 端点 / 是否指向相机(退化) - struct AxisVis { - bool degenerate = false; - ImVec2 tip{0.0f, 0.0f}; // 非退化 = 杆端点;退化 = 杆心 - ImVec2 dir{0.0f, 0.0f}; // 非退化 = 杆方向(单位 px 向量) - } vis[3]; - for (int i = 0; i < 3; ++i) { - const glm::vec3 axis = gizmo.localSpace - ? glm::normalize(rotM[i]) - : glm::vec3(float(i == 0), float(i == 1), float(i == 2)); - vis[i].degenerate = - glm::abs(glm::dot(viewFwd, axis)) > kScaleDegenerateCos; - if (!vis[i].degenerate) { - ImVec2 u; - if (toScreen(posW + axis, u)) { - const float dx = u.x - cScr.x, dy = u.y - cScr.y; - const float len = std::sqrt(dx * dx + dy * dy); - if (len > 1e-4f) { - vis[i].dir = ImVec2(dx / len, dy / len); - vis[i].tip = ImVec2(cScr.x + vis[i].dir.x * axisLen, - cScr.y + vis[i].dir.y * axisLen); - } else { - vis[i].degenerate = true; // 投影意外退化兜底 - } - } else { - vis[i].degenerate = true; // 端点在相机后兜底 - } - } - if (vis[i].degenerate) vis[i].tip = cScr; - } - - // ---- 命中检测(优先级:退化轴小方块 > 轴杆 > 中心等比)---- - const float mx = io.MousePos.x, my = io.MousePos.y; - const auto inSquare = [mx, my](const ImVec2& c, const float half) { - return mx >= c.x - half && mx <= c.x + half && - my >= c.y - half && my <= c.y + half; - }; - const auto distToSeg = [mx, my](const ImVec2& a, const ImVec2& b) { - const float abx = b.x - a.x, aby = b.y - a.y; - const float t = std::clamp( - ((mx - a.x) * abx + (my - a.y) * aby) / - (abx * abx + aby * aby), 0.0f, 1.0f); - const float dx = mx - (a.x + abx * t), - dy = my - (a.y + aby * t); - return std::sqrt(dx * dx + dy * dy); - }; - - int hover = -1; // 0/1/2 轴;-2 中心等比;-1 无 - for (int i = 0; i < 3; ++i) { - if (vis[i].degenerate - ? inSquare(cScr, kScaleGrabHalfPx) - : (inSquare(vis[i].tip, kScaleGrabHalfPx + 1.0f) || - distToSeg(cScr, vis[i].tip) <= kScaleAxisHitPx)) { - hover = i; - break; - } - } - if (hover == -1 && inSquare(cScr, kScaleCenterHalfPx)) hover = -2; - - // ---- 交互:启动 / 更新 / 结束 ---- - gizmo.scaleHover = hover != -1 || gizmo.scaleDragActive; - if (ImGui::IsWindowHovered() && !gizmo.scaleDragActive && - hover != -1 && ImGui::IsMouseClicked(0)) { - gizmo.scaleDragActive = true; - gizmo.scaleDragAxis = hover; - gizmo.scaleStartMouse[0] = mx; - gizmo.scaleStartMouse[1] = my; - const glm::vec3 s = tr->GetScale(); - gizmo.scaleStartScale[0] = s.x; - gizmo.scaleStartScale[1] = s.y; - gizmo.scaleStartScale[2] = s.z; - const float ddx = mx - cScr.x, ddy = my - cScr.y; - gizmo.scaleStartDist = std::sqrt(ddx * ddx + ddy * ddy); - } - - if (gizmo.scaleDragActive) { - if (!io.MouseDown[0]) { - gizmo.scaleDragActive = false; - gizmo.scaleDragAxis = -1; - } else { - const int a = gizmo.scaleDragAxis; - float d = 0.0f; // 缩放倍率增量(乘法语义,等手感于库) - if (a == -2) { - // 中心等比:x 位移(库 MT_SCALE_XYZ 同款数学) - d = (mx - gizmo.scaleStartMouse[0]) * kScaleDragPerPx; - } else if (vis[a].degenerate) { - // 轴指向相机:径向——拖离杆心放大、拖近缩小 - const float ddx = mx - cScr.x, ddy = my - cScr.y; - d = (std::sqrt(ddx * ddx + ddy * ddy) - - gizmo.scaleStartDist) * kScaleDragPerPx; - } else { - // 鼠标位移在轴屏幕方向上的投影 - d = ((mx - gizmo.scaleStartMouse[0]) * vis[a].dir.x + - (my - gizmo.scaleStartMouse[1]) * vis[a].dir.y) * - kScaleDragPerPx; - } - const float k = 1.0f + d; - glm::vec3 s(gizmo.scaleStartScale[0], gizmo.scaleStartScale[1], - gizmo.scaleStartScale[2]); - for (int i = 0; i < 3; ++i) { - if (a == -2 || i == a) { - s[i] = glm::max(kScaleMinScale, s[i] * k); - if (io.KeyCtrl) { // 吸附 = 结果按步长量化 - s[i] = glm::max(kScaleMinScale, - std::round(s[i] / kSnapScale) * - kSnapScale); - } - } - } - tr->SetScale(s); // 标脏 → UpdateTransforms 下帧生效 - } - } - - // ---- 绘制:三轴杆 → 中心等比方块 → 退化轴小方块(压顶)---- - for (int i = 0; i < 3; ++i) { - if (vis[i].degenerate) continue; - const bool hot = - (gizmo.scaleDragActive && gizmo.scaleDragAxis == i) || - (!gizmo.scaleDragActive && hover == i); - const ImU32 col = ScaleAxisColor(i, hot); - dl->AddLine(cScr, vis[i].tip, col, 2.0f); - dl->AddRectFilled( - ImVec2(vis[i].tip.x - kScaleGrabHalfPx, - vis[i].tip.y - kScaleGrabHalfPx), - ImVec2(vis[i].tip.x + kScaleGrabHalfPx, - vis[i].tip.y + kScaleGrabHalfPx), - col, 2.0f); - } - { - const bool hot = - (gizmo.scaleDragActive && gizmo.scaleDragAxis == -2) || - (!gizmo.scaleDragActive && hover == -2); - dl->AddRectFilled( - ImVec2(cScr.x - kScaleCenterHalfPx, - cScr.y - kScaleCenterHalfPx), - ImVec2(cScr.x + kScaleCenterHalfPx, - cScr.y + kScaleCenterHalfPx), - hot ? IM_COL32(255, 255, 255, 255) - : IM_COL32(230, 230, 230, 210), - 3.0f); - } - for (int i = 0; i < 3; ++i) { - if (!vis[i].degenerate) continue; - const bool hot = - (gizmo.scaleDragActive && gizmo.scaleDragAxis == i) || - (!gizmo.scaleDragActive && hover == i); - const ImU32 col = ScaleAxisColor(i, hot); - dl->AddRectFilled( - ImVec2(cScr.x - kScaleGrabHalfPx, - cScr.y - kScaleGrabHalfPx), - ImVec2(cScr.x + kScaleGrabHalfPx, - cScr.y + kScaleGrabHalfPx), - col, 2.0f); - dl->AddRect( - ImVec2(cScr.x - kScaleGrabHalfPx, - cScr.y - kScaleGrabHalfPx), - ImVec2(cScr.x + kScaleGrabHalfPx, - cScr.y + kScaleGrabHalfPx), - IM_COL32(20, 20, 20, 200), 2.0f); - } - } } std::optional ViewportPanel::Draw(const EditorContext &ctx, @@ -379,12 +145,6 @@ std::optional ViewportPanel::Draw(const EditorContext &ctx, selected != entt::null ? reg.try_get(selected) : nullptr; - // 自绘缩放拖拽中选中丢失(清选/实体销毁):复位,防止 - // dragging 永久置位锁死鼠标 - if (gizmo.scaleDragActive && tr == nullptr) { - gizmo.scaleDragActive = false; - gizmo.scaleDragAxis = -1; - } if (tr != nullptr) { // 相机矩阵:primary 相机 view + Vulkan proj(转 GL 形式), // 参数与渲染侧一致(aspect = 渲染分辨率、near/far 0.1/100) @@ -410,84 +170,83 @@ std::optional ViewportPanel::Draw(const EditorContext &ctx, gizmo.localSpace ? ImGuizmo::LOCAL : ImGuizmo::WORLD; const glm::mat3 rotM(m); - // 退化轴隐藏(仅平移):拖拽灵敏度 = sinθ(轴与相机 - // 连线夹角),θ→0 时鼠标位移在轴上投影→0("数值不 - // 变")。旋转 = 屏幕空间角度数学无退化;缩放整体 - // 自绘(见下),两者不 mask + // 退化轴裁剪(防误拖动):被裁轴不可见、不可激活, + // 转个视角即可用。三操作病态几何不同,按 op 分派: + // - 平移:拖拽灵敏度 = sinθ(轴与相机连线夹角),θ→0 + // 时鼠标位移在轴上投影→0("数值不变") + // - 缩放:库内 hover/拖拽依赖"法线=轴"平面求交—— + // 轴⊥相机连线(正对无横偏)时交点分子 numer≡0 + // (抓不到/数值乱跳);轴∥视线时屏幕零位移(数值 + // 不变)。两判据并集裁剪 + // - 旋转:屏幕空间角度数学,无退化,不裁 const bool isTranslate = (gizmo.op & int(ImGuizmo::TRANSLATE)) == int(ImGuizmo::TRANSLATE); const bool isScale = (gizmo.op & int(ImGuizmo::SCALE)) == int(ImGuizmo::SCALE); - if (!isScale) { - // 拖拽中切 op(R→W/E):打断自绘缩放拖拽 - gizmo.scaleDragActive = false; - gizmo.scaleDragAxis = -1; + if (isTranslate || isScale) { bool maskAxis[3] = {false, false, false}; const glm::vec3 camPos(glm::inverse(view)[3]); glm::vec3 camToObj = glm::vec3(m[3]) - camPos; const float camDist = glm::length(camToObj); - if (isTranslate && camDist > 1e-4f) { + if (camDist > 1e-4f) { camToObj /= camDist; + const glm::vec3 viewFwd = -glm::normalize( + glm::vec3(view[0][2], view[1][2], + view[2][2])); for (int i = 0; i < 3; ++i) { const glm::vec3 axis = gizmo.localSpace ? glm::normalize(rotM[i]) : glm::vec3(float(i == 0), float(i == 1), float(i == 2)); - maskAxis[i] = - glm::length(glm::cross(camToObj, axis)) - < kMoveMinSin; + maskAxis[i] = isScale + ? glm::abs(glm::dot(camToObj, axis)) + < kScaleMinDot || + glm::abs(glm::dot(viewFwd, axis)) + > kScaleMaxFwdDot + : glm::length(glm::cross(camToObj, axis)) + < kMoveMinSin; } } ImGuizmo::SetAxisMask(maskAxis[0], maskAxis[1], maskAxis[2]); } - if (isScale) { - // ---- 缩放 = 自绘 gizmo(不走 ImGuizmo)。 - // 库内缩放 hover/拖拽依赖"法线=轴"平面求交, - // 正对视角(轴⊥相机连线)时分子 numer≡0:杆 - // hover 不到、数值随机猛变;轴∥视线无屏幕位 - // 移——AxisMask 只能藏不能补 → 整体自绘,屏 - // 幕空间数学任意视角良构 ---- - ScaleGizmoSelf(ImGui::GetWindowDrawList(), mn, size, - view, projGL, m, tr, gizmo); - } else { - // Ctrl 吸附(UE Grid Snap 语义)。库的约定 - //(docs/documentation.md):平移 = 世界单位 - // float[3] 逐轴;旋转 = snap[0] 按度(库内自行 - // DEG2RAD,传弧度会被解释成极小步进) - 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] = kSnapRotateDeg; // 度,非弧度 - } - } - - ImGuizmo::Manipulate(&view[0][0], &projGL[0][0], - static_cast(gizmo.op), - mode, &m[0][0], nullptr, - snapping ? snap : nullptr); - - // 拖拽生效:分解 world 矩阵写回 SRT(Set* 标脏 → - // UpdateTransforms 重算 → 渲染下帧生效)。旋转用 - // decompose 出的 quat 直写(UE FTransform 语义)—— - // 不经欧拉往返,拖拽零漂移、无万向锁 - if (ImGuizmo::IsUsing()) { - glm::vec3 scale, transl, skew; - glm::quat rot; - glm::vec4 persp; - if (glm::decompose(m, scale, rot, transl, skew, persp)) { - tr->SetPosition(transl) - .SetRotationQuat(rot) - .SetScale(scale); - } + // Ctrl 吸附(UE Grid Snap 语义)。库的约定 + //(docs/documentation.md):平移 = 世界单位 float[3] + // 逐轴;旋转 = snap[0] 按度(库内自行 DEG2RAD,传弧 + // 度会被解释成极小步进);缩放 = 只取 snap[0] + 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] = kSnapRotateDeg; // 度,非弧度 + } else if (gizmo.op == ImGuizmo::SCALE) { + snap[0] = kSnapScale; + } + } + + ImGuizmo::Manipulate(&view[0][0], &projGL[0][0], + static_cast(gizmo.op), + mode, &m[0][0], nullptr, + snapping ? snap : nullptr); + gizmo.dragging = ImGuizmo::IsUsing(); + + // 拖拽生效:分解 world 矩阵写回 SRT(Set* 标脏 → + // UpdateTransforms 重算 → 渲染下帧生效)。旋转用 + // decompose 出的 quat 直写(UE FTransform 语义)—— + // 不经欧拉往返,拖拽零漂移、无万向锁 + if (ImGuizmo::IsUsing()) { + glm::vec3 scale, transl, skew; + glm::quat rot; + glm::vec4 persp; + if (glm::decompose(m, scale, rot, transl, skew, persp)) { + tr->SetPosition(transl) + .SetRotationQuat(rot) + .SetScale(scale); } } - gizmo.dragging = ImGuizmo::IsUsing() || - gizmo.scaleDragActive; } } @@ -520,11 +279,10 @@ std::optional ViewportPanel::Draw(const EditorContext &ctx, } // 点击拾取:单击 Image 且未命中 UI 遮挡(IsItemHovered 已含)。 - // 抓杆时不出 pick(IsOver 悬停杆/IsUsing 拖拽中/自绘缩放 - // 手柄悬停或拖拽中)——否则按在操纵杆上的点击会穿透发拾取 + // 抓杆时不出 pick(IsOver 悬停杆/IsUsing 拖拽中)——否则按在 + // 操纵杆上的点击会穿透发拾取请求 if (ImGui::IsItemHovered() && ImGui::IsMouseClicked(0) && - !ImGuizmo::IsOver() && !ImGuizmo::IsUsing() && - !gizmo.scaleHover) { + !ImGuizmo::IsOver() && !ImGuizmo::IsUsing()) { RequestPickAt(ctx, size); } } else { diff --git a/App/editor/ViewportPanel.h b/App/editor/ViewportPanel.h index 50fa609..e0c65f6 100644 --- a/App/editor/ViewportPanel.h +++ b/App/editor/ViewportPanel.h @@ -35,21 +35,7 @@ struct GizmoState { int op = 7; // ImGuizmo::TRANSLATE(TRANSLATE_X|Y|Z) bool localSpace = true; // 操纵空间:true=模型轴(旋转/缩放语义), // T 键切 world - bool dragging = false; // 拖拽中(IsUsing 或自绘缩放拖拽)→ 鼠标归 - // Gizmo 不给游戏侧(Editor::BlockingMouse) - - // ---- 自绘缩放 gizmo(op==SCALE 整体不走 ImGuizmo)---- - // 库内缩放的 hover/拖拽依赖"法线=轴"平面求交:正对视角(轴⊥ - // 相机连线)时求交分子恒 0 → 杆抓不到、数值随机猛变;轴∥视线 - // 时又无屏幕位移。submodule 禁改,AxisMask 只能"藏"不能"补" - // → 调用层整体自绘三轴杆 + 中心等比方块(纯屏幕空间数学,任意 - // 视角良构),实现见 ViewportPanel.cpp ScaleGizmoSelf - bool scaleDragActive = false; // 自绘缩放拖拽中 - int scaleDragAxis = -1; // 拖拽目标:0/1/2 轴;-2 = 中心等比 - bool scaleHover = false; // 本帧悬停/拖拽自绘手柄 → 抑制 pick - float scaleStartMouse[2] = {0.0f, 0.0f}; // 拖拽起始鼠标(屏幕系) - float scaleStartDist = 0.0f; // 起始鼠标到杆心距离(径向映射用) - float scaleStartScale[3] = {1.0f, 1.0f, 1.0f}; // 拖拽起始 scale + bool dragging = false; // 拖拽中(IsUsing)→ 鼠标归 Gizmo 不给游戏侧 }; class ViewportPanel {