// // ViewportPanel:场景渲染输出面板。 // // tonemap 输出经 ImGui::Image 显示(Editor 模式),并提供 // Editor Viewport / Fullscreen 显示模式切换。面板可用区域 // (物理像素)上报为渲染分辨率 → 画面精确填满面板。 // // 点击拾取(UE 式 GPU HitProxy):左键单击 → 鼠标 NDC 经 // Renderer::RequestPick 请求下一帧 id 渲染 + 像素拷贝;Draw 每帧 // 轮询 TakePickResult 取走结果。像素级精度,与视觉画面一致 //(视觉几何语义:与 Collider 无关,无 collider 实体同样可选中)。 // #include "ViewportPanel.h" #include #include // MeshRenderer / Name / RigidBody(日志对照) #include // RequestPick / TakePickResult 门面 #include // kPickMiss #include // 命中打印 #include using namespace Engine; // u32 / f32(Engine/Core/Types.h) std::optional ViewportPanel::Draw(const EditorContext &ctx, ViewportInputState &out) 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); // 点击拾取:单击 Image 且未命中 UI 遮挡(IsItemHovered 已含)。 // 结果下一帧经轮询到达(本帧不改变选中) if (ImGui::IsItemHovered() && ImGui::IsMouseClicked(0)) { 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); } }