feat: add scene view editor camera controls
This commit is contained in:
@@ -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;
|
||||
|
||||
108
editor/src/Viewport/SceneViewportCameraController.h
Normal file
108
editor/src/Viewport/SceneViewportCameraController.h
Normal file
@@ -0,0 +1,108 @@
|
||||
#pragma once
|
||||
|
||||
#include <XCEngine/Components/TransformComponent.h>
|
||||
#include <XCEngine/Core/Math/Math.h>
|
||||
#include <XCEngine/Core/Math/Vector3.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
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
|
||||
@@ -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 <XCEngine/Components/CameraComponent.h>
|
||||
#include <XCEngine/Components/GameObject.h>
|
||||
#include <XCEngine/RHI/D3D12/D3D12Device.h>
|
||||
#include <XCEngine/RHI/D3D12/D3D12Texture.h>
|
||||
#include <XCEngine/RHI/RHIEnums.h>
|
||||
@@ -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<Components::GameObject> 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<Components::GameObject>("EditorSceneCamera");
|
||||
m_sceneViewCamera.camera = m_sceneViewCamera.gameObject->AddComponent<Components::CameraComponent>();
|
||||
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<float>(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<Components::CameraComponent>();
|
||||
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<Rendering::SceneRenderer> m_sceneRenderer;
|
||||
std::array<ViewportEntry, 2> m_entries = {};
|
||||
SceneViewCameraState m_sceneViewCamera;
|
||||
};
|
||||
|
||||
} // namespace Editor
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,10 @@ class SceneViewPanel : public Panel {
|
||||
public:
|
||||
SceneViewPanel();
|
||||
void Render() override;
|
||||
|
||||
private:
|
||||
bool m_orbitDragging = false;
|
||||
bool m_panDragging = false;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user