EditorCamera (UE FEditorViewportClient semantics) owned by SceneRenderer:
own TransformComponent + reused Camera controller. BuildFrame dispatches
render source by display mode — editor form always renders from the
editor camera (usable in empty scenes), game form renders from the
scene's primary camera entity. AlignTo copies an entity pose into the
editor camera ("Align Viewport to Camera" button in Inspector). F focus
and viewport gizmo now follow the same dispatch. main.cpp stripped of
dangling sandbox camera/spinner state; onSceneLoaded hook removed.
361 lines
18 KiB
C++
361 lines
18 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>
|
||
|
||
#include <cstdio>
|
||
|
||
// 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;
|
||
|
||
// 缩放退化轴裁剪(防误拖动)阈值。库内缩放 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 即
|
||
// 深度 [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;
|
||
}
|
||
|
||
// 视图相机分派(必须与 SceneRenderer::BuildFrame 渲染源同一规则,
|
||
// 否则 Gizmo 杆与画面错位):编辑器形态 = EditorCamera(Scene view
|
||
// 专用,与相机实体无关);游戏形态 = primary 相机实体。
|
||
// 返回 nullptr = 游戏形态且无 primary 相机(本帧无 Gizmo)
|
||
Engine::Camera* ViewCamera(const EditorContext& ctx,
|
||
ECS::TransformComponent*& outT)
|
||
{
|
||
if (ctx.renderer->GetDisplayMode() == kDisplayViewport)
|
||
{
|
||
outT = &ctx.editorCamera->GetTransform();
|
||
return &ctx.editorCamera->GetController();
|
||
}
|
||
auto* cc = ctx.scene->GetPrimaryCamera(&outT);
|
||
return cc ? &cc->camera : nullptr;
|
||
}
|
||
}
|
||
|
||
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);
|
||
|
||
// ---- 工具热键(UE 语义):W/E/R 切 Gizmo 模式、T 切
|
||
// local/world、F 焦点选中实体。仅 Viewport 持焦时生效——
|
||
// 输入框打字(WantTextInput)由 BlockingKeyboard 拦游戏侧,
|
||
// 但 ImGui 自身仍收键,故必须限定持焦面板。RMB 按住 = 相机
|
||
// 飞行模式(键盘归相机),工具热键全部抑制——W 前进与
|
||
// W 平移 Gizmo 的键位冲突就此解耦 ----
|
||
auto ® = ctx.scene->GetRegistry();
|
||
ECS::TransformComponent *tr =
|
||
selected != entt::null
|
||
? reg.try_get<ECS::TransformComponent>(selected)
|
||
: nullptr;
|
||
if (out.focused && !ImGui::GetIO().WantTextInput &&
|
||
!ImGui::IsMouseDown(ImGuiMouseButton_Right)) {
|
||
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;
|
||
if (ImGui::IsKeyPressed(ImGuiKey_F) && tr != nullptr) {
|
||
// F = 焦点选中实体(frame-selection):编辑器相机
|
||
// 环绕 pivot 设为选中实体位置,沿视线平移对准(环
|
||
// 绕距离沿用当前值,滚轮可调)
|
||
ctx.editorCamera->FocusOn(tr->GetPosition());
|
||
}
|
||
}
|
||
|
||
// ---- Gizmo 操纵杆(ImGui drawlist 叠加,无 GPU pass)----
|
||
// 选中实体 + 有 Transform 才出杆;矩阵分解直接写回
|
||
// TransformComponent(与 Inspector 同源,下一帧数值自动跟随)
|
||
if (tr != nullptr) {
|
||
// 相机矩阵:跟随渲染源(编辑器形态 = EditorCamera,游戏
|
||
// 形态 = primary 相机实体;与 SceneRenderer::BuildFrame
|
||
// 同一分派),参数与渲染侧一致(aspect = 渲染分辨率、
|
||
// near/far 0.1/100)
|
||
ECS::TransformComponent *camT = nullptr;
|
||
if (Engine::Camera *cam = ViewCamera(ctx, camT);
|
||
cam != nullptr && camT != nullptr) {
|
||
const glm::mat4 view = cam->GetView(*camT);
|
||
const glm::mat4 projGL = ProjVulkanToGL(
|
||
cam->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);
|
||
|
||
// 退化轴裁剪(防误拖动):被裁轴不可见、不可激活,
|
||
// 转个视角即可用。三操作病态几何不同,按 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 (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 (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] = 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]);
|
||
}
|
||
|
||
// 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<ImGuizmo::OPERATION>(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 状态提示(左上角 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()) {
|
||
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);
|
||
}
|
||
}
|