Files
HelloVulkan/App/editor/ViewportPanel.cpp
T
atdunbg c9d4eed043 editor: scale gizmo fully self-drawn (screen-space, view-independent)
ImGuizmo scale hover/drag relies on ray-plane intersection with the
axis as plane normal; in front views (axis ⟂ camera-to-object) the
intersection numerator is identically zero, so handles can't be
grabbed and values jump randomly; axes parallel to the view have zero
screen displacement. Submodule is read-only and AxisMask can only hide
degenerate axes, not replace them.

Replace the library scale path entirely with a screen-space gizmo:
- three axis rods (line + end square) + center uniform-scale square,
  constant screen size in any view
- axis drag: mouse delta projected onto the axis' screen direction
- axis pointing at camera: radial mapping (drag away = grow)
- center square: uniform scale (library MT_SCALE_XYZ semantics)
- Ctrl snapping quantizes the result; scale clamped >= 0.01
- pick suppressed while hovering/dragging the self-drawn handles
2026-10-03 12:23:00 +08:00

577 lines
29 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//
// ViewportPanel:场景渲染输出面板。
//
// tonemap 输出经 ImGui::Image 显示(Editor 模式),并提供
// Editor Viewport / Fullscreen 显示模式切换。面板可用区域
// (物理像素)上报为渲染分辨率 → 画面精确填满面板。
//
// 点击拾取(UE 式 GPU HitProxy):左键单击 → 鼠标 NDC 经
// Renderer::RequestPick 请求下一帧 id 渲染 + 像素拷贝;Draw 每帧
// 轮询 TakePickResult 取走结果。像素级精度,与视觉画面一致
//(视觉几何语义:与 Collider 无关,无 collider 实体同样可选中)。
//
#include "ViewportPanel.h"
#include <imgui.h>
#include <ImGuizmo.h>
#include <Engine/ECS/Components.h> // MeshRenderer / Name / RigidBody(日志对照)
#include <Engine/Renderer/Renderer.h> // RequestPick / TakePickResult 门面
#include <Engine/Renderer/SceneTypes.h> // kPickMiss
#include <Engine/Core/Log.h> // 命中打印
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <cstdio>
#include <cmath>
#include <algorithm>
// GTX 扩展(实验性)需显式开启:matrix_decompose(矩阵→SRT 分解,
// 旋转以四元数输出,直接写回 TransformComponent)
#define GLM_ENABLE_EXPERIMENTAL
#include <glm/gtx/matrix_decompose.hpp>
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; // 缩放步进
// 平移拖拽灵敏度下限:sinθ(轴与"相机→物体"连线夹角)。低于该值
// 时拖拽投影系数 <0.25 且激活平面与鼠标射线近共面——视觉上
// "看得见、抓了没反应"。≈14.5°,略宽于库默认可见角(≈11.5°),
// 只藏真正病态的轴,不动可用轴
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/负)
// 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;
}
// 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<ImU32>(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<entt::entity> ViewportPanel::Draw(const EditorContext &ctx,
ViewportInputState &out,
GizmoState &gizmo,
const entt::entity selected) const {
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f));
std::optional<entt::entity> picked;
{
ImGui::Begin("Viewport", nullptr, ImGuiWindowFlags_NoCollapse);
// 输入焦点上报(悬停计入遮挡:模态框盖住时不误报)
out.hovered = ImGui::IsWindowHovered();
out.focused = ImGui::IsWindowFocused();
auto &renderer = *ctx.renderer;
const int mode = renderer.GetDisplayMode();
// ---- 拾取结果轮询(先于新请求:先取走上帧结果再发新请求)。
// 拷贝随上一帧命令缓冲提交,TakePickResult 内 waitIdle 保证
// 暂存可见(仅拷贝就绪帧阻塞,点击低频不可感知)----
if (auto bits = renderer.TakePickResult()) {
auto &reg = ctx.scene->GetRegistry();
const entt::entity hit = static_cast<entt::entity>(*bits);
if (*bits != kPickMiss && reg.valid(hit)) {
picked = hit;
LogPick(ctx, hit);
} else {
// 空白(kPickMiss)或悬空位模式:清选中(与旧语义一致)
picked = entt::null;
if (*bits == kPickMiss) {
ENGINE_INFO("[pick] miss (sky/empty)");
} else {
ENGINE_WARN("[pick] dangling entity bits {:#x}", *bits);
}
}
}
const void *sceneTex = renderer.GetSceneTexture();
if (sceneTex != nullptr && mode == kDisplayViewport) {
// 面板可用区域(物理像素)上报为渲染分辨率 → 画面精确填满
// 面板;下一帧帧首 BeginFrameSync 生效(一帧延迟,不可见)
const ImVec2 avail = ImGui::GetContentRegionAvail();
const ImVec2 fb = ImGui::GetIO().DisplayFramebufferScale;
const u32 wantW = static_cast<u32>(avail.x * fb.x + 0.5f);
const u32 wantH = static_cast<u32>(avail.y * fb.y + 0.5f);
if (wantW >= 16 && wantH >= 16) {
renderer.SetViewportSize(wantW, wantH);
}
// 纹理 = 渲染分辨率:等比适配面板可用区域
const vk::Extent2D ext = renderer.GetRenderExtent();
const f32 aspect =
ext.height != 0 ? f32(ext.width) / f32(ext.height) : 1.0f;
ImVec2 size = avail;
if (avail.x / avail.y > aspect) size.x = avail.y * aspect;
else size.y = avail.x / aspect;
ImGui::Image((ImTextureID) (intptr_t) sceneTex, size);
// ---- W/E/R 切换 Gizmo 模式(UE 语义:W 平移 / E 旋转 /
// 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)----
// 选中实体 + 有 Transform 才出杆;矩阵分解直接写回
// TransformComponent(与 Inspector 同源,下一帧数值自动跟随)
auto &reg = ctx.scene->GetRegistry();
ECS::TransformComponent *tr =
selected != entt::null
? reg.try_get<ECS::TransformComponent>(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)
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);
// 不调 SetPlaneLimit/SetAxisLimit——可见度保持库默认
glm::mat4 m = tr->World();
// 操作空间:local(模型轴,旋转/缩放随物体朝向直观)/
// world(T 键切换)。无父层级时两种模式 world 矩阵同源,
// decompose 写回链路不变
const ImGuizmo::MODE mode =
gizmo.localSpace ? ImGuizmo::LOCAL : ImGuizmo::WORLD;
const glm::mat3 rotM(m);
// 退化轴隐藏(仅平移):拖拽灵敏度 = sinθ(轴与相机
// 连线夹角),θ→0 时鼠标位移在轴上投影→0("数值不
// 变")。旋转 = 屏幕空间角度数学无退化;缩放整体
// 自绘(见下),两者不 mask
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;
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) {
camToObj /= camDist;
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;
}
}
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<ImGuizmo::OPERATION>(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);
}
}
}
gizmo.dragging = ImGuizmo::IsUsing() ||
gizmo.scaleDragActive;
}
}
// 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 下方
// 占一行布局 → 滚动条出现/消失循环 → avail 抖动 → 渲染分辨率
// 每帧重报(视口疯狂顶动);且默认字体无中文字形(显示 ?)
if (selected != entt::null) {
if (const auto *rb = reg.try_get<ECS::RigidBodyComponent>(selected);
rb != nullptr &&
rb->type == ECS::RigidBodyComponent::BodyType::Dynamic) {
const ImVec2 imgMin = ImGui::GetItemRectMin();
ImGui::GetWindowDrawList()->AddText(
ImVec2(imgMin.x + 8.0f, imgMin.y + size.y - 20.0f),
IM_COL32(255, 255, 255, 160),
"(Dynamic body: transform driven by physics)");
}
}
// 点击拾取:单击 Image 且未命中 UI 遮挡(IsItemHovered 已含)。
// 抓杆时不出 pick(IsOver 悬停杆/IsUsing 拖拽中/自绘缩放
// 手柄悬停或拖拽中)——否则按在操纵杆上的点击会穿透发拾取
if (ImGui::IsItemHovered() && ImGui::IsMouseClicked(0) &&
!ImGuizmo::IsOver() && !ImGuizmo::IsUsing() &&
!gizmo.scaleHover) {
RequestPickAt(ctx, size);
}
} else {
ImGui::TextDisabled("(scene texture not ready)");
}
ImGui::End();
}
ImGui::PopStyleVar();
return picked;
}
void ViewportPanel::RequestPickAt(const EditorContext &ctx,
const ImVec2 &size) const {
// 鼠标在 Image 内的位置 → NDC。Vulkan NDC y 与屏幕 v 同向
//(向下为正,与渲染同构;skybox.frag 反投影交叉验证过方向)
const ImVec2 mouse = ImGui::GetMousePos();
const ImVec2 mn = ImGui::GetItemRectMin();
const glm::vec2 ndc{
(mouse.x - mn.x) / size.x * 2.0f - 1.0f,
(mouse.y - mn.y) / size.y * 2.0f - 1.0f,
};
// UE 式 GPU HitProxy:下一帧 id 渲染 + 请求像素 1×1 拷贝
ctx.renderer->RequestPick(ndc);
}
void ViewportPanel::LogPick(const EditorContext &ctx, entt::entity e) {
// 命中详情:实体名;有物理体则附 collider 对照(诊断"物理盒 ≠
// 视觉"的错位,如 M1911 的默认 0.5 半尺寸盒)
auto &reg = ctx.scene->GetRegistry();
const auto &name = reg.get<ECS::NameComponent>(e).name;
if (const auto *rb = reg.try_get<ECS::RigidBodyComponent>(e)) {
const auto &tr = reg.get<ECS::TransformComponent>(e);
ENGINE_INFO("[pick] '{}' (hitproxy) | collider={} type={} "
"boxHalf=({:.2f},{:.2f},{:.2f}) sphereR={:.2f} "
"bodyPos=({:.2f},{:.2f},{:.2f})",
name,
rb->collider == ECS::RigidBodyComponent::ColliderType::Box
? "box" : "sphere",
rb->type == ECS::RigidBodyComponent::BodyType::Static
? "static" : "dynamic",
rb->boxHalfExtents.x, rb->boxHalfExtents.y,
rb->boxHalfExtents.z, rb->sphereRadius,
tr.GetPosition().x, tr.GetPosition().y, tr.GetPosition().z);
} else {
ENGINE_INFO("[pick] '{}' (hitproxy) | (no collider)", name);
}
}