// // ViewportPanel:场景渲染输出面板。 // // tonemap 输出经 ImGui::Image 显示(Editor 模式),并提供 // Editor Viewport / Fullscreen 显示模式切换。面板可用区域 // (物理像素)上报为渲染分辨率 → 画面精确填满面板。 // // 点击拾取(UE 式 GPU HitProxy):左键单击 → 鼠标 NDC 经 // Renderer::RequestPick 请求下一帧 id 渲染 + 像素拷贝;Draw 每帧 // 轮询 TakePickResult 取走结果。像素级精度,与视觉画面一致 //(视觉几何语义:与 Collider 无关,无 collider 实体同样可选中)。 // #include "ViewportPanel.h" #include #include #include // MeshRenderer / Name / RigidBody(日志对照) #include // RequestPick / TakePickResult 门面 #include // kPickMiss #include // 命中打印 #include #include #include #include #include // GTX 扩展(实验性)需显式开启:matrix_decompose(矩阵→SRT 分解, // 旋转以四元数输出,直接写回 TransformComponent) #define GLM_ENABLE_EXPERIMENTAL #include 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(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, ViewportInputState &out, GizmoState &gizmo, const entt::entity selected) const { ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f)); std::optional 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 ® = ctx.scene->GetRegistry(); const entt::entity hit = static_cast(*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(avail.x * fb.x + 0.5f); const u32 wantH = static_cast(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 ® = ctx.scene->GetRegistry(); ECS::TransformComponent *tr = 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) 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(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(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 ® = ctx.scene->GetRegistry(); const auto &name = reg.get(e).name; if (const auto *rb = reg.try_get(e)) { const auto &tr = reg.get(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); } }