diff --git a/editor/src/Viewport/IViewportHostService.h b/editor/src/Viewport/IViewportHostService.h index cec4ead5..4150c745 100644 --- a/editor/src/Viewport/IViewportHostService.h +++ b/editor/src/Viewport/IViewportHostService.h @@ -27,12 +27,24 @@ struct EditorViewportFrame { std::string statusText; }; +struct SceneViewportInput { + ImVec2 viewportSize = ImVec2(0.0f, 0.0f); + ImVec2 mouseDelta = ImVec2(0.0f, 0.0f); + float mouseWheel = 0.0f; + bool hovered = false; + bool focused = false; + bool orbiting = false; + bool panning = false; + bool focusSelectionRequested = false; +}; + class IViewportHostService { public: virtual ~IViewportHostService() = default; virtual void BeginFrame() = 0; virtual EditorViewportFrame RequestViewport(EditorViewportKind kind, const ImVec2& requestedSize) = 0; + virtual void UpdateSceneViewInput(IEditorContext& context, const SceneViewportInput& input) = 0; virtual void RenderRequestedViewports( IEditorContext& context, const Rendering::RenderContext& renderContext) = 0; diff --git a/editor/src/Viewport/SceneViewportCameraController.h b/editor/src/Viewport/SceneViewportCameraController.h new file mode 100644 index 00000000..520d2068 --- /dev/null +++ b/editor/src/Viewport/SceneViewportCameraController.h @@ -0,0 +1,108 @@ +#pragma once + +#include +#include +#include + +#include +#include + +namespace XCEngine { +namespace Editor { + +struct SceneViewportCameraInputState { + float orbitDeltaX = 0.0f; + float orbitDeltaY = 0.0f; + float panDeltaX = 0.0f; + float panDeltaY = 0.0f; + float zoomDelta = 0.0f; + float viewportHeight = 0.0f; +}; + +class SceneViewportCameraController { +public: + void Reset() { + m_focalPoint = Math::Vector3::Zero(); + m_distance = 6.0f; + m_yawDegrees = -35.0f; + m_pitchDegrees = -20.0f; + } + + const Math::Vector3& GetFocalPoint() const { + return m_focalPoint; + } + + float GetDistance() const { + return m_distance; + } + + float GetYawDegrees() const { + return m_yawDegrees; + } + + float GetPitchDegrees() const { + return m_pitchDegrees; + } + + Math::Vector3 GetForward() const { + const float yawRadians = m_yawDegrees * Math::DEG_TO_RAD; + const float pitchRadians = m_pitchDegrees * Math::DEG_TO_RAD; + + return Math::Vector3::Normalize(Math::Vector3( + std::cos(pitchRadians) * std::sin(yawRadians), + std::sin(pitchRadians), + std::cos(pitchRadians) * std::cos(yawRadians))); + } + + Math::Vector3 GetPosition() const { + return m_focalPoint - GetForward() * m_distance; + } + + void Focus(const Math::Vector3& point) { + m_focalPoint = point; + } + + void ApplyInput(const SceneViewportCameraInputState& input) { + if (input.viewportHeight <= 0.0f) { + return; + } + + if (std::abs(input.zoomDelta) > Math::EPSILON) { + const float zoomFactor = std::pow(0.85f, input.zoomDelta); + m_distance = std::clamp(m_distance * zoomFactor, 0.5f, 500.0f); + } + + if (std::abs(input.orbitDeltaX) > Math::EPSILON || + std::abs(input.orbitDeltaY) > Math::EPSILON) { + m_yawDegrees += input.orbitDeltaX * 0.30f; + m_pitchDegrees = std::clamp(m_pitchDegrees - input.orbitDeltaY * 0.20f, -89.0f, 89.0f); + } + + if (std::abs(input.panDeltaX) > Math::EPSILON || + std::abs(input.panDeltaY) > Math::EPSILON) { + const Math::Vector3 forward = GetForward(); + const Math::Vector3 right = Math::Vector3::Normalize( + Math::Vector3::Cross(Math::Vector3::Up(), forward)); + const Math::Vector3 up = Math::Vector3::Normalize( + Math::Vector3::Cross(forward, right)); + + const float worldUnitsPerPixel = + 2.0f * m_distance * std::tan(60.0f * Math::DEG_TO_RAD * 0.5f) / input.viewportHeight; + m_focalPoint += ((right * -input.panDeltaX) + (up * input.panDeltaY)) * worldUnitsPerPixel; + } + } + + void ApplyTo(Components::TransformComponent& transform) const { + transform.SetPosition(GetPosition()); + transform.LookAt(m_focalPoint); + } + +private: + Math::Vector3 m_focalPoint = Math::Vector3::Zero(); + float m_distance = 6.0f; + float m_yawDegrees = -35.0f; + float m_pitchDegrees = -20.0f; +}; + +} // namespace Editor +} // namespace XCEngine diff --git a/editor/src/Viewport/ViewportHostService.h b/editor/src/Viewport/ViewportHostService.h index a7c4abab..dd87e600 100644 --- a/editor/src/Viewport/ViewportHostService.h +++ b/editor/src/Viewport/ViewportHostService.h @@ -2,10 +2,13 @@ #include "Core/IEditorContext.h" #include "Core/ISceneManager.h" +#include "Core/ISelectionManager.h" #include "IViewportHostService.h" +#include "SceneViewportCameraController.h" #include "UI/ImGuiBackendBridge.h" #include +#include #include #include #include @@ -38,6 +41,7 @@ public: entry = {}; } + m_sceneViewCamera = {}; m_device = nullptr; m_backend = nullptr; m_sceneRenderer.reset(); @@ -75,6 +79,33 @@ public: return frame; } + void UpdateSceneViewInput(IEditorContext& context, const SceneViewportInput& input) override { + if (!EnsureSceneViewCamera()) { + return; + } + + if (input.focusSelectionRequested) { + FocusSceneView(context); + } + + SceneViewportCameraInputState controllerInput = {}; + controllerInput.viewportHeight = input.viewportSize.y; + controllerInput.zoomDelta = input.hovered ? input.mouseWheel : 0.0f; + + if (input.orbiting) { + controllerInput.orbitDeltaX = input.mouseDelta.x; + controllerInput.orbitDeltaY = input.mouseDelta.y; + } + + if (input.panning) { + controllerInput.panDeltaX = input.mouseDelta.x; + controllerInput.panDeltaY = input.mouseDelta.y; + } + + m_sceneViewCamera.controller.ApplyInput(controllerInput); + ApplySceneViewCameraController(); + } + void RenderRequestedViewports( IEditorContext& context, const Rendering::RenderContext& renderContext) override { @@ -118,6 +149,12 @@ private: std::string statusText; }; + struct SceneViewCameraState { + std::unique_ptr gameObject; + Components::CameraComponent* camera = nullptr; + SceneViewportCameraController controller; + }; + ViewportEntry& GetEntry(EditorViewportKind kind) { const size_t index = kind == EditorViewportKind::Scene ? 0u : 1u; m_entries[index].kind = kind; @@ -130,6 +167,72 @@ private: } } + bool EnsureSceneViewCamera() { + if (m_sceneViewCamera.gameObject != nullptr && m_sceneViewCamera.camera != nullptr) { + return true; + } + + m_sceneViewCamera.gameObject = std::make_unique("EditorSceneCamera"); + m_sceneViewCamera.camera = m_sceneViewCamera.gameObject->AddComponent(); + if (m_sceneViewCamera.camera == nullptr) { + m_sceneViewCamera.gameObject.reset(); + return false; + } + + m_sceneViewCamera.camera->SetPrimary(false); + m_sceneViewCamera.camera->SetProjectionType(Components::CameraProjectionType::Perspective); + m_sceneViewCamera.camera->SetFieldOfView(60.0f); + m_sceneViewCamera.camera->SetNearClipPlane(0.03f); + m_sceneViewCamera.camera->SetFarClipPlane(2000.0f); + m_sceneViewCamera.controller.Reset(); + ApplySceneViewCameraController(); + return true; + } + + void ApplySceneViewCameraController() { + if (m_sceneViewCamera.gameObject == nullptr) { + return; + } + + m_sceneViewCamera.controller.ApplyTo(*m_sceneViewCamera.gameObject->GetTransform()); + } + + void FocusSceneView(IEditorContext& context) { + Components::GameObject* target = nullptr; + const uint64_t selectedEntity = context.GetSelectionManager().GetSelectedEntity(); + if (selectedEntity != 0) { + target = context.GetSceneManager().GetEntity(selectedEntity); + } + + if (target != nullptr) { + m_sceneViewCamera.controller.Focus(target->GetTransform()->GetPosition()); + return; + } + + const auto& roots = context.GetSceneManager().GetRootEntities(); + if (roots.empty()) { + m_sceneViewCamera.controller.Focus(Math::Vector3::Zero()); + return; + } + + Math::Vector3 center = Math::Vector3::Zero(); + uint32_t count = 0; + for (const Components::GameObject* root : roots) { + if (root == nullptr) { + continue; + } + + center += root->GetTransform()->GetPosition(); + ++count; + } + + if (count > 0) { + center /= static_cast(count); + } + + m_sceneViewCamera.controller.Focus(center); + } + bool EnsureViewportResources(ViewportEntry& entry) { if (entry.requestedWidth == 0 || entry.requestedHeight == 0) { return false; @@ -224,6 +327,15 @@ private: return true; } + Rendering::RenderSurface BuildSurface(const ViewportEntry& entry) const { + Rendering::RenderSurface surface(entry.width, entry.height); + surface.SetColorAttachment(entry.colorView); + surface.SetDepthAttachment(entry.depthView); + surface.SetColorStateBefore(entry.colorState); + surface.SetColorStateAfter(RHI::ResourceStates::PixelShaderResource); + return surface; + } + void RenderViewportEntry( ViewportEntry& entry, const Components::Scene* scene, @@ -239,6 +351,27 @@ private: return; } + Rendering::RenderSurface surface = BuildSurface(entry); + + if (entry.kind == EditorViewportKind::Scene) { + if (!EnsureSceneViewCamera()) { + entry.statusText = "Scene view camera is unavailable"; + ClearViewport(entry, renderContext, 0.18f, 0.07f, 0.07f, 1.0f); + return; + } + + ApplySceneViewCameraController(); + if (!m_sceneRenderer->Render(*scene, m_sceneViewCamera.camera, renderContext, surface)) { + entry.statusText = "Scene renderer failed"; + ClearViewport(entry, renderContext, 0.18f, 0.07f, 0.07f, 1.0f); + return; + } + + entry.colorState = RHI::ResourceStates::PixelShaderResource; + entry.statusText.clear(); + return; + } + const auto cameras = scene->FindObjectsOfType(); if (cameras.empty()) { entry.statusText = "No camera in scene"; @@ -246,12 +379,6 @@ private: return; } - Rendering::RenderSurface surface(entry.width, entry.height); - surface.SetColorAttachment(entry.colorView); - surface.SetDepthAttachment(entry.depthView); - surface.SetColorStateBefore(entry.colorState); - surface.SetColorStateAfter(RHI::ResourceStates::PixelShaderResource); - if (!m_sceneRenderer->Render(*scene, nullptr, renderContext, surface)) { entry.statusText = "Scene renderer failed"; ClearViewport(entry, renderContext, 0.18f, 0.07f, 0.07f, 1.0f); @@ -332,6 +459,7 @@ private: RHI::D3D12Device* m_device = nullptr; std::unique_ptr m_sceneRenderer; std::array m_entries = {}; + SceneViewCameraState m_sceneViewCamera; }; } // namespace Editor diff --git a/editor/src/panels/SceneViewPanel.cpp b/editor/src/panels/SceneViewPanel.cpp index cd2df8a9..b7d55ec0 100644 --- a/editor/src/panels/SceneViewPanel.cpp +++ b/editor/src/panels/SceneViewPanel.cpp @@ -1,4 +1,5 @@ #include "Actions/ActionRouting.h" +#include "Core/IEditorContext.h" #include "SceneViewPanel.h" #include "ViewportPanelContent.h" #include "UI/UI.h" @@ -15,7 +16,40 @@ void SceneViewPanel::Render() { return; } - RenderViewportPanelContent(*m_context, EditorViewportKind::Scene); + const ViewportPanelContentResult content = RenderViewportPanelContent(*m_context, EditorViewportKind::Scene); + if (IViewportHostService* viewportHostService = m_context->GetViewportHostService()) { + const ImGuiIO& io = ImGui::GetIO(); + if (!m_orbitDragging && content.hovered && ImGui::IsMouseClicked(ImGuiMouseButton_Right)) { + m_orbitDragging = true; + } + if (!m_panDragging && content.hovered && ImGui::IsMouseClicked(ImGuiMouseButton_Middle)) { + m_panDragging = true; + } + + if (m_orbitDragging && !ImGui::IsMouseDown(ImGuiMouseButton_Right)) { + m_orbitDragging = false; + } + if (m_panDragging && !ImGui::IsMouseDown(ImGuiMouseButton_Middle)) { + m_panDragging = false; + } + + SceneViewportInput input = {}; + input.viewportSize = content.availableSize; + input.hovered = content.hovered; + input.focused = content.focused; + input.mouseWheel = content.hovered ? io.MouseWheel : 0.0f; + input.orbiting = m_orbitDragging; + input.panning = m_panDragging; + input.focusSelectionRequested = + content.focused && !io.WantTextInput && ImGui::IsKeyPressed(ImGuiKey_F, false); + + if (m_orbitDragging || m_panDragging) { + input.mouseDelta = io.MouseDelta; + } + + viewportHostService->UpdateSceneViewInput(*m_context, input); + } + Actions::ObserveInactiveActionRoute(*m_context); } diff --git a/editor/src/panels/SceneViewPanel.h b/editor/src/panels/SceneViewPanel.h index 3e8976a9..0bbe16e3 100644 --- a/editor/src/panels/SceneViewPanel.h +++ b/editor/src/panels/SceneViewPanel.h @@ -9,6 +9,10 @@ class SceneViewPanel : public Panel { public: SceneViewPanel(); void Render() override; + +private: + bool m_orbitDragging = false; + bool m_panDragging = false; }; } diff --git a/editor/src/panels/ViewportPanelContent.h b/editor/src/panels/ViewportPanelContent.h index 6ec08af8..d742b159 100644 --- a/editor/src/panels/ViewportPanelContent.h +++ b/editor/src/panels/ViewportPanelContent.h @@ -10,6 +10,16 @@ namespace XCEngine { namespace Editor { +struct ViewportPanelContentResult { + EditorViewportFrame frame; + ImVec2 availableSize = ImVec2(0.0f, 0.0f); + ImVec2 itemMin = ImVec2(0.0f, 0.0f); + ImVec2 itemMax = ImVec2(0.0f, 0.0f); + bool hasViewportArea = false; + bool hovered = false; + bool focused = false; +}; + inline void DrawViewportStatusMessage(const std::string& message) { if (message.empty()) { return; @@ -30,30 +40,44 @@ inline void DrawViewportStatusMessage(const std::string& message) { drawList->AddText(textPos, ImGui::GetColorU32(ImGuiCol_TextDisabled), message.c_str()); } -inline void RenderViewportPanelContent(IEditorContext& context, EditorViewportKind kind) { +inline ViewportPanelContentResult RenderViewportPanelContent(IEditorContext& context, EditorViewportKind kind) { + ViewportPanelContentResult result = {}; + result.availableSize = ImGui::GetContentRegionAvail(); + result.focused = ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows); + IViewportHostService* viewportHostService = context.GetViewportHostService(); - const ImVec2 availableSize = ImGui::GetContentRegionAvail(); - if (availableSize.x <= 1.0f || availableSize.y <= 1.0f) { + if (result.availableSize.x <= 1.0f || result.availableSize.y <= 1.0f) { ImGui::Dummy(ImVec2(0.0f, 0.0f)); - return; + return result; } + result.hasViewportArea = true; + if (viewportHostService == nullptr) { - ImGui::Dummy(availableSize); + ImGui::Dummy(result.availableSize); + result.itemMin = ImGui::GetItemRectMin(); + result.itemMax = ImGui::GetItemRectMax(); + result.hovered = ImGui::IsMouseHoveringRect(result.itemMin, result.itemMax, true); DrawViewportStatusMessage("Viewport host is unavailable"); - return; + return result; } - const EditorViewportFrame frame = viewportHostService->RequestViewport(kind, availableSize); - if (frame.hasTexture) { - ImGui::Image(frame.textureId, availableSize); - DrawViewportStatusMessage(frame.statusText); - return; + result.frame = viewportHostService->RequestViewport(kind, result.availableSize); + if (result.frame.hasTexture) { + ImGui::Image(result.frame.textureId, result.availableSize); + } else { + ImGui::Dummy(result.availableSize); } - ImGui::Dummy(availableSize); + result.itemMin = ImGui::GetItemRectMin(); + result.itemMax = ImGui::GetItemRectMax(); + result.hovered = ImGui::IsMouseHoveringRect(result.itemMin, result.itemMax, true); + DrawViewportStatusMessage( - frame.statusText.empty() ? "Viewport is initializing" : frame.statusText); + result.frame.statusText.empty() && !result.frame.hasTexture + ? "Viewport is initializing" + : result.frame.statusText); + return result; } } // namespace Editor diff --git a/tests/editor/CMakeLists.txt b/tests/editor/CMakeLists.txt index 0cb2f55d..21471e83 100644 --- a/tests/editor/CMakeLists.txt +++ b/tests/editor/CMakeLists.txt @@ -4,6 +4,7 @@ project(XCEngine_EditorTests) set(EDITOR_TEST_SOURCES test_action_routing.cpp + test_scene_viewport_camera_controller.cpp ${CMAKE_SOURCE_DIR}/editor/src/Core/UndoManager.cpp ${CMAKE_SOURCE_DIR}/editor/src/Managers/SceneManager.cpp ${CMAKE_SOURCE_DIR}/editor/src/Managers/ProjectManager.cpp diff --git a/tests/editor/test_scene_viewport_camera_controller.cpp b/tests/editor/test_scene_viewport_camera_controller.cpp new file mode 100644 index 00000000..5e653699 --- /dev/null +++ b/tests/editor/test_scene_viewport_camera_controller.cpp @@ -0,0 +1,75 @@ +#include + +#include "Viewport/SceneViewportCameraController.h" + +#include + +#include + +namespace { + +bool NearlyEqual(float lhs, float rhs, float epsilon = 1e-4f) { + return std::abs(lhs - rhs) <= epsilon; +} + +bool NearlyEqual(const XCEngine::Math::Vector3& lhs, const XCEngine::Math::Vector3& rhs, float epsilon = 1e-4f) { + return NearlyEqual(lhs.x, rhs.x, epsilon) && + NearlyEqual(lhs.y, rhs.y, epsilon) && + NearlyEqual(lhs.z, rhs.z, epsilon); +} + +} // namespace + +using XCEngine::Components::GameObject; +using XCEngine::Editor::SceneViewportCameraController; +using XCEngine::Editor::SceneViewportCameraInputState; +using XCEngine::Math::Vector3; + +TEST(SceneViewportCameraController_Test, ApplyToMatchesComputedPositionAndForward) { + SceneViewportCameraController controller; + controller.Reset(); + controller.Focus(Vector3(2.0f, 1.0f, -3.0f)); + + GameObject cameraObject("EditorCamera"); + controller.ApplyTo(*cameraObject.GetTransform()); + + EXPECT_TRUE(NearlyEqual(cameraObject.GetTransform()->GetPosition(), controller.GetPosition())); + const Vector3 localFocus = cameraObject.GetTransform()->InverseTransformPoint(controller.GetFocalPoint()); + EXPECT_GT(localFocus.z, 0.0f); +} + +TEST(SceneViewportCameraController_Test, ApplyInputUpdatesOrbitPanAndZoomState) { + SceneViewportCameraController controller; + controller.Reset(); + + const float initialDistance = controller.GetDistance(); + const float initialYaw = controller.GetYawDegrees(); + const float initialPitch = controller.GetPitchDegrees(); + const Vector3 initialFocus = controller.GetFocalPoint(); + + SceneViewportCameraInputState input = {}; + input.viewportHeight = 720.0f; + input.zoomDelta = 1.0f; + input.orbitDeltaX = 20.0f; + input.orbitDeltaY = -15.0f; + input.panDeltaX = 16.0f; + input.panDeltaY = -10.0f; + controller.ApplyInput(input); + + EXPECT_LT(controller.GetDistance(), initialDistance); + EXPECT_NE(controller.GetYawDegrees(), initialYaw); + EXPECT_NE(controller.GetPitchDegrees(), initialPitch); + EXPECT_FALSE(NearlyEqual(controller.GetFocalPoint(), initialFocus)); +} + +TEST(SceneViewportCameraController_Test, FocusMovesPivotWithoutChangingDistance) { + SceneViewportCameraController controller; + controller.Reset(); + + const float initialDistance = controller.GetDistance(); + const Vector3 target(5.0f, -2.0f, 7.5f); + controller.Focus(target); + + EXPECT_TRUE(NearlyEqual(controller.GetFocalPoint(), target)); + EXPECT_FLOAT_EQ(controller.GetDistance(), initialDistance); +}