From b77562d28658879fe87dcfb9a6c81eb3f9283110 Mon Sep 17 00:00:00 2001 From: Max Van den Eynde Date: Tue, 21 Jul 2026 09:40:02 +0200 Subject: [PATCH] Added camera preview to the viewport --- atlas/application/window.cpp | 119 +++++++++++++++++++++++--- editor/views/editor/inspector.cpp | 24 ++++++ editor/views/editor/viewport.cpp | 31 +++++++ editor/views/editor/viewportTools.cpp | 34 +++++++- include/atlas/runtime/context.h | 4 + include/atlas/window.h | 6 ++ include/editor/views/viewport.h | 4 + include/editor/views/viewportTools.h | 2 + runtime/lib/context.cpp | 30 ++++++- 9 files changed, 238 insertions(+), 16 deletions(-) diff --git a/atlas/application/window.cpp b/atlas/application/window.cpp index 57cd7b5a..1b4ce1ea 100644 --- a/atlas/application/window.cpp +++ b/atlas/application/window.cpp @@ -2028,6 +2028,21 @@ void Window::setEditorControlsEnabled(bool enabled) { } } +void Window::setEditorSceneCamera(Camera *sceneCamera) { + editorSceneCamera = sceneCamera; + editorCameraFrustumInitialized = false; +} + +void Window::setEditorCameraFocused(bool focused) { + editorCameraFocused = focused; + editorCameraDragging = false; + editorCameraPanning = false; + editorOrbitVelocityX = 0.0f; + editorOrbitVelocityY = 0.0f; + editorZoomVelocity = 0.0f; + editorCameraKeys.fill(false); +} + void Window::setEditorSimulationEnabled(bool enabled) { editorSimulationEnabled = enabled; editorDragging = false; @@ -2260,6 +2275,8 @@ void Window::editorPointerEvent(int action, float x, float y, int button, } if (button == 3) { + if (editorCameraFocused) + return; if (action == 0) { editorCameraPanning = true; editorCameraLastX = x; @@ -2273,6 +2290,8 @@ void Window::editorPointerEvent(int action, float x, float y, int button, } if (button == 2) { + if (editorCameraFocused) + return; if (action == 0) { editorCameraDragging = true; editorCameraLastX = x; @@ -2348,7 +2367,7 @@ void Window::editorPointerEvent(int action, float x, float y, int button, } void Window::editorScrollEvent(float delta, float scale) { - if (!editorControlsEnabled || camera == nullptr) { + if (!editorControlsEnabled || camera == nullptr || editorCameraFocused) { return; } @@ -2370,6 +2389,10 @@ void Window::editorKeyEvent(int key, bool pressed) { if (key < 0 || key >= static_cast(editorCameraKeys.size())) { return; } + if (editorCameraFocused) { + editorCameraKeys.fill(false); + return; + } editorCameraKeys[static_cast(key)] = pressed; } @@ -2989,7 +3012,7 @@ void Window::applyEditorZoomDelta(float scrollAmount) { } void Window::updateEditorCameraMovement(float deltaTime) { - if (camera == nullptr) { + if (camera == nullptr || editorCameraFocused) { return; } @@ -3040,7 +3063,7 @@ void Window::updateEditorCameraMovement(float deltaTime) { } void Window::updateEditorCameraInertia(float deltaTime) { - if (camera == nullptr) { + if (camera == nullptr || editorCameraFocused) { return; } @@ -3141,6 +3164,62 @@ void Window::updateEditorControlGeometry() { ensureEditorLineObject(editorGridObject, editorGridInitialized, gridVertices); + if (editorSceneCamera != nullptr && !editorCameraFocused) { + const glm::vec3 scenePosition = editorSceneCamera->position.toGlm(); + glm::vec3 sceneForward = + editorSceneCamera->target.toGlm() - scenePosition; + if (glm::length(sceneForward) < 0.000001f) + sceneForward = editorSceneCamera->getFrontVector().toGlm(); + sceneForward = glm::normalize(sceneForward); + glm::vec3 sceneUp(0.0f, 1.0f, 0.0f); + if (std::abs(glm::dot(sceneForward, sceneUp)) > 0.98f) + sceneUp = glm::vec3(1.0f, 0.0f, 0.0f); + const glm::vec3 sceneRight = + glm::normalize(glm::cross(sceneForward, sceneUp)); + sceneUp = glm::normalize(glm::cross(sceneRight, sceneForward)); + + const float depth = std::clamp(editorSceneCamera->farClip, 1.5f, 4.0f); + float halfHeight = + editorSceneCamera->useOrthographic + ? std::max(0.1f, editorSceneCamera->orthographicSize) + : std::tan(glm::radians(editorSceneCamera->fov) * 0.5f) * depth; + halfHeight = std::clamp(halfHeight, 0.15f, 6.0f); + const float halfWidth = halfHeight * aspect; + const glm::vec3 planeCenter = scenePosition + sceneForward * depth; + const glm::vec3 bottomLeft = + planeCenter - sceneRight * halfWidth - sceneUp * halfHeight; + const glm::vec3 bottomRight = + planeCenter + sceneRight * halfWidth - sceneUp * halfHeight; + const glm::vec3 topRight = + planeCenter + sceneRight * halfWidth + sceneUp * halfHeight; + const glm::vec3 topLeft = + planeCenter - sceneRight * halfWidth + sceneUp * halfHeight; + const Color cameraColor{0.2f, 0.78f, 1.0f, 0.95f}; + const Color directionColor{1.0f, 0.72f, 0.2f, 1.0f}; + std::vector cameraVertices; + cameraVertices.reserve(30); + appendEditorLine(cameraVertices, bottomLeft, bottomRight, cameraColor); + appendEditorLine(cameraVertices, bottomRight, topRight, cameraColor); + appendEditorLine(cameraVertices, topRight, topLeft, cameraColor); + appendEditorLine(cameraVertices, topLeft, bottomLeft, cameraColor); + appendEditorLine(cameraVertices, scenePosition, bottomLeft, + cameraColor); + appendEditorLine(cameraVertices, scenePosition, bottomRight, + cameraColor); + appendEditorLine(cameraVertices, scenePosition, topRight, cameraColor); + appendEditorLine(cameraVertices, scenePosition, topLeft, cameraColor); + appendEditorLine(cameraVertices, scenePosition, + scenePosition + sceneForward * (depth * 1.18f), + directionColor); + const glm::vec3 markerCenter = + (topLeft + topRight) * 0.5f + sceneUp * (halfHeight * 0.18f); + appendEditorLine(cameraVertices, topLeft, markerCenter, directionColor); + appendEditorLine(cameraVertices, markerCenter, topRight, + directionColor); + ensureEditorLineObject(editorCameraFrustumObject, + editorCameraFrustumInitialized, cameraVertices); + } + if (selectedEditorObject == nullptr) { return; } @@ -3374,7 +3453,9 @@ void Window::renderEditorGrid( void Window::renderEditorOverlays( const std::shared_ptr &commandBuffer) { if (!editorControlsEnabled || commandBuffer == nullptr || - camera == nullptr || selectedEditorObject == nullptr) { + camera == nullptr || + (selectedEditorObject == nullptr && + (editorSceneCamera == nullptr || editorCameraFocused))) { return; } @@ -3399,17 +3480,31 @@ void Window::renderEditorOverlays( updatePipelineStateField(this->dstBlend, opal::BlendFunc::OneMinusSrcAlpha); updatePipelineStateField(this->writeDepth, false); - updatePipelineStateField(this->primitiveStyle, - opal::PrimitiveStyle::Triangles); - updatePipelineStateField(this->useDepth, true); - updatePipelineStateField(this->depthCompareOp, opal::CompareOp::Less); - renderEditorLineObject(editorOutlineObject.get(), view, projection, - commandBuffer); - if (editorControlMode != EditorControlMode::None) { + if (editorSceneCamera != nullptr && !editorCameraFocused && + editorCameraFrustumObject != nullptr) { + updatePipelineStateField(this->primitiveStyle, + opal::PrimitiveStyle::Lines); updatePipelineStateField(this->useDepth, false); updatePipelineStateField(this->depthCompareOp, opal::CompareOp::Always); - renderEditorLineObject(editorGizmoObject.get(), view, projection, + updatePipelineStateField(this->lineWidth, 2.2f); + renderEditorLineObject(editorCameraFrustumObject.get(), view, + projection, commandBuffer); + } + + if (selectedEditorObject != nullptr) { + updatePipelineStateField(this->primitiveStyle, + opal::PrimitiveStyle::Triangles); + updatePipelineStateField(this->useDepth, true); + updatePipelineStateField(this->depthCompareOp, opal::CompareOp::Less); + renderEditorLineObject(editorOutlineObject.get(), view, projection, commandBuffer); + if (editorControlMode != EditorControlMode::None) { + updatePipelineStateField(this->useDepth, false); + updatePipelineStateField(this->depthCompareOp, + opal::CompareOp::Always); + renderEditorLineObject(editorGizmoObject.get(), view, projection, + commandBuffer); + } } updatePipelineStateField(this->primitiveStyle, previousPrimitiveStyle); diff --git a/editor/views/editor/inspector.cpp b/editor/views/editor/inspector.cpp index 617c70f9..213c367c 100644 --- a/editor/views/editor/inspector.cpp +++ b/editor/views/editor/inspector.cpp @@ -1679,6 +1679,30 @@ void InspectorPanel::showCamera() { headerLayout->addWidget(identity, 1); contentLayout->addWidget(header); + auto *cameraView = new QToolButton(content); + cameraView->setObjectName("inspectorAddComponentButton"); + cameraView->setCheckable(true); + cameraView->setChecked(viewport != nullptr && viewport->isCameraFocused()); + cameraView->setIcon(styling::icon(styling::Icon::Camera, "#9E897D")); + cameraView->setText(cameraView->isChecked() ? "Camera View Active" + : "Look Through Camera"); + cameraView->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); + cameraView->setToolTip("Toggle Main Camera view · Numpad 0"); + contentLayout->addWidget(cameraView); + connect(cameraView, &QToolButton::clicked, this, [this] { + if (viewport != nullptr) + viewport->toggleCameraFocus(); + }); + if (viewport != nullptr) { + connect(viewport, &ViewportPanel::cameraFocusChanged, cameraView, + [cameraView](bool focused) { + const QSignalBlocker blocker(cameraView); + cameraView->setChecked(focused); + cameraView->setText(focused ? "Camera View Active" + : "Look Through Camera"); + }); + } + auto update = [this](const QString &path, const QJsonValue &value) { if (viewport != nullptr) { viewport->setRuntimeSceneProperty("camera", -1, path, value); diff --git a/editor/views/editor/viewport.cpp b/editor/views/editor/viewport.cpp index 5c5faf4d..67d9971a 100644 --- a/editor/views/editor/viewport.cpp +++ b/editor/views/editor/viewport.cpp @@ -501,6 +501,17 @@ void ViewportPanel::wheelEvent(QWheelEvent *event) { void ViewportPanel::keyPressEvent(QKeyEvent *event) { if (!event->isAutoRepeat() && runtimeContext != nullptr && playbackState == 0) { + if (event->key() == Qt::Key_0 && + event->modifiers().testFlag(Qt::KeypadModifier)) { + toggleCameraFocus(); + event->accept(); + return; + } + if (event->key() == Qt::Key_Escape && isCameraFocused()) { + setCameraFocused(false); + event->accept(); + return; + } if (keyboardTransformActive) { if (event->key() == Qt::Key_Escape) { finishKeyboardTransform(false); @@ -617,6 +628,7 @@ void ViewportPanel::startRuntime() { } } emit runtimeAvailabilityChanged(true); + emit cameraFocusChanged(false); playbackState = 0; emit playbackStateChanged(playbackState); frameTimer->start(16); @@ -661,6 +673,7 @@ void ViewportPanel::stopRuntime() { if (undoStack != nullptr) undoStack->clear(); emit runtimeAvailabilityChanged(false); + emit cameraFocusChanged(false); playbackState = 0; emit playbackStateChanged(playbackState); try { @@ -747,6 +760,24 @@ bool ViewportPanel::selectRuntimeObject(int id, bool focusCamera) { return true; } +bool ViewportPanel::setCameraFocused(bool focused) { + if (runtimeContext == nullptr || playbackState != 0 || + !runtimeContext->setEditorCameraFocused(focused)) { + return false; + } + emit cameraFocusChanged(focused); + setFocus(Qt::OtherFocusReason); + return true; +} + +void ViewportPanel::toggleCameraFocus() { + setCameraFocused(!isCameraFocused()); +} + +bool ViewportPanel::isCameraFocused() const { + return runtimeContext != nullptr && runtimeContext->isEditorCameraFocused(); +} + bool ViewportPanel::focusRuntimeObjects(const QList &ids) { if (runtimeContext == nullptr || ids.isEmpty()) return false; diff --git a/editor/views/editor/viewportTools.cpp b/editor/views/editor/viewportTools.cpp index 47728aea..980825a1 100644 --- a/editor/views/editor/viewportTools.cpp +++ b/editor/views/editor/viewportTools.cpp @@ -100,7 +100,19 @@ ViewportTools::ViewportTools(ViewportPanel *viewport, spaceButton->setToolTip("World transform space · Shift+T"); tools->addWidget(spaceButton); + cameraButton = new QToolButton(toolbar); + cameraButton->setObjectName("viewportOptionButton"); + cameraButton->setIcon(styling::icon(styling::Icon::Camera, "#9E897D")); + cameraButton->setCheckable(true); + cameraButton->setToolTip("Look through Main Camera · Numpad 0"); + tools->addWidget(cameraButton); + tools->addStretch(); + + cameraLabel = new QLabel("MAIN CAMERA", toolbar); + cameraLabel->setObjectName("viewportFpsLabel"); + cameraLabel->setVisible(false); + tools->addWidget(cameraLabel); tools->addWidget(playButton); tools->addWidget(pauseButton); tools->addWidget(stepButton); @@ -147,9 +159,10 @@ ViewportTools::ViewportTools(ViewportPanel *viewport, layout->addWidget(toolbar); layout->addWidget(viewport, 1); - shortcutHint = new QLabel( - "Tab Frame · Right-Drag Pan · Middle-Drag Orbit · G Move · R Rotate · S Scale · X Delete", - this); + shortcutHint = + new QLabel("Tab Frame · Num 0 Camera · Right-Drag Pan · Middle-Drag " + "Orbit · G Move · R Rotate · S Scale · X Delete", + this); shortcutHint->setObjectName("viewportShortcutHint"); shortcutHint->setTextInteractionFlags(Qt::NoTextInteraction); layout->addWidget(shortcutHint); @@ -174,6 +187,21 @@ ViewportTools::ViewportTools(ViewportPanel *viewport, }); connect(spaceButton, &QToolButton::clicked, viewport, &ViewportPanel::toggleTransformSpace); + connect(cameraButton, &QToolButton::clicked, viewport, + &ViewportPanel::toggleCameraFocus); + connect(viewport, &ViewportPanel::cameraFocusChanged, this, + [this](bool focused) { + const QSignalBlocker blocker(cameraButton); + cameraButton->setChecked(focused); + cameraButton->setIcon(styling::icon( + styling::Icon::Camera, + focused ? QColor("#5CC8FF") : QColor("#9E897D"))); + cameraButton->setToolTip( + focused + ? "Main Camera view active · Esc or Numpad 0 to exit" + : "Look through Main Camera · Numpad 0"); + cameraLabel->setVisible(focused); + }); connect(viewport, &ViewportPanel::transformSpaceChanged, this, [this](bool local) { spaceButton->setIcon(styling::icon( diff --git a/include/atlas/runtime/context.h b/include/atlas/runtime/context.h index 6b7513e8..0d3bb549 100644 --- a/include/atlas/runtime/context.h +++ b/include/atlas/runtime/context.h @@ -71,6 +71,7 @@ class Context { std::string scriptBundleModuleName = "__atlas_scripts__"; std::unique_ptr camera; + std::unique_ptr editorViewCamera; std::map> renderTargets; std::vector> directionalLights; std::vector> pointLights; @@ -78,6 +79,7 @@ class Context { std::vector> areaLights; std::vector cameraActions; bool cameraAutomaticMoving = false; + bool editorCameraFocused = false; bool editorRuntime = false; std::unique_ptr window; @@ -115,6 +117,8 @@ class Context { bool resize(int width, int height, float scale); bool setEditorControlsEnabled(bool enabled); bool setEditorSimulationEnabled(bool enabled); + bool setEditorCameraFocused(bool focused); + bool isEditorCameraFocused() const { return editorCameraFocused; } bool setEditorControlMode(int mode); bool setEditorShadingMode(int mode); float frameRate() const; diff --git a/include/atlas/window.h b/include/atlas/window.h index 474b9893..42b7fd9f 100644 --- a/include/atlas/window.h +++ b/include/atlas/window.h @@ -440,6 +440,8 @@ class Window { void resize(int width, int height, float scale = 1.0f); void setEditorControlsEnabled(bool enabled); bool areEditorControlsEnabled() const { return editorControlsEnabled; } + void setEditorSceneCamera(Camera *sceneCamera); + void setEditorCameraFocused(bool focused); void setEditorSimulationEnabled(bool enabled); bool isEditorSimulationEnabled() const { return editorSimulationEnabled; } void setEditorControlMode(EditorControlMode mode); @@ -1033,9 +1035,13 @@ class Window { std::unique_ptr editorGridObject; std::unique_ptr editorOutlineObject; std::unique_ptr editorGizmoObject; + std::unique_ptr editorCameraFrustumObject; bool editorGridInitialized = false; bool editorOutlineInitialized = false; bool editorGizmoInitialized = false; + bool editorCameraFrustumInitialized = false; + bool editorCameraFocused = false; + Camera *editorSceneCamera = nullptr; std::array editorCameraKeys{}; std::unordered_map editorObjectParents; std::unordered_map> diff --git a/include/editor/views/viewport.h b/include/editor/views/viewport.h index 0bf47aa3..1571cd81 100644 --- a/include/editor/views/viewport.h +++ b/include/editor/views/viewport.h @@ -99,6 +99,9 @@ class ViewportPanel : public QWidget { void toggleTransformSpace(); void toggleTransformSnapping(); void changeTransformSnapIncrement(float factor); + bool setCameraFocused(bool focused); + void toggleCameraFocus(); + bool isCameraFocused() const; signals: void sceneSnapshotChanged(const QString &snapshot); @@ -112,6 +115,7 @@ class ViewportPanel : public QWidget { void sceneOpened(const QString &path); void transformSpaceChanged(bool local); void transformSnappingChanged(bool enabled, float increment); + void cameraFocusChanged(bool focused); protected: QPaintEngine *paintEngine() const override; diff --git a/include/editor/views/viewportTools.h b/include/editor/views/viewportTools.h index 8215a58e..9b768d85 100644 --- a/include/editor/views/viewportTools.h +++ b/include/editor/views/viewportTools.h @@ -31,7 +31,9 @@ class ViewportTools : public QWidget { QToolButton *stopButton = nullptr; QToolButton *reloadButton = nullptr; QToolButton *spaceButton = nullptr; + QToolButton *cameraButton = nullptr; QLabel *fpsLabel = nullptr; + QLabel *cameraLabel = nullptr; QLabel *shortcutHint = nullptr; QTabBar *sceneTabs = nullptr; QString projectRoot; diff --git a/runtime/lib/context.cpp b/runtime/lib/context.cpp index eba91b01..d946bf6a 100644 --- a/runtime/lib/context.cpp +++ b/runtime/lib/context.cpp @@ -4266,6 +4266,24 @@ bool Context::setEditorSimulationEnabled(bool enabled) { throw std::runtime_error("Window is not initialized"); } window->setEditorSimulationEnabled(enabled); + if (editorRuntime) { + window->setCamera(enabled || editorCameraFocused || + editorViewCamera == nullptr + ? camera.get() + : editorViewCamera.get()); + window->setEditorCameraFocused(enabled || editorCameraFocused); + } + return true; +} + +bool Context::setEditorCameraFocused(bool focused) { + if (window == nullptr || !editorRuntime || camera == nullptr || + editorViewCamera == nullptr) { + return false; + } + editorCameraFocused = focused; + window->setCamera(focused ? camera.get() : editorViewCamera.get()); + window->setEditorCameraFocused(focused); return true; } @@ -6029,6 +6047,8 @@ void Context::loadScene(Window &window, const json &sceneData) { areaLights.clear(); cameraActions.clear(); cameraAutomaticMoving = false; + editorCameraFocused = false; + editorViewCamera.reset(); camera = std::make_unique(); window.resetInputActions(); if (context != nullptr) { @@ -6154,7 +6174,15 @@ void Context::loadScene(Window &window, const json &sceneData) { applyEditorCameraData(*this); - window.setCamera(camera.get()); + if (editorRuntime) { + editorViewCamera = std::make_unique(*camera); + window.setEditorSceneCamera(camera.get()); + window.setEditorCameraFocused(false); + window.setCamera(editorViewCamera.get()); + } else { + window.setEditorSceneCamera(nullptr); + window.setCamera(camera.get()); + } if (sceneData.contains("lights") && sceneData["lights"].is_array()) { for (const auto &lightData : sceneData["lights"]) {