diff --git a/App/editor/ViewportPanel.cpp b/App/editor/ViewportPanel.cpp index 3a5b2e2..009d624 100644 --- a/App/editor/ViewportPanel.cpp +++ b/App/editor/ViewportPanel.cpp @@ -125,26 +125,38 @@ std::optional ViewportPanel::Draw(const EditorContext &ctx, 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 同源,下一帧数值自动跟随) + // ---- 工具热键(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(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 设 + // 为选中实体位置,相机沿视线平移对准(环绕距离沿用 + // 当前值,滚轮可调) + ECS::TransformComponent *camT = nullptr; + if (auto *camComp = ctx.scene->GetPrimaryCamera(&camT); + camComp != nullptr && camT != nullptr) { + camComp->camera.FocusOn(*camT, tr->GetPosition()); + } + } + } + + // ---- Gizmo 操纵杆(ImGui drawlist 叠加,无 GPU pass)---- + // 选中实体 + 有 Transform 才出杆;矩阵分解直接写回 + // TransformComponent(与 Inspector 同源,下一帧数值自动跟随) if (tr != nullptr) { // 相机矩阵:primary 相机 view + Vulkan proj(转 GL 形式), // 参数与渲染侧一致(aspect = 渲染分辨率、near/far 0.1/100) diff --git a/App/main.cpp b/App/main.cpp index 3414394..d663896 100644 --- a/App/main.cpp +++ b/App/main.cpp @@ -201,12 +201,15 @@ namespace void OnUpdate(const FrameTime& frameTime) override { - // 相机(WASD/Space/Shift 移动;位姿读写实体 Transform)。 - // 编辑器相机走真实轨:暂停(timeScale=0)时仍可飞行观察 + // 相机(RMB 飞行 / MMB 焦点环绕 / 滚轮推拉;位姿读写实体 + // Transform)。编辑器相机走真实轨:暂停(timeScale=0)时仍 + // 可飞行观察。编辑器模式移动键需 RMB 按住(UE 语义:飞行时 + // 键盘归相机,W/E/R 归 Gizmo) m_Camera.GetComponent() .camera.Update(GetWindow(), m_Camera.GetComponent(), - frameTime.UnscaledDt()); + frameTime.UnscaledDt(), + GetRenderer().GetDisplayMode() == kDisplayViewport); // 旋转动画:编辑 Transform 分量后 Compose 重算 world。 // 动画时钟 = Time::Elapsed()(scaled 累计,暂停自动冻结—— diff --git a/Engine/include/Engine/Core/Window.h b/Engine/include/Engine/Core/Window.h index b02ef94..5a83722 100644 --- a/Engine/include/Engine/Core/Window.h +++ b/Engine/include/Engine/Core/Window.h @@ -67,6 +67,10 @@ namespace Engine { bool IsMouseButtonPressed(int button) const; void GetMouseDelta(float& dx, float& dy) const; + // 轮询式滚轮增量(上行 = +1,一档 ≈1):读后清零。UI 捕获鼠标 + //(面板悬停)时不累积——滚轮归 ImGui + float ConsumeScrollDelta(); + // 光标捕获(自由视角):DISABLED 隐藏光标出无界增量,NORMAL 恢复。 // 重复设置同值无操作;切换后鼠标增量参考点重置(防跳变) void SetCursorCaptured(bool captured); @@ -92,6 +96,7 @@ namespace Engine { mutable bool m_FirstMouse = true; mutable double m_LastMouseX = 0.0; mutable double m_LastMouseY = 0.0; + mutable float m_ScrollAccum = 0.0f; // 滚轮累积(ConsumeScrollDelta) // GLFW 静态回调(通过 window user pointer 找回 this) static void FramebufferResizeCallback(GLFWwindow* window, int width, int height); diff --git a/Engine/include/Engine/Renderer/Camera.h b/Engine/include/Engine/Renderer/Camera.h index 4ba71a7..a63da00 100644 --- a/Engine/include/Engine/Renderer/Camera.h +++ b/Engine/include/Engine/Renderer/Camera.h @@ -4,9 +4,18 @@ // Camera:独立的相机模块。 // // 设计(Transform 单一事实源):相机的位置与朝向完全由实体上的 -// TransformComponent 承载——Update 把输入直接写回 transform(末尾 -// Compose),视图矩阵 = 实体世界矩阵的逆(本地 -Z 为视线方向)。 -// 相机类自身不保存任何位姿状态,杜绝"位置两套来源"的脱节。 +// TransformComponent 承载——Update 把输入直接写回 transform,视图 +// 矩阵 = 实体世界矩阵的逆(本地 -Z 为视线方向)。相机类自身只保存 +// 焦点环绕(pivot orbit)状态(pivot/距离),杜绝位姿两套来源。 +// +// 交互(UE Scene view 语义): +// RMB 按住 自由视角:鼠标转向 + WASD/Space/Shift 飞行 +// (编辑器模式下 RMB 按住期间键盘归相机,W/E/R 不切 +// Gizmo——键位冲突的解法与 UE 一致) +// MMB 拖拽 焦点环绕:绕 pivot 转向,距离不变 +// Shift+MMB 屏幕平面平移(相机与 pivot 同步移动) +// 滚轮 推拉:改环绕距离(有 pivot 时指数步进) +// F(面板侧) 焦点选中实体(ViewportPanel 调 FocusOn) // #ifndef HELLOVULKAN_CAMERA_H @@ -22,9 +31,15 @@ namespace Engine { class Camera { public: - // 输入驱动:WASD/Space/Shift 移动 + 按住右键鼠标视角。 - // 全部写回传入的 transform,有变更才 Compose。 - void Update(Window& window, ECS::TransformComponent& transform, f32 deltaTime) const; + // 输入驱动。flyRequiresRMB=true(编辑器模式):移动键仅在 RMB + // 按住时生效——解决与 Gizmo W/E/R 工具热键的键位冲突(UE 语义: + // 飞行时键盘归相机)。全部写回传入的 transform + void Update(Window& window, ECS::TransformComponent& transform, + f32 deltaTime, bool flyRequiresRMB = false); + + // 焦点选中(UE frame-selection):pivot 设为 point,相机沿当前 + // 视线平移对准;环绕距离沿用当前值(初始 5m,滚轮可改) + void FocusOn(ECS::TransformComponent& transform, const glm::vec3& point); // 相机视图 = 实体世界矩阵的逆(要求 transform 已 Compose) glm::mat4 GetView(const ECS::TransformComponent& transform) const; @@ -35,8 +50,7 @@ namespace Engine { void SetMoveSpeed(f32 speed) { m_MoveSpeed = speed; } void SetMouseSensitivity(f32 sens) { m_MouseSensitivity = sens; } - // 鼠标视角总开关(默认开)。交互 = 按住右键自由视角(光标捕获), - // 松开释放光标;右键落在 ImGui 面板上时归 UI 不触发相机 + // 相机输入总开关(默认开)。关闭后转向/环绕/推拉/飞行全部静止 void SetMouseLookEnabled(const bool enabled) { m_MouseLookEnabled = enabled; } bool IsMouseLookEnabled() const { return m_MouseLookEnabled; } @@ -44,6 +58,12 @@ namespace Engine { f32 m_MoveSpeed = 2.5f; f32 m_MouseSensitivity = 0.1f; bool m_MouseLookEnabled = true; + + // 焦点环绕状态:pivot + 距离。首次环绕无 pivot 时自动取当前 + // 视线上 m_OrbitDistance 处;FocusOn 显式设置 + glm::vec3 m_OrbitPivot{0.0f}; + f32 m_OrbitDistance = 5.0f; + bool m_HasPivot = false; }; } // namespace Engine diff --git a/Engine/src/Core/Window.cpp b/Engine/src/Core/Window.cpp index 66007a6..a1c09c9 100644 --- a/Engine/src/Core/Window.cpp +++ b/Engine/src/Core/Window.cpp @@ -72,6 +72,8 @@ namespace Engine if (!self->m_EventCallback) return; if (self->m_MouseCapture && self->m_MouseCapture()) return; + self->m_ScrollAccum += static_cast(yoffset); // 轮询消费通道 + Event e; e.type = EventType::MouseScroll; e.deltaX = static_cast(xoffset); @@ -245,4 +247,11 @@ namespace Engine dx = dy = 0.0f; } } + + float Window::ConsumeScrollDelta() + { + const float d = m_ScrollAccum; + m_ScrollAccum = 0.0f; + return d; + } } // namespace Engine diff --git a/Engine/src/Renderer/Camera.cpp b/Engine/src/Renderer/Camera.cpp index a7b35bd..464df56 100644 --- a/Engine/src/Renderer/Camera.cpp +++ b/Engine/src/Renderer/Camera.cpp @@ -8,16 +8,38 @@ #include +#include + #include "GLFW/glfw3.h" namespace Engine { - void Camera::Update(Window& window, ECS::TransformComponent& transform, const f32 deltaTime) const { - // 鼠标视角(编辑器习惯):按住右键捕获光标进入自由视角, - // 松开释放光标(ImGui 面板可正常交互)。m_MouseLookEnabled 总开关 + namespace { + // 欧拉(度)→ 旋转矩阵:应用序 Y→X,与 TransformComponent:: + // Compose 一致(roll 不参与 FPS/编辑器相机) + glm::mat3 EulerRotMat(const glm::vec3& rotDeg) + { + return static_cast( + glm::rotate(glm::mat4(1.0f), glm::radians(rotDeg.y), + glm::vec3(0, 1, 0)) * + glm::rotate(glm::mat4(1.0f), glm::radians(rotDeg.x), + glm::vec3(1, 0, 0))); + } + } + + void Camera::Update(Window& window, ECS::TransformComponent& transform, + const f32 deltaTime, const bool flyRequiresRMB) + { + // ---- 输入模式(UE Scene view 语义):RMB=自由视角(鼠标转向 + // + 飞行),MMB=焦点环绕(Shift+MMB=平移),滚轮=推拉。三种 + // 光标型交互互斥共用捕获;编辑器模式 flyRequiresRMB=true 时 + // 移动键仅 RMB 按住生效(否则与 Gizmo W/E/R 热键冲突)---- const bool wantLook = m_MouseLookEnabled && window.IsMouseButtonPressed(GLFW_MOUSE_BUTTON_RIGHT); - if (wantLook) + const bool wantOrbit = m_MouseLookEnabled && + window.IsMouseButtonPressed(GLFW_MOUSE_BUTTON_MIDDLE); + + if (wantLook || wantOrbit) { if (!window.IsCursorCaptured()) { @@ -37,6 +59,42 @@ namespace Engine { rot.x = glm::clamp( rot.x - mouseDY * m_MouseSensitivity, -89.0f, 89.0f); transform.SetRotation(rot); + + if (wantOrbit) + { + if (window.IsKeyPressed(GLFW_KEY_LEFT_SHIFT)) + { + // ---- 屏幕平面平移:内容跟手(拖右 → 相机左移、 + // 拖下 → 相机上移),pivot 同步平移保持环绕点的 + // 屏幕相对位置 ---- + const glm::mat3 r = EulerRotMat(rot); + const glm::vec3 delta = + (-r * glm::vec3(1, 0, 0)) * mouseDX + + (r * glm::vec3(0, 1, 0)) * mouseDY; + const f32 k = m_OrbitDistance * 0.002f; // m/px + transform.SetPosition(transform.GetPosition() + delta * k); + if (m_HasPivot) m_OrbitPivot += delta * k; + } + else + { + // ---- 焦点环绕:转向后保持到 pivot 的距离。首次 + // 环绕无 pivot:取当前视线上 m_OrbitDistance 处 + if (!m_HasPivot) + { + m_OrbitPivot = transform.GetPosition() + + EulerRotMat(transform.GetRotation()) * + glm::vec3(0, 0, -1) * + m_OrbitDistance; + m_HasPivot = true; + } + m_OrbitDistance = glm::length( + transform.GetPosition() - m_OrbitPivot); + const glm::vec3 orbitFwd = + EulerRotMat(rot) * glm::vec3(0, 0, -1); + transform.SetPosition(m_OrbitPivot - + orbitFwd * m_OrbitDistance); + } + } } } else if (window.IsCursorCaptured()) @@ -44,24 +102,50 @@ namespace Engine { window.SetCursorCaptured(false); } - // 基向量由实体旋转决定(本地 -Z 为视线方向;应用顺序 Y→X, - // 与 TransformComponent::Compose 一致;roll 不参与 FPS 相机) - const glm::vec3 rot = transform.GetRotation(); // 派生欧拉(度) - const glm::mat3 r = static_cast( - glm::rotate(glm::mat4(1.0f), glm::radians(rot.y), - glm::vec3(0, 1, 0)) * - glm::rotate(glm::mat4(1.0f), glm::radians(rot.x), - glm::vec3(1, 0, 0))); + // 基向量由(可能已更新的)实体旋转决定(本地 -Z 为视线方向) + const glm::mat3 r = EulerRotMat(transform.GetRotation()); const glm::vec3 forward = r * glm::vec3(0, 0, -1); const glm::vec3 right = r * glm::vec3(1, 0, 0); - const f32 speed = m_MoveSpeed * deltaTime; - if (window.IsKeyPressed(GLFW_KEY_W)) { transform.SetPosition(transform.GetPosition() + forward * speed); } - if (window.IsKeyPressed(GLFW_KEY_S)) { transform.SetPosition(transform.GetPosition() - forward * speed); } - if (window.IsKeyPressed(GLFW_KEY_A)) { transform.SetPosition(transform.GetPosition() - right * speed); } - if (window.IsKeyPressed(GLFW_KEY_D)) { transform.SetPosition(transform.GetPosition() + right * speed); } - if (window.IsKeyPressed(GLFW_KEY_SPACE)) { transform.SetPosition(transform.GetPosition() + glm::vec3(0, speed, 0)); } - if (window.IsKeyPressed(GLFW_KEY_LEFT_SHIFT)) { transform.SetPosition(transform.GetPosition() - glm::vec3(0, speed, 0)); } + // ---- 飞行移动:编辑器模式仅 RMB(wantLook)时生效;游戏模式 + // 常开(原行为)---- + if (wantLook || !flyRequiresRMB) + { + const f32 speed = m_MoveSpeed * deltaTime; + if (window.IsKeyPressed(GLFW_KEY_W)) { transform.SetPosition(transform.GetPosition() + forward * speed); } + if (window.IsKeyPressed(GLFW_KEY_S)) { transform.SetPosition(transform.GetPosition() - forward * speed); } + if (window.IsKeyPressed(GLFW_KEY_A)) { transform.SetPosition(transform.GetPosition() - right * speed); } + if (window.IsKeyPressed(GLFW_KEY_D)) { transform.SetPosition(transform.GetPosition() + right * speed); } + if (window.IsKeyPressed(GLFW_KEY_SPACE)) { transform.SetPosition(transform.GetPosition() + glm::vec3(0, speed, 0)); } + if (window.IsKeyPressed(GLFW_KEY_LEFT_SHIFT)) { transform.SetPosition(transform.GetPosition() - glm::vec3(0, speed, 0)); } + } + + // ---- 滚轮推拉:有 pivot 时改环绕距离(指数步进,近处精细/ + // 远处大步);无 pivot 时沿视线平移 ---- + const f32 wheel = window.ConsumeScrollDelta(); + if (m_MouseLookEnabled && wheel != 0.0f) + { + if (m_HasPivot) + { + m_OrbitDistance = glm::clamp( + m_OrbitDistance * std::pow(0.9f, wheel), 0.5f, 500.0f); + transform.SetPosition(m_OrbitPivot - forward * m_OrbitDistance); + } + else + { + transform.SetPosition(transform.GetPosition() + + forward * wheel * m_MoveSpeed * deltaTime); + } + } + } + + void Camera::FocusOn(ECS::TransformComponent& transform, const glm::vec3& point) + { + const glm::vec3 fwd = + EulerRotMat(transform.GetRotation()) * glm::vec3(0, 0, -1); + m_OrbitPivot = point; + m_HasPivot = true; + transform.SetPosition(point - fwd * m_OrbitDistance); } glm::mat4 Camera::GetView(const ECS::TransformComponent& t) const