- Editor::Draw 每帧首调 ImGuizmo::BeginFrame():不调则 mbOverGizmoHotspot hover 一次杆后永久置位(仅 BeginFrame 清零), GetMoveType 恒返 MT_NONE → 杆可见但永远抓不住 - Dynamic 刚体提示改 drawlist 叠加(Image 内左下角、纯英文): TextDisabled 占布局行 → 滚动条出现/消失循环 → avail 抖动 → 渲染分辨率每帧重报(视口疯狂顶动);且默认字体无中文字形
240 lines
12 KiB
C++
240 lines
12 KiB
C++
//
|
||
// 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>
|
||
|
||
// GTX 扩展(实验性)需显式开启:matrix_decompose(矩阵→SRT 分解)、
|
||
// quaternion(eulerAngles 四元数→欧拉角)
|
||
#define GLM_ENABLE_EXPERIMENTAL
|
||
#include <glm/gtx/matrix_decompose.hpp>
|
||
#include <glm/gtx/quaternion.hpp>
|
||
|
||
using namespace Engine; // u32 / f32(Engine/Core/Types.h)
|
||
|
||
namespace
|
||
{
|
||
// 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;
|
||
}
|
||
}
|
||
|
||
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 ® = 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 缩放)。仅 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;
|
||
}
|
||
|
||
// ---- Gizmo 操纵杆(ImGui drawlist 叠加,无 GPU pass)----
|
||
// 选中实体 + 有 Transform 才出杆;矩阵分解直接写回
|
||
// TransformComponent(与 Inspector 同源,下一帧数值自动跟随)
|
||
auto ® = ctx.scene->GetRegistry();
|
||
ECS::TransformComponent *tr =
|
||
selected != entt::null
|
||
? reg.try_get<ECS::TransformComponent>(selected)
|
||
: nullptr;
|
||
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);
|
||
|
||
glm::mat4 m = tr->World();
|
||
// 注:操作空间为 WORLD(UE 默认);snap / local-world
|
||
// 切换留 TODO
|
||
ImGuizmo::Manipulate(&view[0][0], &projGL[0][0],
|
||
static_cast<ImGuizmo::OPERATION>(gizmo.op),
|
||
ImGuizmo::WORLD, &m[0][0]);
|
||
gizmo.dragging = ImGuizmo::IsUsing();
|
||
|
||
// 拖拽生效:分解 world 矩阵写回 SRT(Set* 标脏 →
|
||
// UpdateTransforms 重算 → 渲染下帧生效)。
|
||
// eulerAngles 与 Compose 的 YXZ 旋转序在复合旋转下
|
||
// 可能微差——每帧从 World 重读不累积;表现不对再换
|
||
// ImGuizmo::DecomposeMatrixToComponents(TODO)
|
||
if (ImGuizmo::IsUsing()) {
|
||
glm::vec3 scale, transl, skew;
|
||
glm::quat rot;
|
||
glm::vec4 persp;
|
||
if (glm::decompose(m, scale, rot, transl, skew, persp)) {
|
||
const glm::vec3 euler =
|
||
degrees(eulerAngles(rot));
|
||
tr->SetPosition(transl)
|
||
.SetRotation(euler)
|
||
.SetScale(scale);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 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()) {
|
||
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<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);
|
||
}
|
||
}
|