feat: add scene view editor camera controls

This commit is contained in:
2026-03-28 17:40:14 +08:00
parent 3c45a051a2
commit 3cc823aebd
8 changed files with 406 additions and 20 deletions

View File

@@ -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;

View 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

View File

@@ -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

View File

@@ -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);
}

View File

@@ -9,6 +9,10 @@ class SceneViewPanel : public Panel {
public:
SceneViewPanel();
void Render() override;
private:
bool m_orbitDragging = false;
bool m_panDragging = false;
};
}

View File

@@ -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