diff --git a/CMakeLists.txt b/CMakeLists.txt index d361e601..3894934f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -143,9 +143,10 @@ set(FT_DISABLE_HARFBUZZ ON CACHE BOOL "" FORCE) set(FT_DISABLE_BROTLI ON CACHE BOOL "" FORCE) FetchContent_Declare( - freetype - URL https://download.savannah.gnu.org/releases/freetype/freetype-2.13.3.tar.xz - DOWNLOAD_EXTRACT_TIMESTAMP FALSE + freetype + GIT_REPOSITORY https://github.com/freetype/freetype.git + GIT_TAG VER-2-13-3 + GIT_SHALLOW TRUE ) FetchContent_GetProperties(freetype) diff --git a/atlas/application/window.cpp b/atlas/application/window.cpp index c2296b50..50029d76 100644 --- a/atlas/application/window.cpp +++ b/atlas/application/window.cpp @@ -299,6 +299,31 @@ glm::vec3 editorScaleAxisVector(GameObject *object, int axis) { return glm::normalize(object->getRotation().toGlmQuat() * localAxis); } +int editorScaleComponent(GameObject *object, int axis, bool localSpace) { + if (object == nullptr || localSpace) + return axis; + const glm::vec3 worldAxis = editorAxisVector(axis); + int component = axis; + float bestAlignment = -1.0f; + for (int localAxis = 1; localAxis <= 3; ++localAxis) { + const float alignment = std::abs( + glm::dot(editorScaleAxisVector(object, localAxis), worldAxis)); + if (alignment > bestAlignment) { + bestAlignment = alignment; + component = localAxis; + } + } + return component; +} + +float clampedEditorScale(float value) { + if (!std::isfinite(value)) + return 1.0f; + if (std::abs(value) >= 0.001f) + return value; + return std::signbit(value) ? -0.001f : 0.001f; +} + void ensureEditorLineObject(std::unique_ptr &object, bool &initialized, const std::vector &vertices) { @@ -2033,10 +2058,10 @@ void Window::resize(int width, int height, float scale) { device->getDefaultFramebuffer()->setViewport(0, 0, pixelWidth, pixelHeight); setViewportState(0, 0, pixelWidth, pixelHeight); - const int targetWidth = std::max( - 1, static_cast(pixelWidth * this->getRenderScale())); - const int targetHeight = std::max( - 1, static_cast(pixelHeight * this->getRenderScale())); + const int targetWidth = + std::max(1, static_cast(pixelWidth * this->getRenderScale())); + const int targetHeight = + std::max(1, static_cast(pixelHeight * this->getRenderScale())); for (RenderTarget *target : renderTargets) { if (target != nullptr && (target->type == RenderTargetType::Scene || @@ -2047,9 +2072,8 @@ void Window::resize(int width, int height, float scale) { } } const std::array *, 7> internalTargets = { - &gBuffer, &ssaoBuffer, &ssaoBlurBuffer, - &volumetricBuffer, &lightBuffer, &ssrFramebuffer, - &ssrHistoryFramebuffer}; + &gBuffer, &ssaoBuffer, &ssaoBlurBuffer, &volumetricBuffer, + &lightBuffer, &ssrFramebuffer, &ssrHistoryFramebuffer}; for (auto *target : internalTargets) { if (target != nullptr && *target != nullptr) { (*target)->resize(*this); @@ -2438,12 +2462,7 @@ void Window::editorScrollEvent(float delta, float scale) { } applyEditorZoomDelta(scrollAmount); - if (usePathTracing) { - editorZoomVelocity = 0.0f; - } else { - editorZoomVelocity += scrollAmount * 0.01f; - editorZoomVelocity = std::clamp(editorZoomVelocity, -80.0f, 80.0f); - } + editorZoomVelocity = 0.0f; } void Window::editorKeyEvent(int key, bool pressed) { @@ -2532,11 +2551,11 @@ int Window::hitTestEditorGizmoAxis(float x, float y, float scale) { glm::mat4 viewProjection = calculateProjectionMatrix() * camera->calculateViewMatrix(); glm::vec2 pointer(x, y); - glm::vec3 center = (boundsMin + boundsMax) * 0.5f; + glm::vec3 center = selectedEditorObject->getPosition().toGlm(); float cameraDistance = glm::length(camera->position.toGlm() - center); float gizmoScale = std::max(1.2f, cameraDistance * 0.16f); float axisLength = gizmoScale * 1.35f; - float hitPadding = std::max(28.0f, 40.0f / effectiveScale); + float hitPadding = std::max(10.0f, 16.0f / effectiveScale); int bestAxis = 0; float bestDistance = hitPadding; @@ -2557,8 +2576,17 @@ int Window::hitTestEditorGizmoAxis(float x, float y, float scale) { depth)) { continue; } + const glm::vec2 segment = to - from; + const float lengthSquared = glm::dot(segment, segment); + if (lengthSquared < 0.000001f) + continue; + const float position = glm::clamp( + glm::dot(pointer - from, segment) / lengthSquared, 0.0f, 1.0f); + if (position < 0.14f) + continue; float distance = distanceToScreenSegment(pointer, from, to); - distance = std::min(distance, distanceToScreenPoint(pointer, to)); + distance = + std::min(distance, distanceToScreenPoint(pointer, to) * 0.72f); if (distance < bestDistance) { bestDistance = distance; bestAxis = axis; @@ -2757,15 +2785,16 @@ void Window::updateEditorDrag(float x, float y, float scale) { if (editorTransformSnapping) angle = std::round(angle / editorTransformSnapIncrement) * editorTransformSnapIncrement; - Rotation3d rotation = editorDragStartRotation; - if (editorActiveGizmoAxis == 1) { - rotation.pitch = editorDragStartRotation.pitch + angle; - } else if (editorActiveGizmoAxis == 2) { - rotation.yaw = editorDragStartRotation.yaw + angle; - } else if (editorActiveGizmoAxis == 3) { - rotation.roll = editorDragStartRotation.roll + angle; - } - selectedEditorObject->setRotation(rotation); + const glm::quat start = + glm::normalize(editorDragStartRotation.toGlmQuat()); + const glm::vec3 rotationAxis = + editorAxisVector(editorActiveGizmoAxis); + const glm::quat delta = + glm::angleAxis(glm::radians(angle), rotationAxis); + const glm::quat next = editorLocalTransformSpace ? start * delta + : delta * start; + selectedEditorObject->setRotation( + Rotation3d::fromGlmQuat(glm::normalize(next))); } else if (editorControlMode == EditorControlMode::Scale) { float viewWidth = std::max(1.0f, static_cast(width)); float viewHeight = std::max(1.0f, static_cast(height)); @@ -2792,16 +2821,18 @@ void Window::updateEditorDrag(float x, float y, float scale) { scaleDelta = (dx + dy) * 0.01f; } Scale3d nextScale = editorDragStartObjectScale; - if (editorActiveGizmoAxis == 1) { + const int component = + editorScaleComponent(selectedEditorObject, editorActiveGizmoAxis, + editorLocalTransformSpace); + if (component == 1) nextScale.x = - std::max(0.05f, editorDragStartObjectScale.x + scaleDelta); - } else if (editorActiveGizmoAxis == 2) { + clampedEditorScale(editorDragStartObjectScale.x + scaleDelta); + else if (component == 2) nextScale.y = - std::max(0.05f, editorDragStartObjectScale.y + scaleDelta); - } else if (editorActiveGizmoAxis == 3) { + clampedEditorScale(editorDragStartObjectScale.y + scaleDelta); + else if (component == 3) nextScale.z = - std::max(0.05f, editorDragStartObjectScale.z + scaleDelta); - } + clampedEditorScale(editorDragStartObjectScale.z + scaleDelta); if (editorTransformSnapping) { nextScale.x = std::round(nextScale.x / editorTransformSnapIncrement) * @@ -2812,9 +2843,9 @@ void Window::updateEditorDrag(float x, float y, float scale) { nextScale.z = std::round(nextScale.z / editorTransformSnapIncrement) * editorTransformSnapIncrement; - nextScale.x = std::max(0.001f, nextScale.x); - nextScale.y = std::max(0.001f, nextScale.y); - nextScale.z = std::max(0.001f, nextScale.z); + nextScale.x = clampedEditorScale(nextScale.x); + nextScale.y = clampedEditorScale(nextScale.y); + nextScale.z = clampedEditorScale(nextScale.z); } selectedEditorObject->setScale(nextScale); } @@ -2910,12 +2941,20 @@ void Window::updateEditorKeyboardTransform(float x, float y, float scale) { } else if (editorControlMode == EditorControlMode::Scale) { const float amount = (dx - dy) * 0.01f; Scale3d next = editorDragStartObjectScale; - if ((axes & 1) != 0) - next.x = std::max(0.05f, next.x + amount); - if ((axes & 2) != 0) - next.y = std::max(0.05f, next.y + amount); - if ((axes & 4) != 0) - next.z = std::max(0.05f, next.z + amount); + std::array components{}; + for (int axisIndex = 0; axisIndex < 3; ++axisIndex) { + if ((axes & (1 << axisIndex)) == 0) + continue; + const int component = editorScaleComponent( + selectedEditorObject, axisIndex + 1, editorLocalTransformSpace); + components[static_cast(component - 1)] = true; + } + if (components[0]) + next.x = clampedEditorScale(next.x + amount); + if (components[1]) + next.y = clampedEditorScale(next.y + amount); + if (components[2]) + next.z = clampedEditorScale(next.z + amount); if (editorTransformSnapping) { next.x = std::round(next.x / editorTransformSnapIncrement) * editorTransformSnapIncrement; @@ -2923,9 +2962,9 @@ void Window::updateEditorKeyboardTransform(float x, float y, float scale) { editorTransformSnapIncrement; next.z = std::round(next.z / editorTransformSnapIncrement) * editorTransformSnapIncrement; - next.x = std::max(0.001f, next.x); - next.y = std::max(0.001f, next.y); - next.z = std::max(0.001f, next.z); + next.x = clampedEditorScale(next.x); + next.y = clampedEditorScale(next.y); + next.z = clampedEditorScale(next.z); } selectedEditorObject->setScale(next); } else if (editorControlMode == EditorControlMode::Rotate) { @@ -2933,14 +2972,21 @@ void Window::updateEditorKeyboardTransform(float x, float y, float scale) { if (editorTransformSnapping) angle = std::round(angle / editorTransformSnapIncrement) * editorTransformSnapIncrement; - Rotation3d next = editorDragStartRotation; - if ((axes & 1) != 0) - next.pitch += angle; - if ((axes & 2) != 0) - next.yaw += angle; - if ((axes & 4) != 0) - next.roll += angle; - selectedEditorObject->setRotation(next); + const glm::quat start = + glm::normalize(editorDragStartRotation.toGlmQuat()); + glm::quat delta(1.0f, 0.0f, 0.0f, 0.0f); + for (int axisIndex = 0; axisIndex < 3; ++axisIndex) { + if ((axes & (1 << axisIndex)) == 0) + continue; + const glm::quat axisDelta = glm::angleAxis( + glm::radians(angle), editorAxisVector(axisIndex + 1)); + delta = editorLocalTransformSpace ? delta * axisDelta + : axisDelta * delta; + } + const glm::quat next = editorLocalTransformSpace ? start * delta + : delta * start; + selectedEditorObject->setRotation( + Rotation3d::fromGlmQuat(glm::normalize(next))); } shadowMapsDirty = true; ssaoMapsDirty = true; @@ -3066,10 +3112,12 @@ void Window::applyEditorZoomDelta(float scrollAmount) { toPivot = glm::normalize(toPivot); } + const float fov = camera->fov; float zoomFactor = std::pow(0.9f, scrollAmount); float nextDistance = std::clamp(distance * zoomFactor, 0.2f, 1000.0f); camera->position = Position3d::fromGlm(pivot - toPivot * nextDistance); camera->lookAt(Position3d::fromGlm(pivot)); + camera->fov = fov; editorOrbitPivot = Position3d::fromGlm(pivot); editorOrbitDistance = nextDistance; editorOrbitPivotInitialized = true; diff --git a/atlas/object/shape.cpp b/atlas/object/shape.cpp index fa8ef6ac..eb0f3cad 100644 --- a/atlas/object/shape.cpp +++ b/atlas/object/shape.cpp @@ -224,7 +224,6 @@ CoreObject createPlane(Size2d size, Color color) { CoreObject plane; plane.attachVertices(vertices); plane.attachIndices({0, 1, 2, 2, 3, 0}); - plane.rotate({-90.0, 0.0, 0.0}); plane.material.albedo = color; return plane; } diff --git a/editor/main.cpp b/editor/main.cpp index 23f52a26..abd90e0d 100644 --- a/editor/main.cpp +++ b/editor/main.cpp @@ -42,13 +42,9 @@ int main(int argc, char **argv) { qWarning() << "Failed to load Manrope"; } - const QStringList manropeFamilies = - QFontDatabase::applicationFontFamilies(manropeFont); - QFont applicationFont = manropeFamilies.isEmpty() - ? QFontDatabase::systemFont( - QFontDatabase::GeneralFont) - : QFont(manropeFamilies.first()); - applicationFont.setPointSizeF(11.0); + QFont applicationFont = + QFontDatabase::systemFont(QFontDatabase::GeneralFont); + applicationFont.setPointSizeF(10.5); app.setFont(applicationFont); styling::loadIconFont(); @@ -85,19 +81,19 @@ int main(int argc, char **argv) { 0, splash, [projectBrowser, projectFile, splash] { auto *editor = new EditorWindow(projectFile); editor->setAttribute(Qt::WA_DeleteOnClose); - QObject::connect( - editor, &EditorWindow::startupStatusChanged, - splash, &SplashScreen::setStatus); - QObject::connect( - editor, &EditorWindow::startupReady, splash, - [projectBrowser, editor, splash](bool, - const QString &) { - splash->finish(); - splash->deleteLater(); - projectBrowser->deleteLater(); - editor->raise(); - editor->activateWindow(); - }); + QObject::connect(editor, + &EditorWindow::startupStatusChanged, + splash, &SplashScreen::setStatus); + QObject::connect(editor, &EditorWindow::startupReady, + splash, + [projectBrowser, editor, + splash](bool, const QString &) { + splash->finish(); + splash->deleteLater(); + projectBrowser->deleteLater(); + editor->raise(); + editor->activateWindow(); + }); editor->show(); }); }); diff --git a/editor/styling/dark.qss b/editor/styling/dark.qss index 8b8b586b..a7f9f02e 100644 --- a/editor/styling/dark.qss +++ b/editor/styling/dark.qss @@ -1,20 +1,20 @@ * { - font-family: "Manrope"; - font-size: 12px; - color: #E7ECF3; - selection-background-color: #647B8D; + font-family: "SF Pro Text", "Inter", "Segoe UI", "Arial"; + font-size: 11px; + color: #D6D6D6; + selection-background-color: #4772B3; selection-color: #FFFFFF; } QWidget { - background-color: #18191B; - color: #E7ECF3; + background-color: #1C1C1C; + color: #D6D6D6; } QMainWindow, QDialog, QFrame { - background-color: #18191B; + background-color: #1C1C1C; } QLabel { @@ -26,19 +26,19 @@ QLabel:disabled { } QToolTip { - background-color: #34373A; + background-color: #303030; color: #F7F9FC; - border: 1px solid #505459; - border-radius: 8px; + border: 1px solid #505050; + border-radius: 4px; padding: 4px 6px; } QGroupBox { - background-color: #242628; - border: 1px solid #3A3D40; - border-radius: 10px; - margin-top: 14px; - padding: 8px; + background-color: #242424; + border: 1px solid #3B3B3B; + border-radius: 4px; + margin-top: 13px; + padding: 7px; color: #F1F4F8; font-weight: 650; } @@ -49,17 +49,17 @@ QGroupBox::title { left: 9px; padding: 0 6px; color: #AEB8C8; - background-color: #242628; + background-color: #242424; } QScrollArea, QAbstractScrollArea { - background-color: #1E2022; + background-color: #202020; border: none; } QAbstractScrollArea::corner { - background-color: #1E2022; + background-color: #202020; } QScrollBar:vertical { @@ -78,7 +78,7 @@ QScrollBar:horizontal { QScrollBar::handle:vertical, QScrollBar::handle:horizontal { - background-color: #505357; + background-color: #4A4A4A; border-radius: 4px; min-height: 30px; min-width: 30px; @@ -86,7 +86,7 @@ QScrollBar::handle:horizontal { QScrollBar::handle:vertical:hover, QScrollBar::handle:horizontal:hover { - background-color: #65696E; + background-color: #666666; } QScrollBar::add-line, @@ -101,53 +101,53 @@ QScrollBar::sub-page { QPushButton, QToolButton { - background-color: #2B2E31; - border: 1px solid #45494D; - border-radius: 8px; - padding: 4px 8px; - color: #E9EDF4; - min-height: 18px; - font-weight: 550; + background-color: #303030; + border: 1px solid #464646; + border-radius: 3px; + padding: 3px 8px; + color: #D8D8D8; + min-height: 22px; + font-weight: 500; } QPushButton:hover, QToolButton:hover { - background-color: #363A3E; - border-color: #5A5F65; + background-color: #3A3A3A; + border-color: #606060; } QPushButton:pressed, QToolButton:pressed { - background-color: #232527; - border-color: #6F7B84; + background-color: #242424; + border-color: #4772B3; } QPushButton:checked, QToolButton:checked { - background-color: #3A4248; - border-color: #6F7B84; + background-color: #3B5F8A; + border-color: #5D8BC0; color: #FFFFFF; } QPushButton:default, #primaryAction { - background-color: #596A76; - border-color: #73838E; + background-color: #4772B3; + border-color: #6791C9; color: #FFFFFF; font-weight: 650; } QPushButton:default:hover, #primaryAction:hover { - background-color: #667985; - border-color: #82919B; + background-color: #5682C2; + border-color: #7AA1D4; } QPushButton:disabled, QToolButton:disabled { - background-color: #202224; - border-color: #303235; - color: #566174; + background-color: #242424; + border-color: #333333; + color: #686868; } QToolButton::menu-indicator { @@ -163,11 +163,12 @@ QDoubleSpinBox, QDateEdit, QTimeEdit, QDateTimeEdit { - background-color: #1B1D1F; - border: 1px solid #3B3E42; - border-radius: 8px; - padding: 4px 6px; - color: #EEF2F7; + background-color: #181818; + border: 1px solid #3D3D3D; + border-radius: 3px; + padding: 3px 7px; + color: #E2E2E2; + min-height: 22px; } QLineEdit:hover, @@ -176,7 +177,7 @@ QPlainTextEdit:hover, QComboBox:hover, QSpinBox:hover, QDoubleSpinBox:hover { - border-color: #54585D; + border-color: #5A5A5A; } QLineEdit:focus, @@ -185,8 +186,8 @@ QPlainTextEdit:focus, QComboBox:focus, QSpinBox:focus, QDoubleSpinBox:focus { - background-color: #222426; - border-color: #71808A; + background-color: #222222; + border-color: #5D8BC0; } QLineEdit:disabled, @@ -194,9 +195,9 @@ QTextEdit:disabled, QComboBox:disabled, QSpinBox:disabled, QDoubleSpinBox:disabled { - background-color: #202224; - border-color: #303235; - color: #566174; + background-color: #202020; + border-color: #303030; + color: #666666; } QComboBox { @@ -207,27 +208,27 @@ QComboBox::drop-down { subcontrol-origin: padding; subcontrol-position: top right; width: 22px; - border-left: 1px solid #3B3E42; + border-left: 1px solid #3B3B3B; } QComboBox QAbstractItemView { - background-color: #292C2F; - border: 1px solid #494D52; - border-radius: 10px; + background-color: #292929; + border: 1px solid #4B4B4B; + border-radius: 4px; padding: 3px; - selection-background-color: #3A4248; + selection-background-color: #3B5F8A; } QAbstractSpinBox::up-button, QAbstractSpinBox::down-button { - background-color: #292C2F; + background-color: #292929; border: none; width: 16px; } QAbstractSpinBox::up-button:hover, QAbstractSpinBox::down-button:hover { - background-color: #3E4246; + background-color: #3E3E3E; } QCheckBox, @@ -240,47 +241,47 @@ QCheckBox::indicator, QRadioButton::indicator { width: 15px; height: 15px; - background-color: #1B1D1F; - border: 1px solid #55595D; - border-radius: 5px; + background-color: #171717; + border: 1px solid #555555; + border-radius: 3px; } QCheckBox::indicator:hover, QRadioButton::indicator:hover { - border-color: #747B81; + border-color: #777777; } QCheckBox::indicator:checked, QRadioButton::indicator:checked { - background-color: #71808A; - border-color: #89969F; + background-color: #4772B3; + border-color: #6D98CE; } QSlider::groove:horizontal { - background-color: #34373A; + background-color: #353535; height: 4px; border-radius: 2px; } QSlider::handle:horizontal { - background-color: #7D8991; - border: 2px solid #A8AFB4; + background-color: #5D8BC0; + border: 2px solid #A0B9D7; width: 12px; margin: -5px 0; border-radius: 7px; } QProgressBar { - background-color: #26282B; - border: 1px solid #42464A; - border-radius: 6px; + background-color: #262626; + border: 1px solid #424242; + border-radius: 3px; height: 8px; text-align: center; } QProgressBar::chunk { - background-color: #71808A; - border-radius: 6px; + background-color: #4772B3; + border-radius: 3px; } QTreeView, @@ -288,8 +289,8 @@ QListView, QListWidget, QTableView, QTableWidget { - background-color: #1E2022; - alternate-background-color: #232527; + background-color: #202020; + alternate-background-color: #242424; border: none; color: #DCE3EC; show-decoration-selected: 1; @@ -301,7 +302,7 @@ QListWidget::item, QTableView::item, QTableWidget::item { border: 1px solid transparent; - border-radius: 7px; + border-radius: 2px; padding: 3px 5px; } @@ -310,8 +311,8 @@ QListView::item:hover, QListWidget::item:hover, QTableView::item:hover, QTableWidget::item:hover { - background-color: #2B2E31; - border-color: #43474B; + background-color: #303030; + border-color: #464646; } QTreeView::item:selected, @@ -319,89 +320,89 @@ QListView::item:selected, QListWidget::item:selected, QTableView::item:selected, QTableWidget::item:selected { - background-color: #393F44; - border-color: #66737C; + background-color: #3B5F8A; + border-color: #5D8BC0; color: #FFFFFF; } QHeaderView { - background-color: #202224; + background-color: #202020; } QHeaderView::section { - background-color: #292C2F; + background-color: #292929; border: none; - border-right: 1px solid #404347; - border-bottom: 1px solid #404347; + border-right: 1px solid #404040; + border-bottom: 1px solid #404040; padding: 5px 7px; color: #98A4B7; font-weight: 600; } QTabWidget::pane { - background-color: #1E2022; - border: 1px solid #3B3E42; - border-radius: 0 0 10px 10px; + background-color: #202020; + border: 1px solid #3B3B3B; + border-radius: 0; } QTabBar::tab { - background-color: #202224; + background-color: #252525; border: none; - border-right: 1px solid #383B3F; - border-bottom: 1px solid #3B3E42; + border-right: 1px solid #393939; + border-bottom: 1px solid #3B3B3B; color: #7F8B9D; min-width: 88px; padding: 5px 10px; - margin: 2px 1px; - border-radius: 8px; + margin: 1px 0; + border-radius: 0; } QTabBar::tab:hover { - background-color: #2B2E31; + background-color: #303030; color: #C8D1DF; } QTabBar::tab:selected { - background-color: #36393C; + background-color: #333333; color: #F2F5F9; - border-bottom: 2px solid #78858E; + border-bottom: 2px solid #5D8BC0; } QMenuBar#atlasMenuBar { - background-color: #141517; - border-bottom: 1px solid #303236; - padding: 2px 6px; + background-color: #181818; + border-bottom: 1px solid #303030; + padding: 1px 6px; } QMenuBar#atlasMenuBar::item { background: transparent; color: #B8C2D0; padding: 5px 9px; - border-radius: 7px; + border-radius: 3px; } QMenuBar#atlasMenuBar::item:selected, QMenuBar#atlasMenuBar::item:pressed { - background-color: #34373A; + background-color: #343434; color: #FFFFFF; } QMenu { - background-color: #292C2F; - border: 1px solid #494D52; - border-radius: 10px; - padding: 3px; + background-color: #292929; + border: 1px solid #515151; + border-radius: 4px; + padding: 4px; } QMenu::item { background: transparent; - border-radius: 7px; - padding: 5px 30px 5px 8px; + border-radius: 2px; + padding: 6px 32px 6px 9px; color: #DCE3EC; } QMenu::item:selected { - background-color: #3A3E42; + background-color: #3B5F8A; color: #FFFFFF; } @@ -410,13 +411,13 @@ QMenu::item:disabled { } QMenu::separator { - background-color: #424549; + background-color: #424242; height: 1px; margin: 5px 8px; } QSplitter::handle { - background-color: #111214; + background-color: #111111; } QSplitter::handle:horizontal { @@ -428,28 +429,38 @@ QSplitter::handle:vertical { } QSplitter::handle:hover { - background-color: #71889A; + background-color: #5D8BC0; } QStatusBar { - background-color: #141517; - border-top: 1px solid #303236; + background-color: #181818; + border-top: 1px solid #303030; color: #8490A4; } #atlasStatusBar { - min-height: 21px; + min-height: 23px; padding: 0 6px; } #statusRuntimeIcon { - padding: 0 4px; + padding: 0; +} + +#statusRuntime { + background: transparent; + border-right: 1px solid #343434; +} + +#statusRuntimeText { + color: #9BA79F; + font-size: 10px; } #statusRenderer { - background-color: #2B2E30; - border: 1px solid #45494D; - border-radius: 8px; + background-color: #292929; + border: 1px solid #454545; + border-radius: 3px; color: #B6BCB8; font-size: 9px; font-weight: 700; @@ -463,103 +474,180 @@ QStatusBar { } #workspaceBar { - background-color: #1E2022; + background-color: #202020; border: none; - border-bottom: 1px solid #3B3E42; - spacing: 3px; - padding: 2px 5px 2px 0; + border-bottom: 1px solid #3B3B3B; + spacing: 4px; + padding: 3px 6px 3px 0; + min-height: 38px; } #workspaceIdentity { background: transparent; - border-right: 1px solid #424549; + border-right: 1px solid #424242; } #workspaceMark { + background-color: #292929; + border: 1px solid #3F3F3F; + border-radius: 4px; + padding: 3px; +} + +#workspaceIdentityText { background: transparent; } #workspaceBrand { - color: #F5F7FA; - font-size: 12px; - font-weight: 800; + color: #8C8C8C; + font-size: 8px; + font-weight: 750; } #workspaceProject { - color: #7F8B9D; - font-size: 11px; + color: #EFEFEF; + font-size: 12px; + font-weight: 650; +} + +#workspaceSwitcher { + background-color: #181818; + border: 1px solid #383838; + border-radius: 4px; + margin-left: 7px; } #workspaceModeButton { - background-color: transparent; + background-color: #222222; border: 1px solid transparent; - border-radius: 9px; - color: #94A0B2; - padding: 4px 8px; - margin: 0 1px; + border-radius: 3px; + color: #A0A0A0; + padding: 3px 9px; + min-height: 22px; } #workspaceModeButton:hover { - background-color: #303336; - color: #DDE4ED; + background-color: #333333; + color: #E2E2E2; } #workspaceModeButton:checked { - background-color: #3A3E42; - border-color: #5E656B; + background-color: #3B5F8A; + border-color: #5D8BC0; color: #FFFFFF; } -#workspaceUtilityButton { - background: transparent; - border-color: transparent; - min-width: 26px; - padding: 3px 5px; +#workspaceCommandButton, +#workspaceUtilityButton, +#workspaceBuildButton, +#workspaceLaunchButton { + min-width: 42px; + padding: 3px 9px; + margin-left: 2px; + min-height: 24px; +} + +#workspaceCommandButton, +#workspaceUtilityButton, +#workspaceBuildButton { + background-color: #292929; + border-color: #444444; } -#workspaceBuildButton, #workspaceLaunchButton { - min-width: 26px; - padding: 3px 5px; - background-color: #292C2F; - border-color: #45494D; - margin-left: 3px; + background-color: #3F684F; + border-color: #5A8B6B; + color: #FFFFFF; + font-weight: 650; } +#workspaceCommandButton:hover, +#workspaceUtilityButton:hover, #workspaceBuildButton:hover, #workspaceLaunchButton:hover { - background-color: #363A3E; - border-color: #5A5F65; + background-color: #3A3A3A; + border-color: #606060; +} + +#workspaceLaunchButton:hover { + background-color: #4E7B5E; + border-color: #6B9B79; +} + +#workspaceContextBar { + background-color: #292929; + border: none; + border-bottom: 1px solid #3C3C3C; + spacing: 2px; + padding: 1px 5px 1px 0; + min-height: 27px; +} + +#workspaceBreadcrumb { + background: transparent; + border-right: 1px solid #414141; +} + +#workspaceContextIcon { + background: transparent; +} + +#workspaceContextTitle { + color: #E0E0E0; + font-weight: 650; +} + +#workspaceContextPath { + color: #7F7F7F; +} + +#workspacePanelButton { + background-color: transparent; + border-color: transparent; + color: #8F8F8F; + padding: 2px 7px; + min-height: 20px; +} + +#workspacePanelButton:hover { + background-color: #363636; + color: #DADADA; +} + +#workspacePanelButton:checked { + background-color: #333333; + border-color: #4A4A4A; + color: #EAEAEA; } #sceneTabs { - background-color: #191A1C; - border-bottom: 1px solid #3B3E42; + background-color: #181818; + border-bottom: 1px solid #3B3B3B; } #sceneTabs::tab { - background-color: #202224; - border-right: 1px solid #3A3D40; + background-color: #242424; + border-right: 1px solid #3A3A3A; color: #7F8B9D; min-width: 112px; padding: 5px 10px; - margin: 2px; - border-radius: 8px; + margin: 0; + border-radius: 0; } #sceneTabs::tab:selected { - background-color: #36393C; + background-color: #333333; color: #F3F6FA; - border-bottom: 2px solid #78858E; + border-bottom: 2px solid #5D8BC0; } #viewportTools { - background-color: #141517; + background-color: #151515; } #viewportToolbar { - background-color: #202224; - border-bottom: 1px solid #3B3E42; + background-color: #292929; + border-bottom: 1px solid #3B3B3B; } #viewportPlaybackButton, @@ -605,16 +693,16 @@ QStatusBar { #materialEditorHeader, #postProcessingToolbar, #workspaceToolbar { - background-color: #232527; - border-bottom: 1px solid #404347; - padding: 3px; + background-color: #292929; + border-bottom: 1px solid #404040; + padding: 2px; } #panelAddButton, #materialSaveButton, #workspaceApplyButton { - background-color: #303438; - border-color: #4C5257; + background-color: #333333; + border-color: #4C4C4C; color: #D8DBDE; } @@ -644,49 +732,49 @@ QStatusBar { } QTreeView#hierarchyTree { - background-color: #1E2022; - border-top: 1px solid #303236; - padding: 5px 3px; + background-color: #202020; + border-top: 1px solid #303030; + padding: 3px 2px; } QTreeView#hierarchyTree::item { - min-height: 26px; - padding: 3px 7px; + min-height: 22px; + padding: 1px 6px; } QTreeView#hierarchyTree::item:hover { - background-color: #2B2E31; - border-color: #43474B; + background-color: #303030; + border-color: #444444; } QTreeView#hierarchyTree::item:selected { - background-color: #343C43; - border-color: #66737C; + background-color: #3B5F8A; + border-color: #5D8BC0; color: #FFFFFF; } QListView#contentGrid { - background-color: #1A1C1E; - border-top: 1px solid #303236; - padding: 6px; + background-color: #1C1C1C; + border-top: 1px solid #303030; + padding: 5px; } QListView#contentGrid::item { - background-color: #232527; - border: 1px solid #34373A; - border-radius: 11px; - padding: 7px; + background-color: #252525; + border: 1px solid #363636; + border-radius: 4px; + padding: 6px; color: #BCC6D4; } QListView#contentGrid::item:hover { - background-color: #2E3134; - border-color: #4B5055; + background-color: #303030; + border-color: #505050; } QListView#contentGrid::item:selected { - background-color: #393F44; - border-color: #66737C; + background-color: #3B5F8A; + border-color: #5D8BC0; color: #FFFFFF; } @@ -694,21 +782,21 @@ QScrollArea#inspectorScroll, #inspectorContent, #materialEditorBody, #postProcessingBody { - background-color: #1E2022; + background-color: #202020; border: none; } #inspectorHeader { - background-color: #25272A; - border: 1px solid #404347; - border-radius: 12px; - padding: 6px; + background-color: #292929; + border: 1px solid #414141; + border-radius: 4px; + padding: 5px; } #inspectorObjectIcon { - background-color: #34373B; - border: 1px solid #4E5257; - border-radius: 10px; + background-color: #343434; + border: 1px solid #4E4E4E; + border-radius: 4px; padding: 4px; } @@ -722,8 +810,8 @@ QScrollArea#inspectorScroll, #inspectorNameField:hover, #inspectorNameField:focus { - background-color: #1E2022; - border-color: #5A5F65; + background-color: #202020; + border-color: #5A5A5A; } #inspectorTypeLabel { @@ -733,16 +821,16 @@ QScrollArea#inspectorScroll, } #inspectorComponent { - background-color: #242628; - border: 1px solid #3B3E41; - border-radius: 11px; - margin-top: 3px; + background-color: #242424; + border: 1px solid #3D3D3D; + border-radius: 3px; + margin-top: 2px; } #inspectorComponentHeaderRow { - background-color: #292C2F; - border-bottom: 1px solid #3B3E41; - border-radius: 11px 11px 0 0; + background-color: #2D2D2D; + border-bottom: 1px solid #3B3B3B; + border-radius: 3px 3px 0 0; } #inspectorComponentHeader { @@ -751,11 +839,11 @@ QScrollArea#inspectorScroll, color: #E8ECF2; font-weight: 650; text-align: left; - padding: 5px 7px; + padding: 4px 7px; } #inspectorComponentHeader:hover { - background-color: #303337; + background-color: #383838; } #inspectorComponentRemoveButton { @@ -770,8 +858,8 @@ QScrollArea#inspectorScroll, } #inspectorComponentBody { - background-color: #242628; - padding: 5px; + background-color: #242424; + padding: 4px; } #inspectorPropertyRow { @@ -787,9 +875,9 @@ QScrollArea#inspectorScroll, #inspectorVectorField, #inspectorColorField, #inspectorNumericField { - background-color: #1B1D1F; - border: 1px solid #404448; - border-radius: 9px; + background-color: #181818; + border: 1px solid #3D3D3D; + border-radius: 3px; } #inspectorVectorField QDoubleSpinBox, @@ -798,13 +886,26 @@ QScrollArea#inspectorScroll, border: none; } -#inspectorAxisLabel { - background-color: #34373A; - border-radius: 7px; - color: #AAB5C5; +#inspectorAxisX, +#inspectorAxisY, +#inspectorAxisZ { + border-radius: 2px; + color: #F1F1F1; font-size: 9px; font-weight: 750; - padding: 2px 4px; + padding: 2px 5px; +} + +#inspectorAxisX { + background-color: #874747; +} + +#inspectorAxisY { + background-color: #477451; +} + +#inspectorAxisZ { + background-color: #435E86; } #inspectorSyncButton, @@ -821,9 +922,9 @@ QScrollArea#inspectorScroll, } #inspectorColorSwatch { - background-color: #292C2F; - border: 1px solid #4D5155; - border-radius: 8px; + background-color: #292929; + border: 1px solid #4D4D4D; + border-radius: 3px; padding: 2px; } @@ -834,9 +935,9 @@ QScrollArea#inspectorScroll, } #inspectorNestedGroup { - background-color: #202224; - border: 1px solid #383B3F; - border-radius: 10px; + background-color: #202020; + border: 1px solid #383838; + border-radius: 3px; margin: 3px 0; padding: 5px; } @@ -861,21 +962,21 @@ QScrollArea#inspectorScroll, #inspectorOpenAssetButton, #inspectorAddComponentButton { - background-color: #292C2F; - border: 1px dashed #5A5F65; + background-color: #292929; + border: 1px dashed #5A5A5A; color: #C7D0DD; padding: 6px; } #inspectorAddComponentButton:hover { - background-color: #363A3E; - border-color: #646B70; + background-color: #363636; + border-color: #666666; color: #FFFFFF; } #inspectorAudioControls { - background-color: #202224; - border-radius: 9px; + background-color: #202020; + border-radius: 3px; } #materialEditorTitle, @@ -897,15 +998,15 @@ QScrollArea#inspectorScroll, } #materialPreview { - background-color: #141517; - border: 1px solid #44484C; - border-radius: 12px; + background-color: #151515; + border: 1px solid #444444; + border-radius: 4px; } #materialColorButton { - background-color: #292C2F; - border-color: #45494D; - border-radius: 9px; + background-color: #292929; + border-color: #454545; + border-radius: 3px; color: #C9CDD0; text-align: left; padding: 4px 8px; @@ -917,16 +1018,16 @@ QScrollArea#inspectorScroll, } #materialTextureSlot { - background-color: #232527; - border: 1px solid #404448; - border-radius: 10px; + background-color: #252525; + border: 1px solid #404040; + border-radius: 4px; padding: 4px; } #materialTexturePreview { - background-color: #1A1C1E; - border: 1px solid #494D52; - border-radius: 9px; + background-color: #1A1A1A; + border: 1px solid #494949; + border-radius: 3px; color: #9E897D; font-weight: 750; } @@ -943,13 +1044,13 @@ QScrollArea#inspectorScroll, #environmentPages, #environmentWorkspace, #environmentPage { - background-color: #1E2022; + background-color: #202020; border: none; } QListWidget#environmentCategories { - background-color: #1B1D1F; - border-right: 1px solid #3B3E42; + background-color: #1B1B1B; + border-right: 1px solid #3B3B3B; padding: 5px; } @@ -959,8 +1060,8 @@ QListWidget#environmentCategories::item { } QListWidget#environmentCategories::item:selected { - background-color: #343C43; - border-left: 2px solid #8498A8; + background-color: #3B5F8A; + border-left: 2px solid #5D8BC0; } #environmentPageTitle { @@ -974,43 +1075,43 @@ QListWidget#environmentCategories::item:selected { } QGroupBox#environmentSection { - background-color: #242628; - border-color: #3B3E42; + background-color: #242424; + border-color: #3B3B3B; } ads--CDockContainerWidget { - background-color: #111214; + background-color: #111111; } ads--CDockContainerWidget > QSplitter { - background-color: #111214; + background-color: #111111; } ads--CDockContainerWidget ads--CDockSplitter::handle { - background-color: #111214; + background-color: #111111; } ads--CDockContainerWidget ads--CDockSplitter::handle:hover { - background-color: #71889A; + background-color: #5D8BC0; } ads--CDockAreaWidget { - background-color: #1E2022; - border: 1px solid #34373A; + background-color: #202020; + border: 1px solid #343434; } ads--CDockAreaWidget[focused="true"] { - border-color: #50545A; + border-color: #4A6382; } ads--CDockAreaTitleBar { - background-color: #202224; - border-bottom: 1px solid #3B3E42; - min-height: 26px; + background-color: #292929; + border-bottom: 1px solid #3B3B3B; + min-height: 25px; } ads--CDockAreaWidget[focused="true"] ads--CDockAreaTitleBar { - background-color: #26282B; + background-color: #2D2D2D; } #tabsContainerWidget { @@ -1020,37 +1121,37 @@ ads--CDockAreaWidget[focused="true"] ads--CDockAreaTitleBar { ads--CTitleBarButton { background: transparent; border: none; - border-radius: 7px; + border-radius: 2px; min-width: 22px; min-height: 22px; padding: 2px; } ads--CTitleBarButton:hover { - background-color: #3E4246; + background-color: #404040; } ads--CDockWidgetTab { - background-color: #202224; + background-color: #292929; border: none; - border-right: 1px solid #3A3D40; - border-bottom: 1px solid #3B3E42; + border-right: 1px solid #3A3A3A; + border-bottom: 1px solid #3B3B3B; padding: 1px 5px; - margin: 2px 1px; - border-radius: 7px; + margin: 1px 0; + border-radius: 0; } ads--CDockWidgetTab:hover { - background-color: #2C2F32; + background-color: #333333; } ads--CDockWidgetTab[activeTab="true"] { - background-color: #373A3D; - border-bottom: 2px solid #78858E; + background-color: #333333; + border-bottom: 2px solid #5D8BC0; } ads--CDockWidgetTab[focused="true"] { - background-color: #2B2E31; + background-color: #303030; } ads--CDockWidgetTab #dockWidgetTabLabel { @@ -1086,7 +1187,7 @@ ads--CDockWidgetTab[focused="true"] #dockWidgetTabLabel { } ads--CDockWidget { - background-color: #1E2022; + background-color: #202020; } ads--CAutoHideSideBar, diff --git a/editor/views/editor/editor.cpp b/editor/views/editor/editor.cpp index 70954176..41515518 100644 --- a/editor/views/editor/editor.cpp +++ b/editor/views/editor/editor.cpp @@ -40,6 +40,7 @@ #include #include #include +#include #include #include #include @@ -47,6 +48,7 @@ #include #include #include +#include #include #include #include @@ -87,8 +89,8 @@ #include "editor/views/splashScreen.h" namespace { -constexpr int DockStateVersion = 9; -constexpr auto DockStateKey = "docking/state/v9"; +constexpr int DockStateVersion = 10; +constexpr auto DockStateKey = "docking/state/v10"; QIcon commandIcon(const QString &name) { const QString command = name.toLower(); @@ -354,43 +356,15 @@ void EditorWindow::setupMenus() { styling::icon(styling::Icon::ArrowCounterClockwise, "#7E929C")); undoAction->setShortcut(QKeySequence::Undo); undoAction->setShortcutContext(Qt::ApplicationShortcut); - connect(undoAction, &QAction::triggered, this, [this] { - if (auto *field = - qobject_cast(QApplication::focusWidget())) { - field->undo(); - } else if (materialEditorPanel != nullptr && - materialEditorPanel->isAncestorOf( - QApplication::focusWidget())) { - materialEditorPanel->undo(); - } else if (graphiteEditorPanel != nullptr && - graphiteEditorPanel->isAncestorOf( - QApplication::focusWidget())) { - graphiteEditorPanel->undo(); - } else if (viewportPanel != nullptr) { - viewportPanel->undo(); - } - }); + connect(undoAction, &QAction::triggered, this, + &EditorWindow::undoActiveEditor); auto *redoAction = editMenu->addAction("Redo"); redoAction->setIcon( styling::icon(styling::Icon::ArrowClockwise, "#7E929C")); redoAction->setShortcut(QKeySequence::Redo); redoAction->setShortcutContext(Qt::ApplicationShortcut); - connect(redoAction, &QAction::triggered, this, [this] { - if (auto *field = - qobject_cast(QApplication::focusWidget())) { - field->redo(); - } else if (materialEditorPanel != nullptr && - materialEditorPanel->isAncestorOf( - QApplication::focusWidget())) { - materialEditorPanel->redo(); - } else if (graphiteEditorPanel != nullptr && - graphiteEditorPanel->isAncestorOf( - QApplication::focusWidget())) { - graphiteEditorPanel->redo(); - } else if (viewportPanel != nullptr) { - viewportPanel->redo(); - } - }); + connect(redoAction, &QAction::triggered, this, + &EditorWindow::redoActiveEditor); editMenu->addSeparator(); addCommand(editMenu, "Find…", "Meta+F", [this] { showGlobalSearch(); }); editMenu->addSeparator(); @@ -614,14 +588,13 @@ void EditorWindow::setupDocks() { : "Runtime unavailable"); emit startupReady(success, message); }); - connect(viewportPanel, &ViewportPanel::runtimeLoadingStarted, this, - [this] { - if (!startupComplete || assetLoadingSplash != nullptr) { - return; - } - assetLoadingSplash = new SplashScreen(this); - assetLoadingSplash->start("Loading assets..."); - }); + connect(viewportPanel, &ViewportPanel::runtimeLoadingStarted, this, [this] { + if (!startupComplete || assetLoadingSplash != nullptr) { + return; + } + assetLoadingSplash = new SplashScreen(this); + assetLoadingSplash->start("Loading assets..."); + }); connect(viewportPanel, &ViewportPanel::runtimeLoadingStatusChanged, this, [this](const QString &status) { emit startupStatusChanged(status); @@ -641,8 +614,7 @@ void EditorWindow::setupDocks() { viewportTools = new ViewportTools(viewportPanel, projectFile); materialEditorPanel = new MaterialEditorPanel(viewportPanel); postProcessingPanel = new PostProcessingPanel(viewportPanel); - graphiteEditorPanel = - new GraphiteEditorPanel(viewportPanel, projectFile); + graphiteEditorPanel = new GraphiteEditorPanel(viewportPanel, projectFile); workspaceStack = new QStackedWidget(this); workspaceStack->setObjectName("editorWorkspaceStack"); workspaceStack->addWidget(viewportTools); @@ -663,7 +635,7 @@ void EditorWindow::setupDocks() { {.id = "hierarchy", .title = "Scene", .widget = hierarchyPanel, - .area = EditorDockArea::Left, + .area = EditorDockArea::Right, .icon = styling::icon(styling::Icon::TreeStructure, "#8498A8")}); inspectorPanel = new InspectorPanel(viewportPanel, projectFile); @@ -673,6 +645,10 @@ void EditorWindow::setupDocks() { .widget = inspectorPanel, .area = EditorDockArea::Right, .icon = styling::icon(styling::Icon::SlidersHorizontal, "#A1957D")}); + if (hierarchyDock->dockAreaWidget() != nullptr) { + coreManager->addDockWidget(ads::BottomDockWidgetArea, inspectorDock, + hierarchyDock->dockAreaWidget()); + } contentBrowser = new ContentBrowserPanel(projectFile); auto *contentDock = dockManager->addPanel( @@ -726,11 +702,10 @@ void EditorWindow::setupDocks() { action->setShortcutContext(Qt::ApplicationShortcut); } windowMenu->addSeparator(); - const QList> workspaceModes{ - {"Scene", 0}, - {"Shading", 1}, - {"Post-Processing", 2}, - {"Graphite", 3}}; + const QList> workspaceModes{{"Scene", 0}, + {"Shading", 1}, + {"Post-Processing", 2}, + {"Graphite", 3}}; for (const auto &[name, index] : workspaceModes) { auto *action = windowMenu->addAction( QStringLiteral("Open %1 Workspace").arg(name), this, @@ -742,8 +717,7 @@ void EditorWindow::setupDocks() { }); action->setIcon( index == 0 ? styling::icon(styling::Icon::CubeFocus, "#7E929C") - : index == 1 - ? styling::icon(styling::Icon::Material, "#A1957D") + : index == 1 ? styling::icon(styling::Icon::Material, "#A1957D") : index == 2 ? styling::icon(styling::Icon::FilmStrip, "#849589") : styling::icon(styling::Icon::Palette, "#849589")); @@ -813,43 +787,96 @@ void EditorWindow::setupWorkspaceBar() { bar->setObjectName("workspaceBar"); bar->setMovable(false); bar->setFloatable(false); - bar->setIconSize(QSize(17, 17)); + bar->setIconSize(QSize(18, 18)); bar->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); addToolBar(Qt::TopToolBarArea, bar); auto *identity = new QWidget(bar); identity->setObjectName("workspaceIdentity"); auto *identityLayout = new QHBoxLayout(identity); - identityLayout->setContentsMargins(10, 0, 14, 0); - identityLayout->setSpacing(8); + identityLayout->setContentsMargins(10, 2, 16, 2); + identityLayout->setSpacing(9); auto *mark = new QLabel(identity); mark->setObjectName("workspaceMark"); - mark->setPixmap(windowIcon().pixmap(22, 22)); - auto *brand = new QLabel("ATLAS", identity); + mark->setPixmap(windowIcon().pixmap(24, 24)); + auto *identityText = new QWidget(identity); + identityText->setObjectName("workspaceIdentityText"); + auto *identityTextLayout = new QVBoxLayout(identityText); + identityTextLayout->setContentsMargins(0, 0, 0, 0); + identityTextLayout->setSpacing(0); + auto *brand = new QLabel("ATLAS ENGINE", identityText); brand->setObjectName("workspaceBrand"); - auto *project = new QLabel(projectName, identity); + auto *project = new QLabel(projectName, identityText); project->setObjectName("workspaceProject"); + identityTextLayout->addWidget(brand); + identityTextLayout->addWidget(project); identityLayout->addWidget(mark); - identityLayout->addWidget(brand); - identityLayout->addWidget(project); + identityLayout->addWidget(identityText); bar->addWidget(identity); - workspaceModeGroup = new QButtonGroup(bar); + auto *switcher = new QFrame(bar); + switcher->setObjectName("workspaceSwitcher"); + auto *switcherLayout = new QHBoxLayout(switcher); + switcherLayout->setContentsMargins(3, 3, 3, 3); + switcherLayout->setSpacing(1); + workspaceModeGroup = new QButtonGroup(switcher); workspaceModeGroup->setExclusive(true); - auto addMode = [this, bar](const QString &text, styling::Icon icon, - const QColor &color, int index, - bool selected = false) { - auto *button = new QToolButton(bar); + bar->addWidget(switcher); + + addToolBarBreak(Qt::TopToolBarArea); + auto *contextBar = new QToolBar("Context", this); + contextBar->setObjectName("workspaceContextBar"); + contextBar->setMovable(false); + contextBar->setFloatable(false); + contextBar->setIconSize(QSize(15, 15)); + contextBar->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); + addToolBar(Qt::TopToolBarArea, contextBar); + + auto *breadcrumb = new QWidget(contextBar); + breadcrumb->setObjectName("workspaceBreadcrumb"); + auto *breadcrumbLayout = new QHBoxLayout(breadcrumb); + breadcrumbLayout->setContentsMargins(8, 0, 12, 0); + breadcrumbLayout->setSpacing(7); + auto *workspaceContextIcon = new QLabel(breadcrumb); + workspaceContextIcon->setObjectName("workspaceContextIcon"); + workspaceContextIcon->setPixmap( + styling::icon(styling::Icon::CubeFocus, "#7E929C").pixmap(15, 15)); + auto *workspaceContextTitle = new QLabel("Scene", breadcrumb); + workspaceContextTitle->setObjectName("workspaceContextTitle"); + auto *workspaceContextPath = new QLabel("· Layout", breadcrumb); + workspaceContextPath->setObjectName("workspaceContextPath"); + breadcrumbLayout->addWidget(workspaceContextIcon); + breadcrumbLayout->addWidget(workspaceContextTitle); + breadcrumbLayout->addWidget(workspaceContextPath); + contextBar->addWidget(breadcrumb); + + const QStringList workspacePaths{"· Layout", "· Materials", "· Effects", + "· Interface"}; + auto addMode = [this, switcher, switcherLayout, workspaceContextIcon, + workspaceContextTitle, workspaceContextPath, + workspacePaths](const QString &text, styling::Icon icon, + const QColor &color, int index, + bool selected = false) { + auto *button = new QToolButton(switcher); button->setObjectName("workspaceModeButton"); button->setText(text); button->setIcon(styling::icon(icon, color)); button->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); button->setCheckable(true); button->setChecked(selected); + button->setProperty("workspacePath", workspacePaths.at(index)); workspaceModeGroup->addButton(button, index); - bar->addWidget(button); + switcherLayout->addWidget(button); connect(button, &QToolButton::clicked, this, - [this, index] { activateWorkspace(index); }); + [this, workspaceContextIcon, workspaceContextTitle, + workspaceContextPath, workspacePaths, text, icon, color, + index] { + activateWorkspace(index); + workspaceContextIcon->setPixmap( + styling::icon(icon, color).pixmap(15, 15)); + workspaceContextTitle->setText(text); + workspaceContextPath->setText(workspacePaths.at(index)); + }); }; addMode("Scene", styling::Icon::CubeFocus, "#7E929C", 0, true); addMode("Shading", styling::Icon::Material, "#A1957D", 1); @@ -860,9 +887,19 @@ void EditorWindow::setupWorkspaceBar() { spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); bar->addWidget(spacer); + auto *commands = new QToolButton(bar); + commands->setObjectName("workspaceCommandButton"); + commands->setIcon(styling::icon(styling::Icon::MagnifyingGlass, "#7E929C")); + commands->setText("Commands"); + commands->setToolTip("Command Palette · Command Shift P"); + bar->addWidget(commands); + connect(commands, &QToolButton::clicked, this, + &EditorWindow::showCommandPalette); + auto *save = new QToolButton(bar); save->setObjectName("workspaceUtilityButton"); save->setIcon(styling::icon(styling::Icon::FloppyDisk, "#A1957D")); + save->setText("Save"); save->setToolTip("Save Scene"); bar->addWidget(save); connect(save, &QToolButton::clicked, this, [this] { @@ -877,6 +914,7 @@ void EditorWindow::setupWorkspaceBar() { auto *build = new QToolButton(bar); build->setObjectName("workspaceBuildButton"); build->setIcon(styling::icon(styling::Icon::Package, "#A1957D")); + build->setText("Build"); build->setToolTip("Build Project"); bar->addWidget(build); connect(build, &QToolButton::clicked, this, @@ -885,17 +923,57 @@ void EditorWindow::setupWorkspaceBar() { auto *launch = new QToolButton(bar); launch->setObjectName("workspaceLaunchButton"); launch->setIcon(styling::icon(styling::Icon::RocketLaunch, "#849589")); + launch->setText("Run"); launch->setToolTip("Run Project"); bar->addWidget(launch); connect(launch, &QToolButton::clicked, this, [this] { runProjectCommand(false); }); + auto *contextSpacer = new QWidget(contextBar); + contextSpacer->setSizePolicy(QSizePolicy::Expanding, + QSizePolicy::Preferred); + contextBar->addWidget(contextSpacer); + auto addPanelToggle = [this, contextBar](const QString &id, + const QString &text, + styling::Icon icon) { + ads::CDockWidget *dock = dockManager->panel(id); + if (dock == nullptr) + return; + auto *button = new QToolButton(contextBar); + button->setObjectName("workspacePanelButton"); + button->setText(text); + button->setIcon(styling::icon(icon, "#8490A4")); + button->setCheckable(true); + button->setChecked(dock->isVisible()); + contextBar->addWidget(button); + connect(button, &QToolButton::toggled, dock, + [dock](bool visible) { dock->toggleView(visible); }); + connect(dock, &ads::CDockWidget::viewToggled, button, + [button](bool visible) { + const QSignalBlocker blocker(button); + button->setChecked(visible); + }); + }; + addPanelToggle("hierarchy", "Scene", styling::Icon::TreeStructure); + addPanelToggle("fileExplorer", "Assets", styling::Icon::FolderOpen); + addPanelToggle("inspector", "Inspector", + styling::Icon::SlidersHorizontal); + statusBar()->setObjectName("atlasStatusBar"); statusBar()->showMessage("Ready"); - auto *runtimeIcon = new QLabel(statusBar()); + auto *runtimeStatus = new QWidget(statusBar()); + runtimeStatus->setObjectName("statusRuntime"); + auto *runtimeLayout = new QHBoxLayout(runtimeStatus); + runtimeLayout->setContentsMargins(5, 0, 7, 0); + runtimeLayout->setSpacing(4); + auto *runtimeIcon = new QLabel(runtimeStatus); runtimeIcon->setObjectName("statusRuntimeIcon"); runtimeIcon->setPixmap( styling::icon(styling::Icon::Check, "#849589").pixmap(14, 14)); + auto *runtimeText = new QLabel("Runtime ready", runtimeStatus); + runtimeText->setObjectName("statusRuntimeText"); + runtimeLayout->addWidget(runtimeIcon); + runtimeLayout->addWidget(runtimeText); auto *renderer = new QLabel(statusBar()); renderer->setObjectName("statusRenderer"); const auto projectInfo = ProjectStore::projectInfo(projectFile); @@ -903,17 +981,19 @@ void EditorWindow::setupWorkspaceBar() { : QStringLiteral("ATLAS")); auto *version = new QLabel(QStringLiteral(ATLAS_VERSION), statusBar()); version->setObjectName("statusVersion"); - statusBar()->addPermanentWidget(runtimeIcon); + statusBar()->addPermanentWidget(runtimeStatus); statusBar()->addPermanentWidget(renderer); statusBar()->addPermanentWidget(version); connect( viewportPanel, &ViewportPanel::runtimeAvailabilityChanged, this, - [this, runtimeIcon](bool available) { + [this, runtimeIcon, runtimeText](bool available) { runtimeIcon->setPixmap( styling::icon(available ? styling::Icon::Check : styling::Icon::Warning, available ? QColor("#849589") : QColor("#A1957D")) .pixmap(14, 14)); + runtimeText->setText(available ? "Runtime ready" + : "Runtime unavailable"); statusBar()->showMessage( available ? "Runtime ready" : "Runtime unavailable", 3000); }); @@ -925,8 +1005,15 @@ void EditorWindow::activateWorkspace(int index) { return; workspaceStack->setCurrentIndex(index); if (workspaceModeGroup != nullptr) { - if (auto *button = workspaceModeGroup->button(index)) + if (auto *button = workspaceModeGroup->button(index)) { button->setChecked(true); + if (auto *icon = findChild("workspaceContextIcon")) + icon->setPixmap(button->icon().pixmap(15, 15)); + if (auto *title = findChild("workspaceContextTitle")) + title->setText(button->text()); + if (auto *path = findChild("workspaceContextPath")) + path->setText(button->property("workspacePath").toString()); + } } scheduleLayoutSave(); } @@ -1782,9 +1869,54 @@ bool EditorWindow::contentBrowserHasFocus() const { (focused == contentBrowser || contentBrowser->isAncestorOf(focused)); } +void EditorWindow::undoActiveEditor() { + if (auto *field = qobject_cast(QApplication::focusWidget())) { + field->undo(); + } else if (materialEditorPanel != nullptr && + materialEditorPanel->isAncestorOf(QApplication::focusWidget())) { + materialEditorPanel->undo(); + } else if (graphiteEditorPanel != nullptr && + graphiteEditorPanel->isAncestorOf(QApplication::focusWidget())) { + graphiteEditorPanel->undo(); + } else if (viewportPanel != nullptr) { + viewportPanel->undo(); + } +} + +void EditorWindow::redoActiveEditor() { + if (auto *field = qobject_cast(QApplication::focusWidget())) { + field->redo(); + } else if (materialEditorPanel != nullptr && + materialEditorPanel->isAncestorOf(QApplication::focusWidget())) { + materialEditorPanel->redo(); + } else if (graphiteEditorPanel != nullptr && + graphiteEditorPanel->isAncestorOf(QApplication::focusWidget())) { + graphiteEditorPanel->redo(); + } else if (viewportPanel != nullptr) { + viewportPanel->redo(); + } +} + bool EditorWindow::eventFilter(QObject *watched, QEvent *event) { + if (event->type() == QEvent::Polish) { + if (auto *menu = qobject_cast(watched)) { + menu->setAttribute(Qt::WA_TranslucentBackground); + } else if (auto *dialog = qobject_cast(watched); + dialog != nullptr && + dialog->windowFlags().testFlag(Qt::FramelessWindowHint)) { + dialog->setAttribute(Qt::WA_TranslucentBackground); + } + } if (event->type() == QEvent::KeyPress) { auto *key = static_cast(event); + if (!key->isAutoRepeat() && key->matches(QKeySequence::Undo)) { + undoActiveEditor(); + return true; + } + if (!key->isAutoRepeat() && key->matches(QKeySequence::Redo)) { + redoActiveEditor(); + return true; + } QWidget *focused = QApplication::focusWidget(); const bool typing = qobject_cast(focused) != nullptr; if (!typing && key->key() == Qt::Key_Tab && diff --git a/editor/views/editor/hierarchy.cpp b/editor/views/editor/hierarchy.cpp index 9865eea6..2b84b611 100644 --- a/editor/views/editor/hierarchy.cpp +++ b/editor/views/editor/hierarchy.cpp @@ -27,6 +27,7 @@ #include #include #include +#include #include #include #include @@ -148,8 +149,7 @@ HierarchyPanel::HierarchyPanel(ViewportPanel *viewport, QWidget *parent) moreButton = new QToolButton(toolbar); moreButton->setObjectName("panelMoreButton"); - moreButton->setIcon( - styling::icon(styling::Icon::DotsVertical, "#8490A4")); + moreButton->setIcon(styling::icon(styling::Icon::DotsVertical, "#8490A4")); moreButton->setPopupMode(QToolButton::InstantPopup); moreButton->setToolTip("Hierarchy actions"); @@ -258,8 +258,7 @@ HierarchyPanel::HierarchyPanel(ViewportPanel *viewport, QWidget *parent) auto *deleteAction = new QAction(this); deleteAction->setShortcuts( - {QKeySequence::Delete, - QKeySequence(Qt::META | Qt::Key_Backspace)}); + {QKeySequence::Delete, QKeySequence(Qt::META | Qt::Key_Backspace)}); deleteAction->setShortcutContext(Qt::WidgetWithChildrenShortcut); connect(deleteAction, &QAction::triggered, this, &HierarchyPanel::deleteSelectedObject); @@ -341,11 +340,11 @@ void HierarchyPanel::applySceneSnapshot(const QString &snapshot) { const int selectedId = scene.value("selectedId").toInt(-1); const QString signature = sceneSignature(sceneName, objects, interfaces); - const bool incompleteModel = - model->rowCount() != 1 || itemsById.size() != objectCount(objects) || - !specialItems.contains("camera") || - !specialItems.contains("environment") || - !specialItems.contains("graphite"); + const bool incompleteModel = model->rowCount() != 1 || + itemsById.size() != objectCount(objects) || + !specialItems.contains("camera") || + !specialItems.contains("environment") || + !specialItems.contains("graphite"); if (signature != lastStructureSignature || incompleteModel) { rebuildScene(sceneName, objects, interfaces, selectedId); lastStructureSignature = signature; @@ -365,7 +364,8 @@ void HierarchyPanel::applySceneSnapshot(const QString &snapshot) { treeView->scrollTo(index, QAbstractItemView::EnsureVisible); selectedSpecialType.clear(); } else if (specialItems.contains(selectedSpecialType)) { - const QModelIndex index = specialItems.value(selectedSpecialType)->index(); + const QModelIndex index = + specialItems.value(selectedSpecialType)->index(); treeView->setCurrentIndex(index); treeView->scrollTo(index, QAbstractItemView::EnsureVisible); } else { @@ -377,7 +377,8 @@ void HierarchyPanel::applySceneSnapshot(const QString &snapshot) { void HierarchyPanel::rebuildScene(const QString &sceneName, const QJsonArray &objects, - const QJsonArray &interfaces, int selectedId) { + const QJsonArray &interfaces, + int selectedId) { applyingSnapshot = true; model->clear(); itemsById.clear(); @@ -398,8 +399,8 @@ void HierarchyPanel::rebuildScene(const QString &sceneName, specialItems.insert("camera", mainCamera); root->appendRow(mainCamera); - auto *environment = new QStandardItem( - hierarchyIcon(this, "environment"), "Environment"); + auto *environment = + new QStandardItem(hierarchyIcon(this, "environment"), "Environment"); environment->setData(-1, ObjectIdRole); environment->setData("environment", ObjectTypeRole); environment->setToolTip("Scene atmosphere and environment"); @@ -407,8 +408,8 @@ void HierarchyPanel::rebuildScene(const QString &sceneName, specialItems.insert("environment", environment); root->appendRow(environment); - auto *graphite = new QStandardItem( - hierarchyIcon(this, "graphite"), "Graphite Overlay"); + auto *graphite = + new QStandardItem(hierarchyIcon(this, "graphite"), "Graphite Overlay"); graphite->setData(-1, ObjectIdRole); graphite->setData("graphite", ObjectTypeRole); graphite->setToolTip("Scene UI overlays"); @@ -431,8 +432,8 @@ void HierarchyPanel::rebuildScene(const QString &sceneName, label = source; if (!enabled) label += " (Disabled)"; - auto *asset = new QStandardItem( - hierarchyIcon(this, "graphiteAsset"), label); + auto *asset = + new QStandardItem(hierarchyIcon(this, "graphiteAsset"), label); asset->setData(-1, ObjectIdRole); asset->setData("graphiteAsset", ObjectTypeRole); asset->setData(source, AssetPathRole); @@ -461,6 +462,14 @@ void HierarchyPanel::rebuildScene(const QString &sceneName, } bool HierarchyPanel::eventFilter(QObject *watched, QEvent *event) { + if (treeView != nullptr && watched == treeView->viewport() && + event->type() == QEvent::MouseButtonPress) { + auto *mouse = static_cast(event); + const QModelIndex index = + treeView->indexAt(mouse->position().toPoint()); + draggedObjectId = + index.isValid() ? index.data(ObjectIdRole).toInt() : -1; + } if (treeView != nullptr && watched == treeView->viewport() && (event->type() == QEvent::DragEnter || event->type() == QEvent::DragMove || event->type() == QEvent::Drop)) { @@ -472,11 +481,11 @@ bool HierarchyPanel::eventFilter(QObject *watched, QEvent *event) { QFileInfo(drop->mimeData()->urls().constFirst().toLocalFile()) .suffix() .toLower(); - const bool supported = - suffix == "amat" || suffix == "material" || suffix == "ts" || - suffix == "js" || suffix == "wav" || suffix == "mp3" || - suffix == "ogg" || suffix == "flac" || suffix == "m4a" || - suffix == "aac"; + const bool supported = suffix == "amat" || suffix == "material" || + suffix == "ts" || suffix == "js" || + suffix == "wav" || suffix == "mp3" || + suffix == "ogg" || suffix == "flac" || + suffix == "m4a" || suffix == "aac"; if (supported && event->type() == QEvent::Drop && viewport != nullptr && viewport->attachRuntimeAsset( @@ -495,11 +504,12 @@ bool HierarchyPanel::eventFilter(QObject *watched, QEvent *event) { } if (drop->mimeData()->hasFormat( "application/x-qstandarditemmodeldatalist")) { - const int childId = selectedObjectId(); + const int childId = draggedObjectId; const int parentId = index.isValid() ? objectId : -1; const bool valid = childId >= 0 && childId != parentId; if (valid && event->type() == QEvent::Drop && viewport != nullptr) { if (viewport->setRuntimeObjectParent(childId, parentId)) { + draggedObjectId = -1; drop->setDropAction(Qt::MoveAction); drop->accept(); return true; @@ -555,14 +565,14 @@ void HierarchyPanel::showContextMenu(const QPoint &position) { menu.addSeparator(); menu.addAction(styling::icon(styling::Icon::Crosshair, "#7E929C"), "Focus", this, &HierarchyPanel::focusSelectedObject); - menu.addAction(styling::icon(styling::Icon::File, "#8498A8"), - "Rename", this, &HierarchyPanel::renameSelectedObject); + menu.addAction(styling::icon(styling::Icon::File, "#8498A8"), "Rename", + this, &HierarchyPanel::renameSelectedObject); menu.addAction(styling::icon(styling::Icon::TreeStructure, "#849589"), "Move to Scene Root", this, &HierarchyPanel::moveSelectedObjectToRoot); menu.addSeparator(); - menu.addAction(styling::icon(styling::Icon::Trash, "#A17F7F"), - "Delete", this, &HierarchyPanel::deleteSelectedObject); + menu.addAction(styling::icon(styling::Icon::Trash, "#A17F7F"), "Delete", + this, &HierarchyPanel::deleteSelectedObject); } menu.exec(treeView->viewport()->mapToGlobal(position)); } @@ -661,7 +671,8 @@ QList HierarchyPanel::selectedObjectIds() const { QList ids; if (treeView == nullptr || treeView->selectionModel() == nullptr) return ids; - for (const QModelIndex &index : treeView->selectionModel()->selectedRows()) { + for (const QModelIndex &index : + treeView->selectionModel()->selectedRows()) { bool valid = false; const int id = index.data(ObjectIdRole).toInt(&valid); if (valid && id >= 0 && !ids.contains(id)) @@ -707,8 +718,8 @@ void HierarchyPanel::showCreationPopup() { for (const CreationEntry &entry : creationEntries()) { auto *item = new QListWidgetItem(objects); - item->setText(QStringLiteral("%1 · %2") - .arg(entry.name, entry.category)); + item->setText( + QStringLiteral("%1 · %2").arg(entry.name, entry.category)); item->setIcon(hierarchyIcon(this, entry.type)); item->setData(CreationTypeRole, entry.type); item->setData(CreationNameRole, entry.name); @@ -740,9 +751,8 @@ void HierarchyPanel::showCreationPopup() { firstMatch = index; } noObjects->setHidden(firstMatch >= 0); - objects->setCurrentItem(firstMatch >= 0 - ? objects->item(firstMatch) - : noObjects); + objects->setCurrentItem( + firstMatch >= 0 ? objects->item(firstMatch) : noObjects); }); connect(objects, &QListWidget::itemActivated, &dialog, [this, &dialog](QListWidgetItem *item) { diff --git a/editor/views/editor/inspector.cpp b/editor/views/editor/inspector.cpp index d9ea29d4..bfede19d 100644 --- a/editor/views/editor/inspector.cpp +++ b/editor/views/editor/inspector.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -100,8 +101,7 @@ class PickerSearchField : public QLineEdit { event->accept(); return; } - if (event->key() == Qt::Key_Return || - event->key() == Qt::Key_Enter) { + if (event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter) { const QList actions = selectableActions(); QAction *action = menu->activeAction(); if ((action == nullptr || !actions.contains(action)) && @@ -178,8 +178,7 @@ QIcon inspectorIcon(QWidget *, const QString &type) { return styling::icon(styling::Icon::Folder, "#7E929C"); if (normalized.contains("camera")) return styling::icon(styling::Icon::Camera, "#9E897D"); - if (normalized.contains("environment") || - normalized.contains("atmosphere")) + if (normalized.contains("environment") || normalized.contains("atmosphere")) return styling::icon(styling::Icon::Globe, "#7E929C"); if (normalized.contains("light") || normalized == "sun") return styling::icon(styling::Icon::Lightbulb, "#A1957D"); @@ -188,16 +187,14 @@ QIcon inspectorIcon(QWidget *, const QString &type) { if (normalized.contains("particle")) return styling::icon(styling::Icon::Sparkle, "#8498A8"); if (normalized.contains("audio") || normalized == "wav" || - normalized == "mp3" || normalized == "ogg" || - normalized == "flac") + normalized == "mp3" || normalized == "ogg" || normalized == "flac") return styling::icon(styling::Icon::MusicNote, "#849589"); if (normalized.contains("material")) return styling::icon(styling::Icon::Material, "#9E897D"); - if (normalized == "png" || normalized == "jpg" || - normalized == "jpeg" || normalized == "bmp" || - normalized == "gif" || normalized == "webp" || - normalized == "tif" || normalized == "tiff" || - normalized == "tga" || normalized == "hdr" || normalized == "exr") + if (normalized == "png" || normalized == "jpg" || normalized == "jpeg" || + normalized == "bmp" || normalized == "gif" || normalized == "webp" || + normalized == "tif" || normalized == "tiff" || normalized == "tga" || + normalized == "hdr" || normalized == "exr") return styling::icon(styling::Icon::Image, "#A1957D"); if (normalized.contains("script") || normalized == "ts" || normalized == "js") @@ -253,12 +250,11 @@ QJsonObject environmentSchema() { {"lookupTexture", ""}, {"fog", QJsonObject{{"color", QJsonArray{0.7, 0.78, 1.0}}, {"intensity", 0.0}}}, - {"volumetricLighting", - QJsonObject{{"enabled", false}, - {"density", 0.35}, - {"weight", 0.02}, - {"decay", 0.95}, - {"exposure", 0.7}}}, + {"volumetricLighting", QJsonObject{{"enabled", false}, + {"density", 0.35}, + {"weight", 0.02}, + {"decay", 0.95}, + {"exposure", 0.7}}}, {"lightBloom", QJsonObject{{"radius", 0.01}, {"maxSamples", 6}}}, {"rimLight", QJsonObject{{"intensity", 0.0}, {"color", QJsonArray{1.0, 0.96, 0.86}}}}, @@ -276,34 +272,31 @@ QJsonObject environmentSchema() { {"sunTintStrength", 0.35}, {"moonTintStrength", 0.8}, {"starIntensity", 2.5}, - {"globalLight", - QJsonObject{{"enabled", true}, - {"castsShadows", true}, - {"shadowResolution", 2048}}}, - {"clouds", - QJsonObject{{"enabled", false}, - {"frequency", 4}, - {"divisions", 6}, - {"position", QJsonArray{0.0, 100.0, 0.0}}, - {"size", QJsonArray{500.0, 80.0, 500.0}}, - {"scale", 1.5}, - {"offset", QJsonArray{0.0, 0.0, 0.0}}, - {"density", 0.45}, - {"densityMultiplier", 1.5}, - {"absorption", 1.1}, - {"scattering", 0.85}, - {"phase", 0.55}, - {"clusterStrength", 0.5}, - {"primaryStepCount", 12}, - {"lightStepCount", 6}, - {"lightStepMultiplier", 1.6}, - {"minStepLength", 0.05}, - {"wind", QJsonArray{0.03, 0.0, 0.02}}}}, - {"weather", - QJsonObject{{"enabled", false}, - {"condition", "clear"}, - {"intensity", 0.0}, - {"wind", QJsonArray{0.0, -0.4, 0.0}}}}}}}; + {"globalLight", QJsonObject{{"enabled", true}, + {"castsShadows", true}, + {"shadowResolution", 2048}}}, + {"clouds", QJsonObject{{"enabled", false}, + {"frequency", 4}, + {"divisions", 6}, + {"position", QJsonArray{0.0, 100.0, 0.0}}, + {"size", QJsonArray{500.0, 80.0, 500.0}}, + {"scale", 1.5}, + {"offset", QJsonArray{0.0, 0.0, 0.0}}, + {"density", 0.45}, + {"densityMultiplier", 1.5}, + {"absorption", 1.1}, + {"scattering", 0.85}, + {"phase", 0.55}, + {"clusterStrength", 0.5}, + {"primaryStepCount", 12}, + {"lightStepCount", 6}, + {"lightStepMultiplier", 1.6}, + {"minStepLength", 0.05}, + {"wind", QJsonArray{0.03, 0.0, 0.02}}}}, + {"weather", QJsonObject{{"enabled", false}, + {"condition", "clear"}, + {"intensity", 0.0}, + {"wind", QJsonArray{0.0, -0.4, 0.0}}}}}}}; } QJsonObject vehicleWheelSchema() { @@ -525,6 +518,129 @@ QString componentShape(const QJsonArray &components) { return result; } +QJsonValue valueAtPath(QJsonValue value, const QString &path) { + const QStringList segments = path.split('/', Qt::SkipEmptyParts); + for (QString segment : segments) { + segment.replace("~1", "/").replace("~0", "~"); + if (value.isObject()) { + value = value.toObject().value(segment); + } else if (value.isArray()) { + bool validIndex = false; + const int index = segment.toInt(&validIndex); + const QJsonArray array = value.toArray(); + if (!validIndex || index < 0 || index >= array.size()) + return {}; + value = array.at(index); + } else { + return {}; + } + } + return value; +} + +void tagEditor(QWidget *editor, const QString &path, const QString &kind, + int index = -1) { + editor->setProperty("inspectorPath", path); + editor->setProperty("inspectorValueKind", kind); + if (index >= 0) + editor->setProperty("inspectorValueIndex", index); +} + +bool isEditing(QWidget *editor) { + QWidget *focused = QApplication::focusWidget(); + return focused != nullptr && + (focused == editor || editor->isAncestorOf(focused)); +} + +void refreshTaggedEditors(QFrame *card, const QJsonObject &properties) { + const QList editors = card->findChildren(); + for (QWidget *editor : editors) { + const QString path = editor->property("inspectorPath").toString(); + const QString kind = editor->property("inspectorValueKind").toString(); + if (kind.isEmpty() || isEditing(editor)) + continue; + const QJsonValue value = valueAtPath(properties, path); + if (value.isUndefined()) + continue; + if (kind == "number") { + auto *field = qobject_cast(editor); + if (field != nullptr && value.isDouble()) { + const QSignalBlocker blocker(field); + field->setValue(value.toDouble()); + } + } else if (kind == "vector") { + auto *field = qobject_cast(editor); + const int index = editor->property("inspectorValueIndex").toInt(); + const QJsonArray array = value.toArray(); + if (field != nullptr && index >= 0 && index < array.size()) { + const QSignalBlocker blocker(field); + field->setValue(array.at(index).toDouble()); + } + } else if (kind == "bool") { + auto *field = qobject_cast(editor); + if (field != nullptr && value.isBool()) { + const QSignalBlocker blocker(field); + field->setChecked(value.toBool()); + } + } else if (kind == "choice") { + auto *field = qobject_cast(editor); + if (field != nullptr && value.isString()) { + const QSignalBlocker blocker(field); + field->setCurrentText(value.toString()); + } + } else if (kind == "text") { + auto *field = qobject_cast(editor); + if (field != nullptr && value.isString()) { + const QSignalBlocker blocker(field); + field->setText(value.toString()); + } + } else if (kind == "array") { + auto *field = qobject_cast(editor); + if (field == nullptr || !value.isArray()) + continue; + QStringList entries; + for (const QJsonValue &entry : value.toArray()) { + entries.append(entry.isString() + ? entry.toString() + : QString::number(entry.toDouble())); + } + const QSignalBlocker blocker(field); + field->setText(entries.join(", ")); + } else if (kind == "color") { + const QJsonArray array = value.toArray(); + if (array.size() < 3) + continue; + const bool normalized = + std::all_of(array.begin(), array.end(), [](QJsonValue entry) { + return entry.toDouble() <= 1.0; + }); + const double factor = normalized ? 255.0 : 1.0; + const QColor color( + std::clamp(static_cast(array.at(0).toDouble() * factor), 0, + 255), + std::clamp(static_cast(array.at(1).toDouble() * factor), 0, + 255), + std::clamp(static_cast(array.at(2).toDouble() * factor), 0, + 255), + array.size() > 3 + ? std::clamp( + static_cast(array.at(3).toDouble() * factor), 0, + 255) + : 255); + auto *swatch = editor->findChild(); + auto *text = editor->findChild(); + if (swatch != nullptr) { + swatch->setIcon(styling::colorSwatch(color, QSize(22, 14))); + swatch->setIconSize(QSize(22, 14)); + } + if (text != nullptr) { + const QSignalBlocker blocker(text); + text->setText(color.name(QColor::HexArgb).toUpper()); + } + } + } +} + QStringList choicesFor(const QString &path) { const QString key = path.section('/', -1).toLower(); if (key == "motiontype") @@ -556,8 +672,7 @@ QDoubleSpinBox *numberField(double value, QWidget *parent) { return field; } -QJsonValue adaptedSyncValue(const QJsonValue &source, - const QJsonValue &target, +QJsonValue adaptedSyncValue(const QJsonValue &source, const QJsonValue &target, const QString &path) { if (target.isDouble()) { if (source.isDouble()) @@ -589,9 +704,8 @@ QJsonValue adaptedSyncValue(const QJsonValue &source, const QJsonArray values = source.toArray(); for (int index = 0; index < dimensions; ++index) { result.append(index < values.size() ? values.at(index) - : values.isEmpty() - ? QJsonValue(0.0) - : values.last()); + : values.isEmpty() ? QJsonValue(0.0) + : values.last()); } return result; } @@ -603,11 +717,10 @@ void setNumericEditorValue(QWidget *editor, const QJsonValue &value) { const QJsonArray values = value.toArray(); for (int index = 0; index < fields.size(); ++index) { QSignalBlocker blocker(fields.at(index)); - fields.at(index)->setValue(value.isDouble() - ? value.toDouble() - : index < values.size() - ? values.at(index).toDouble() - : 0.0); + fields.at(index)->setValue(value.isDouble() ? value.toDouble() + : index < values.size() + ? values.at(index).toDouble() + : 0.0); } } @@ -632,10 +745,9 @@ void addSyncPicker(QHBoxLayout *layout, const QString &path, : QSizePolicy::Fixed, QSizePolicy::Preferred); button->setMinimumWidth(matched ? 180 : 22); - button->setToolTip(matched - ? QStringLiteral("Matched to %1. Click to change") - .arg(name) - : "Match this value with another property"); + button->setToolTip( + matched ? QStringLiteral("Matched to %1. Click to change").arg(name) + : "Match this value with another property"); button->style()->unpolish(button); button->style()->polish(button); }; @@ -652,17 +764,16 @@ void addSyncPicker(QHBoxLayout *layout, const QString &path, menu->addSeparator(); auto searchable = std::make_shared>(); auto generated = std::make_shared>(); - QObject::connect(search, &QLineEdit::textChanged, menu, - [searchable](const QString &text) { - const QString query = text.trimmed().toLower(); - for (QAction *action : *searchable) { - action->setVisible( - query.isEmpty() || - action->property("searchText") - .toString() - .contains(query)); - } - }); + QObject::connect( + search, &QLineEdit::textChanged, menu, + [searchable](const QString &text) { + const QString query = text.trimmed().toLower(); + for (QAction *action : *searchable) { + action->setVisible( + query.isEmpty() || + action->property("searchText").toString().contains(query)); + } + }); QObject::connect( menu, &QMenu::aboutToShow, search, [menu, search, searchable, generated, provider, current, path, changed, @@ -673,13 +784,12 @@ void addSyncPicker(QHBoxLayout *layout, const QString &path, } generated->clear(); searchable->clear(); - QAction *manual = menu->addAction( - "Enter value manually", menu, - [provider, path, showMatch] { - if (provider.clearMatch) - provider.clearMatch(path); - showMatch(QString()); - }); + QAction *manual = menu->addAction("Enter value manually", menu, + [provider, path, showMatch] { + if (provider.clearMatch) + provider.clearMatch(path); + showMatch(QString()); + }); manual->setProperty("searchText", "enter value manually unlink"); searchable->append(manual); generated->append(manual); @@ -705,8 +815,7 @@ void addSyncPicker(QHBoxLayout *layout, const QString &path, generated->append(action); } if (options.isEmpty()) { - QAction *empty = - menu->addAction("No compatible properties"); + QAction *empty = menu->addAction("No compatible properties"); empty->setEnabled(false); generated->append(empty); } @@ -752,10 +861,11 @@ QWidget *vectorField(const QJsonArray &value, const PropertyChanged &changed, QList boxes; for (int index = 0; index < dimensions; ++index) { auto *axis = new QLabel(axes.at(index), valueEditor); - axis->setObjectName("inspectorAxisLabel"); + axis->setObjectName("inspectorAxis" + axes.at(index)); auto *box = numberField(values.at(index).toDouble(), valueEditor); box->setButtonSymbols(QAbstractSpinBox::NoButtons); - box->setMinimumWidth(38); + box->setMinimumWidth(52); + tagEditor(box, path, "vector", index); boxes.append(box); valueLayout->addWidget(axis); valueLayout->addWidget(box, 1); @@ -779,6 +889,7 @@ QWidget *colorField(const QJsonArray &value, const PropertyChanged &changed, const QString &path, QWidget *parent) { auto *field = new QFrame(parent); field->setObjectName("inspectorColorField"); + tagEditor(field, path, "color"); auto *layout = new QHBoxLayout(field); layout->setContentsMargins(3, 2, 3, 2); layout->setSpacing(5); @@ -851,6 +962,7 @@ QWidget *primitiveField(const QString &name, const QString &path, const SyncProvider &syncProvider, QWidget *parent) { if (value.isBool()) { auto *field = new QCheckBox(parent); + tagEditor(field, path, "bool"); field->setChecked(value.toBool()); QObject::connect( field, &QCheckBox::toggled, parent, @@ -864,13 +976,13 @@ QWidget *primitiveField(const QString &name, const QString &path, layout->setContentsMargins(0, 0, 0, 0); layout->setSpacing(2); auto *field = numberField(value.toDouble(), container); + tagEditor(field, path, "number"); layout->addWidget(field, 1); addSyncPicker(layout, path, value, changed, syncProvider, field, container); - QObject::connect(field, &QDoubleSpinBox::valueChanged, container, - [field, changed, path](double) { - changed(path, field->value()); - }); + QObject::connect( + field, &QDoubleSpinBox::valueChanged, container, + [field, changed, path](double) { changed(path, field->value()); }); return container; } if (value.isArray()) { @@ -878,11 +990,11 @@ QWidget *primitiveField(const QString &name, const QString &path, if (isColorProperty(name, array)) { return colorField(array, changed, path, parent); } - if ((array.size() == 2 || array.size() == 3) && - isNumericArray(array)) { + if ((array.size() == 2 || array.size() == 3) && isNumericArray(array)) { return vectorField(array, changed, path, syncProvider, parent); } auto *field = new QLineEdit(parent); + tagEditor(field, path, "array"); QStringList entries; for (const QJsonValue &entry : array) { entries.append(entry.isString() @@ -910,6 +1022,7 @@ QWidget *primitiveField(const QString &name, const QString &path, const QStringList choices = choicesFor(path); if (!choices.isEmpty()) { auto *field = new QComboBox(parent); + tagEditor(field, path, "choice"); field->addItems(choices); field->setCurrentText(value.toString()); QObject::connect( @@ -918,6 +1031,7 @@ QWidget *primitiveField(const QString &name, const QString &path, return field; } auto *field = new QLineEdit(value.toString(), parent); + tagEditor(field, path, "text"); connectLiveText(field, [field, changed, path] { changed(path, field->text()); }); return field; @@ -931,8 +1045,8 @@ QFrame *propertyRow(const QString &label, QWidget *editor, QWidget *parent) { layout->setSpacing(8); auto *name = new QLabel(label, row); name->setObjectName("inspectorPropertyLabel"); - name->setMinimumWidth(104); - name->setMaximumWidth(128); + name->setMinimumWidth(82); + name->setMaximumWidth(108); layout->addWidget(name); layout->addWidget(editor, 1); return row; @@ -979,8 +1093,7 @@ void addPropertyRows(QVBoxLayout *layout, const QJsonObject &properties, title->setObjectName("inspectorNestedTitle"); auto *add = new QToolButton(heading); add->setObjectName("inspectorArrayButton"); - add->setIcon( - styling::icon(styling::Icon::Plus, "#8498A8")); + add->setIcon(styling::icon(styling::Icon::Plus, "#8498A8")); add->setToolTip("Add item"); add->setToolTip(QStringLiteral("Add %1").arg(humanize(key))); headingLayout->addWidget(title, 1); @@ -1032,9 +1145,8 @@ void addPropertyRows(QVBoxLayout *layout, const QJsonObject &properties, continue; } } - QWidget *editor = - primitiveField(key, nextPath, iterator.value(), changed, - syncProvider, parent); + QWidget *editor = primitiveField(key, nextPath, iterator.value(), + changed, syncProvider, parent); layout->addWidget(propertyRow(humanize(key), editor, parent)); } } @@ -1127,7 +1239,7 @@ SyncProvider bindSyncProvider(const SyncProvider &provider, return QString(); }; bound.setMatch = [viewport, target](const QString &path, - const QJsonObject &source) { + const QJsonObject &source) { if (viewport != nullptr) viewport->setRuntimePropertySync(syncTargetAtPath(target, path), source); @@ -1151,9 +1263,11 @@ QJsonValue objectSyncReference(const QJsonObject &object) { QFrame *componentCard(const QString &title, const QJsonObject &properties, const QString &path, const PropertyChanged &changed, QWidget *parent, const SyncProvider &syncProvider = {}, - const std::function &remove = {}) { + const std::function &remove = {}, + const QString &scope = {}) { auto *card = new QFrame(parent); card->setObjectName("inspectorComponent"); + card->setProperty("inspectorScope", scope); auto *layout = new QVBoxLayout(card); layout->setContentsMargins(0, 0, 0, 7); layout->setSpacing(2); @@ -1173,8 +1287,7 @@ QFrame *componentCard(const QString &title, const QJsonObject &properties, if (remove) { auto *removeButton = new QToolButton(headerRow); removeButton->setObjectName("inspectorComponentRemoveButton"); - removeButton->setIcon( - styling::icon(styling::Icon::Trash, "#A17F7F")); + removeButton->setIcon(styling::icon(styling::Icon::Trash, "#A17F7F")); removeButton->setToolTip(QStringLiteral("Remove %1").arg(title)); headerLayout->addWidget(removeButton); QObject::connect(removeButton, &QToolButton::clicked, card, remove); @@ -1190,9 +1303,9 @@ QFrame *componentCard(const QString &title, const QJsonObject &properties, QObject::connect( header, &QToolButton::toggled, card, [header, body](bool expanded) { body->setVisible(expanded); - header->setIcon(styling::icon( - expanded ? styling::Icon::CaretDown : styling::Icon::CaretRight, - "#8490A4")); + header->setIcon(styling::icon(expanded ? styling::Icon::CaretDown + : styling::Icon::CaretRight, + "#8490A4")); }); return card; } @@ -1215,7 +1328,7 @@ InspectorPanel::InspectorPanel(ViewportPanel *viewport, const QString &projectFile, QWidget *parent) : QWidget(parent), viewport(viewport) { setObjectName("inspectorPanel"); - setMinimumWidth(360); + setMinimumWidth(400); setAcceptDrops(true); const QFileInfo projectInfo(projectFile); projectRoot = projectInfo.absoluteDir().absolutePath(); @@ -1235,7 +1348,21 @@ InspectorPanel::InspectorPanel(ViewportPanel *viewport, if (viewport != nullptr) { connect(viewport, &ViewportPanel::sceneSnapshotChanged, this, &InspectorPanel::applySceneSnapshot); + QTimer::singleShot(0, this, [this] { + const QString snapshot = this->viewport->currentSceneSnapshot(); + if (!snapshot.isEmpty()) + applySceneSnapshot(snapshot); + }); } + connect(qApp, &QApplication::focusChanged, this, + [this](QWidget *previous, QWidget *current) { + if (previous == nullptr || !isAncestorOf(previous) || + previous == current || fileTarget || cameraTarget || + environmentTarget || inspectedObjectId < 0) { + return; + } + refreshObjectEditors(inspectedObject); + }); showEmptyState(); } @@ -1246,29 +1373,96 @@ void InspectorPanel::applySceneSnapshot(const QString &snapshot) { if (error.error != QJsonParseError::NoError || !document.isObject()) return; scene = document.object(); - if (environmentTarget) + if (environmentTarget) { + QJsonObject values = mergeObjects( + environmentSchema(), scene.value("environment").toObject()); + QJsonObject atmosphere = values.take("atmosphere").toObject(); + QJsonObject globalLight = atmosphere.take("globalLight").toObject(); + QJsonObject clouds = atmosphere.take("clouds").toObject(); + QJsonObject weather = atmosphere.take("weather").toObject(); + const QList cards = content->findChildren(); + for (QFrame *card : cards) { + if (card->objectName() != "inspectorComponent") + continue; + const QString scope = card->property("inspectorScope").toString(); + if (scope == "environment") + refreshTaggedEditors(card, values); + else if (scope == "environment:atmosphere") + refreshTaggedEditors(card, atmosphere); + else if (scope == "environment:globalLight") + refreshTaggedEditors(card, globalLight); + else if (scope == "environment:clouds") + refreshTaggedEditors(card, clouds); + else if (scope == "environment:weather") + refreshTaggedEditors(card, weather); + } return; + } if (cameraTarget) { inspectedCamera = scene.value("camera").toObject(); + const QJsonObject transform{ + {"position", inspectedCamera.value("position")}, + {"target", inspectedCamera.value("target")}}; + const QJsonObject projection{ + {"orthographic", inspectedCamera.value("orthographic")}, + {"fov", inspectedCamera.value("fov")}, + {"orthoSize", inspectedCamera.value("orthoSize")}, + {"nearClip", inspectedCamera.value("nearClip")}, + {"farClip", inspectedCamera.value("farClip")}}; + const QJsonObject focus{ + {"focusDepth", inspectedCamera.value("focusDepth")}, + {"focusRange", inspectedCamera.value("focusRange")}}; + const QJsonObject controls{ + {"movementSpeed", inspectedCamera.value("movementSpeed")}, + {"mouseSensitivity", inspectedCamera.value("mouseSensitivity")}, + {"controllerLookSensitivity", + inspectedCamera.value("controllerLookSensitivity")}, + {"lookSmoothness", inspectedCamera.value("lookSmoothness")}, + {"automaticMoving", inspectedCamera.value("automaticMoving")}, + {"actions", inspectedCamera.value("actions").isArray() + ? inspectedCamera.value("actions") + : QJsonValue(QJsonArray{})}}; + const QList cards = content->findChildren(); + for (QFrame *card : cards) { + if (card->objectName() != "inspectorComponent") + continue; + const QString scope = card->property("inspectorScope").toString(); + if (scope == "camera:transform") + refreshTaggedEditors(card, transform); + else if (scope == "camera:projection") + refreshTaggedEditors(card, projection); + else if (scope == "camera:focus") + refreshTaggedEditors(card, focus); + else if (scope == "camera:controls") + refreshTaggedEditors(card, controls); + } return; } const int selected = scene.value("selectedId").toInt(-1); const bool selectionChanged = selected != lastRuntimeSelection; lastRuntimeSelection = selected; - if (selectionChanged) { + if (selectionChanged || + (!fileTarget && selected >= 0 && inspectedObjectId != selected)) { inspectRuntimeObject(selected); } else if (!fileTarget && inspectedObjectId >= 0) { const QJsonObject updated = findObject(inspectedObjectId); - const bool contentChanged = - updated.value("name") != inspectedObject.value("name") || - updated.value("properties").toObject().value("material") != - inspectedObject.value("properties").toObject().value( - "material") || + if (updated.isEmpty()) { + inspectedObject = {}; + inspectedObjectId = -1; + showEmptyState(); + return; + } + const bool structureChanged = + updated.value("type") != inspectedObject.value("type") || + jsonShape(updated.value("properties")) != + jsonShape(inspectedObject.value("properties")) || componentShape(updated.value("components").toArray()) != componentShape(inspectedObject.value("components").toArray()); inspectedObject = updated; - if (contentChanged) { + if (structureChanged) { showObject(inspectedObject); + } else { + refreshObjectEditors(inspectedObject); } } } @@ -1280,6 +1474,7 @@ void InspectorPanel::inspectRuntimeObject(int id) { inspectedFile.clear(); inspectedCamera = {}; inspectedObjectId = id; + lastRuntimeSelection = id; inspectedObject = findObject(id); if (inspectedObject.isEmpty()) { showEmptyState(); @@ -1288,6 +1483,53 @@ void InspectorPanel::inspectRuntimeObject(int id) { } } +void InspectorPanel::refreshObjectEditors(const QJsonObject &object) { + if (nameField != nullptr && !isEditing(nameField)) { + const QSignalBlocker blocker(nameField); + nameField->setText(object.value("name").toString("Object")); + } + const QString type = object.value("type").toString("Object"); + QJsonObject objectProperties = object.value("properties").toObject(); + if (type.contains("light", Qt::CaseInsensitive) || + type.compare("sun", Qt::CaseInsensitive) == 0) { + objectProperties = mergeObjects(lightSchema(type), objectProperties); + } + const QString materialPath = objectProperties.value("material").toString(); + objectProperties.remove("material"); + const QStringList hidden{"id", "name", "type", + "position", "rotation", "scale", + "parent", "components", "objects"}; + for (const QString &key : hidden) + objectProperties.remove(key); + const QJsonObject transform{{"position", object.value("position")}, + {"rotation", object.value("rotation")}, + {"scale", object.value("scale")}}; + const QJsonArray components = object.value("components").toArray(); + const QList cards = content->findChildren(); + for (QFrame *card : cards) { + if (card->objectName() != "inspectorComponent") + continue; + const QString scope = card->property("inspectorScope").toString(); + if (scope == "transform") { + refreshTaggedEditors(card, transform); + } else if (scope == "object") { + refreshTaggedEditors(card, objectProperties); + } else if (scope == "material") { + refreshTaggedEditors(card, + QJsonObject{{"source", materialPath}}); + } else if (scope.startsWith("component:")) { + bool validIndex = false; + const int index = scope.section(':', 1, 1).toInt(&validIndex); + if (!validIndex || index < 0 || index >= components.size()) + continue; + const QJsonObject component = components.at(index).toObject(); + refreshTaggedEditors( + card, componentValues(component.value("type").toString(), + component)); + } + } +} + void InspectorPanel::inspectCamera() { fileTarget = false; cameraTarget = true; @@ -1393,37 +1635,34 @@ void InspectorPanel::showObject(const QJsonObject &object) { const QJsonValue objectReference = objectSyncReference(object); const QJsonArray components = object.value("components").toArray(); SyncOptions syncOptions; - syncOptions.append( - {"Object Size", object.value("boundsSize"), - QJsonObject{{"section", "object"}, - {"object", objectReference}, - {"component", "bounds"}, - {"componentIndex", -1}, - {"path", QString()}}}); - collectSyncOptions( - "Transform", transform, - QJsonObject{{"section", "object"}, - {"object", objectReference}, - {"component", "transform"}, - {"componentIndex", -1}}, - QString(), syncOptions); - collectSyncOptions( - "Object", object.value("properties"), - QJsonObject{{"section", "object"}, - {"object", objectReference}, - {"component", "object"}, - {"componentIndex", -1}}, - QString(), syncOptions); + syncOptions.append({"Object Size", object.value("boundsSize"), + QJsonObject{{"section", "object"}, + {"object", objectReference}, + {"component", "bounds"}, + {"componentIndex", -1}, + {"path", QString()}}}); + collectSyncOptions("Transform", transform, + QJsonObject{{"section", "object"}, + {"object", objectReference}, + {"component", "transform"}, + {"componentIndex", -1}}, + QString(), syncOptions); + collectSyncOptions("Object", object.value("properties"), + QJsonObject{{"section", "object"}, + {"object", objectReference}, + {"component", "object"}, + {"componentIndex", -1}}, + QString(), syncOptions); for (int index = 0; index < components.size(); ++index) { const QJsonObject raw = components.at(index).toObject(); const QString componentType = raw.value("type").toString("component"); - collectSyncOptions( - componentTitle(componentType), componentValues(componentType, raw), - QJsonObject{{"section", "object"}, - {"object", objectReference}, - {"component", componentType}, - {"componentIndex", index}}, - QString(), syncOptions); + collectSyncOptions(componentTitle(componentType), + componentValues(componentType, raw), + QJsonObject{{"section", "object"}, + {"object", objectReference}, + {"component", componentType}, + {"componentIndex", index}}, + QString(), syncOptions); } const SyncProvider syncProvider = makeSyncProvider(syncOptions); const QJsonObject transformTarget{{"section", "object"}, @@ -1435,8 +1674,9 @@ void InspectorPanel::showObject(const QJsonObject &object) { [update](const QString &path, const QJsonValue &value) { update("transform", -1, path, value); }, - content, bindSyncProvider(syncProvider, viewport, &scene, - transformTarget))); + content, + bindSyncProvider(syncProvider, viewport, &scene, transformTarget), {}, + "transform")); QJsonObject objectProperties = object.value("properties").toObject(); if (type.contains("light", Qt::CaseInsensitive) || @@ -1457,12 +1697,12 @@ void InspectorPanel::showObject(const QJsonObject &object) { update("object", -1, path, value); }, content, - bindSyncProvider( - syncProvider, viewport, &scene, - QJsonObject{{"section", "object"}, - {"object", objectReference}, - {"component", "object"}, - {"componentIndex", -1}}))); + bindSyncProvider(syncProvider, viewport, &scene, + QJsonObject{{"section", "object"}, + {"object", objectReference}, + {"component", "object"}, + {"componentIndex", -1}}), + {}, "object")); } if (!materialPath.isEmpty()) { @@ -1474,7 +1714,7 @@ void InspectorPanel::showObject(const QJsonObject &object) { viewport->applyRuntimeMaterial(objectId, value.toString()); } }, - content)); + content, {}, {}, "material")); } for (int index = 0; index < components.size(); ++index) { @@ -1488,12 +1728,11 @@ void InspectorPanel::showObject(const QJsonObject &object) { update(componentType, index, path, value); }, content, - bindSyncProvider( - syncProvider, viewport, &scene, - QJsonObject{{"section", "object"}, - {"object", objectReference}, - {"component", componentType}, - {"componentIndex", index}}), + bindSyncProvider(syncProvider, viewport, &scene, + QJsonObject{{"section", "object"}, + {"object", objectReference}, + {"component", componentType}, + {"componentIndex", index}}), [this, objectId, index, componentType] { if (QMessageBox::question( this, "Remove Component", @@ -1503,12 +1742,12 @@ void InspectorPanel::showObject(const QJsonObject &object) { return; } if (viewport == nullptr || - !viewport->removeRuntimeObjectComponent(objectId, - index)) { + !viewport->removeRuntimeObjectComponent(objectId, index)) { QMessageBox::warning(this, "Remove Component", "The component could not be removed."); } - })); + }, + QStringLiteral("component:%1").arg(index))); if (componentType.toLower().remove('_').remove('-') == "audioplayer") { auto *controls = new QFrame(content); controls->setObjectName("inspectorAudioControls"); @@ -1517,23 +1756,22 @@ void InspectorPanel::showObject(const QJsonObject &object) { controlsLayout->setSpacing(5); const QStringList audioActions{"Play", "Pause", "Stop"}; const QList audioIcons{ - styling::Icon::Play, styling::Icon::Pause, - styling::Icon::Stop}; + styling::Icon::Play, styling::Icon::Pause, styling::Icon::Stop}; const QList audioColors{ QColor("#849589"), QColor("#A1957D"), QColor("#A17F7F")}; for (int actionIndex = 0; actionIndex < audioActions.size(); ++actionIndex) { const QString &action = audioActions.at(actionIndex); auto *button = new QToolButton(controls); - button->setIcon(styling::icon( - audioIcons.at(actionIndex), audioColors.at(actionIndex))); + button->setIcon(styling::icon(audioIcons.at(actionIndex), + audioColors.at(actionIndex))); button->setToolTip(action); controlsLayout->addWidget(button); connect(button, &QToolButton::clicked, this, [this, objectId, index, action] { if (viewport != nullptr) { - viewport->controlRuntimeAudio( - objectId, index, action.toLower()); + viewport->controlRuntimeAudio(objectId, index, + action.toLower()); } }); } @@ -1543,15 +1781,15 @@ void InspectorPanel::showObject(const QJsonObject &object) { } auto *addComponent = new QToolButton(content); addComponent->setObjectName("inspectorAddComponentButton"); - addComponent->setIcon( - styling::icon(styling::Icon::Plus, "#8498A8")); + addComponent->setIcon(styling::icon(styling::Icon::Plus, "#8498A8")); addComponent->setText("Add Component"); addComponent->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); addComponent->setPopupMode(QToolButton::InstantPopup); auto *componentMenu = new QMenu(addComponent); auto *searchAction = new QWidgetAction(componentMenu); auto *componentSearch = new PickerSearchField(componentMenu); - componentSearch->setPlaceholderText("Search components, scripts, materials"); + componentSearch->setPlaceholderText( + "Search components, scripts, materials"); componentSearch->setClearButtonEnabled(true); componentSearch->setMinimumWidth(280); searchAction->setDefaultWidget(componentSearch); @@ -1592,16 +1830,15 @@ void InspectorPanel::showObject(const QJsonObject &object) { searchableActions.append(action); } QDirIterator assets(projectRoot, - {"*.ts", "*.amat", "*.material", "*.wav", - "*.mp3", "*.ogg", "*.flac", "*.m4a", - "*.aac"}, + {"*.ts", "*.amat", "*.material", "*.wav", "*.mp3", + "*.ogg", "*.flac", "*.m4a", "*.aac"}, QDir::Files, QDirIterator::Subdirectories); while (assets.hasNext()) { const QFileInfo info(assets.next()); const QString suffix = info.suffix().toLower(); if (suffix == "ts") { - QString relativePath = QDir(projectRoot).relativeFilePath( - info.absoluteFilePath()); + QString relativePath = + QDir(projectRoot).relativeFilePath(info.absoluteFilePath()); const QStringList pathParts = QDir::fromNativeSeparators(relativePath) .split('/', Qt::SkipEmptyParts); @@ -1622,28 +1859,31 @@ void InspectorPanel::showObject(const QJsonObject &object) { suffix == "ogg" || suffix == "flac" || suffix == "m4a" || suffix == "aac"; const QString label = - QStringLiteral("%1 · %2") - .arg(material ? "Material" : audio ? "Audio" : "Script", - info.completeBaseName()); + QStringLiteral("%1 · %2").arg(material ? "Material" + : audio ? "Audio" + : "Script", + info.completeBaseName()); QAction *action = componentMenu->addAction( label, this, [this, objectId, path = info.absoluteFilePath()] { attachAsset(path, objectId); }); - action->setIcon(inspectorIcon( - this, material ? "material" : audio ? "audio" : "script")); + action->setIcon(inspectorIcon(this, material ? "material" + : audio ? "audio" + : "script")); action->setProperty("searchText", (label + ' ' + info.absoluteFilePath()).toLower()); searchableActions.append(action); } - connect(componentSearch, &QLineEdit::textChanged, componentMenu, - [searchableActions](const QString &text) { - const QString query = text.trimmed().toLower(); - for (QAction *action : searchableActions) { - action->setVisible( - query.isEmpty() || - action->property("searchText").toString().contains(query)); - } - }); + connect( + componentSearch, &QLineEdit::textChanged, componentMenu, + [searchableActions](const QString &text) { + const QString query = text.trimmed().toLower(); + for (QAction *action : searchableActions) { + action->setVisible( + query.isEmpty() || + action->property("searchText").toString().contains(query)); + } + }); connect(componentMenu, &QMenu::aboutToShow, componentSearch, [componentSearch] { componentSearch->clear(); @@ -1733,28 +1973,26 @@ void InspectorPanel::showCamera() { QJsonObject{{"section", "camera"}}, QString(), syncOptions); const SyncProvider syncProvider = makeSyncProvider(syncOptions); - contentLayout->addWidget(componentCard("Transform", transform, QString(), - update, content, - bindSyncProvider( - syncProvider, viewport, &scene, - QJsonObject{{"section", - "camera"}}))); - contentLayout->addWidget(componentCard( - "Projection", projection, QString(), - update, - content, - bindSyncProvider(syncProvider, viewport, &scene, - QJsonObject{{"section", "camera"}}))); + contentLayout->addWidget( + componentCard("Transform", transform, QString(), update, content, + bindSyncProvider(syncProvider, viewport, &scene, + QJsonObject{{"section", "camera"}}), + {}, "camera:transform")); + contentLayout->addWidget( + componentCard("Projection", projection, QString(), update, content, + bindSyncProvider(syncProvider, viewport, &scene, + QJsonObject{{"section", "camera"}}), + {}, "camera:projection")); contentLayout->addWidget( componentCard("Depth of Field", focus, QString(), update, content, bindSyncProvider(syncProvider, viewport, &scene, - QJsonObject{{"section", "camera"}}))); - contentLayout->addWidget(componentCard("Camera Controls", controls, - QString(), update, content, - bindSyncProvider( - syncProvider, viewport, &scene, - QJsonObject{{"section", - "camera"}}))); + QJsonObject{{"section", "camera"}}), + {}, "camera:focus")); + contentLayout->addWidget( + componentCard("Camera Controls", controls, QString(), update, content, + bindSyncProvider(syncProvider, viewport, &scene, + QJsonObject{{"section", "camera"}}), + {}, "camera:controls")); contentLayout->addStretch(); } @@ -1784,8 +2022,8 @@ void InspectorPanel::showEnvironment() { headerLayout->addWidget(identity, 1); contentLayout->addWidget(header); - QJsonObject values = mergeObjects( - environmentSchema(), scene.value("environment").toObject()); + QJsonObject values = mergeObjects(environmentSchema(), + scene.value("environment").toObject()); SyncOptions syncOptions; collectSyncOptions("Environment", values, QJsonObject{{"section", "environment"}}, QString(), @@ -1798,8 +2036,8 @@ void InspectorPanel::showEnvironment() { auto update = [this](const QString &prefix, const QString &path, const QJsonValue &value) { if (viewport != nullptr) - viewport->setRuntimeSceneProperty("environment", -1, - prefix + path, value); + viewport->setRuntimeSceneProperty("environment", -1, prefix + path, + value); }; contentLayout->addWidget(componentCard( "Environment", values, QString(), @@ -1808,16 +2046,18 @@ void InspectorPanel::showEnvironment() { }, content, bindSyncProvider(syncProvider, viewport, &scene, - QJsonObject{{"section", "environment"}}))); + QJsonObject{{"section", "environment"}}), + {}, "environment")); contentLayout->addWidget(componentCard( "Atmosphere", atmosphere, QString(), [update](const QString &path, const QJsonValue &value) { update("/atmosphere", path, value); }, content, - bindSyncProvider(syncProvider, viewport, &scene, - QJsonObject{{"section", "environment"}, - {"path", "/atmosphere"}}))); + bindSyncProvider( + syncProvider, viewport, &scene, + QJsonObject{{"section", "environment"}, {"path", "/atmosphere"}}), + {}, "environment:atmosphere")); contentLayout->addWidget(componentCard( "Global Light", globalLight, QString(), [update](const QString &path, const QJsonValue &value) { @@ -1826,7 +2066,8 @@ void InspectorPanel::showEnvironment() { content, bindSyncProvider(syncProvider, viewport, &scene, QJsonObject{{"section", "environment"}, - {"path", "/atmosphere/globalLight"}}))); + {"path", "/atmosphere/globalLight"}}), + {}, "environment:globalLight")); contentLayout->addWidget(componentCard( "Clouds", clouds, QString(), [update](const QString &path, const QJsonValue &value) { @@ -1835,7 +2076,8 @@ void InspectorPanel::showEnvironment() { content, bindSyncProvider(syncProvider, viewport, &scene, QJsonObject{{"section", "environment"}, - {"path", "/atmosphere/clouds"}}))); + {"path", "/atmosphere/clouds"}}), + {}, "environment:clouds")); contentLayout->addWidget(componentCard( "Weather", weather, QString(), [update](const QString &path, const QJsonValue &value) { @@ -1844,7 +2086,8 @@ void InspectorPanel::showEnvironment() { content, bindSyncProvider(syncProvider, viewport, &scene, QJsonObject{{"section", "environment"}, - {"path", "/atmosphere/weather"}}))); + {"path", "/atmosphere/weather"}}), + {}, "environment:weather")); contentLayout->addStretch(); } @@ -1942,8 +2185,7 @@ void InspectorPanel::showFile() { imageLabel->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); imageLabel->setPixmap(QPixmap::fromImage(image).scaled( - 360, 240, Qt::KeepAspectRatio, - Qt::SmoothTransformation)); + 360, 240, Qt::KeepAspectRatio, Qt::SmoothTransformation)); previewBodyLayout->addWidget(imageLabel); previewLayout->addWidget(previewTitle); previewLayout->addWidget(previewBody); diff --git a/editor/views/editor/materialEditor.cpp b/editor/views/editor/materialEditor.cpp index cbfd0467..328289f8 100644 --- a/editor/views/editor/materialEditor.cpp +++ b/editor/views/editor/materialEditor.cpp @@ -349,7 +349,7 @@ MaterialEditorPanel::normalizedMaterial(const QJsonObject &source) const { if (!result.value("ao").isDouble()) result.insert("ao", 1.0); if (!result.value("reflectivity").isDouble()) - result.insert("reflectivity", 0.5); + result.insert("reflectivity", 0.0); if (!result.value("emissiveColor").isArray()) result.insert("emissiveColor", QJsonArray{0.0, 0.0, 0.0, 1.0}); if (!result.value("emissiveIntensity").isDouble()) diff --git a/editor/views/editor/viewport.cpp b/editor/views/editor/viewport.cpp index 2244f074..c951ef28 100644 --- a/editor/views/editor/viewport.cpp +++ b/editor/views/editor/viewport.cpp @@ -79,12 +79,13 @@ int runtimeMouseButton(Qt::MouseButton button) { } int activeRuntimeMouseButton(Qt::MouseButtons buttons, - int rightDragRuntimeButton) { + int rightDragRuntimeButton, + int middleDragRuntimeButton) { if (buttons.testFlag(Qt::RightButton)) { return rightDragRuntimeButton; } if (buttons.testFlag(Qt::MiddleButton)) { - return runtimeMouseButton(Qt::MiddleButton); + return middleDragRuntimeButton; } if (buttons.testFlag(Qt::LeftButton)) { return runtimeMouseButton(Qt::LeftButton); @@ -246,7 +247,7 @@ class RuntimeRenameCommand : public QUndoCommand { QString before; QString after; }; -} +} // namespace ViewportPanel::ViewportPanel(const QString &projectFile, QWidget *parent) : QWidget(parent), projectFile(projectFile) { @@ -274,7 +275,7 @@ ViewportPanel::ViewportPanel(const QString &projectFile, QWidget *parent) environmentReloadTimer->setInterval(140); connect(frameTimer, &QTimer::timeout, this, [this] { if (stepRuntime() && isVisible()) - frameTimer->start(pathTracingPreview ? 16 : 1); + frameTimer->start(pbrPreview ? 16 : 1); }); connect(resizeTimer, &QTimer::timeout, this, [this] { resizeRuntime(); }); connect(environmentReloadTimer, &QTimer::timeout, this, @@ -302,7 +303,7 @@ void ViewportPanel::setRuntimeStartupEnabled(bool enabled) { void ViewportPanel::showEvent(QShowEvent *event) { QWidget::showEvent(event); if (runtimeContext != nullptr) { - frameTimer->start(pathTracingPreview ? 16 : 1); + frameTimer->start(pbrPreview ? 16 : 1); return; } if (runtimeStartupEnabled) @@ -424,6 +425,11 @@ void ViewportPanel::mousePressEvent(QMouseEvent *event) { ? runtimeMouseButton(Qt::RightButton) : runtimeMouseButton(Qt::MiddleButton); pointerButton = rightDragRuntimeButton; + } else if (event->button() == Qt::MiddleButton) { + middleDragRuntimeButton = event->modifiers().testFlag(Qt::ShiftModifier) + ? runtimeMouseButton(Qt::RightButton) + : runtimeMouseButton(Qt::MiddleButton); + pointerButton = middleDragRuntimeButton; } sendPointerEvent(0, static_cast(event->position().x()), static_cast(event->position().y()), pointerButton); @@ -436,10 +442,11 @@ void ViewportPanel::mousePressEvent(QMouseEvent *event) { void ViewportPanel::mouseMoveEvent(QMouseEvent *event) { if (event->buttons().testFlag(Qt::LeftButton)) leftPointerMoved = true; - sendPointerEvent( - 1, static_cast(event->position().x()), - static_cast(event->position().y()), - activeRuntimeMouseButton(event->buttons(), rightDragRuntimeButton)); + sendPointerEvent(1, static_cast(event->position().x()), + static_cast(event->position().y()), + activeRuntimeMouseButton(event->buttons(), + rightDragRuntimeButton, + middleDragRuntimeButton)); if (keyboardTransformActive) { const QRect bounds(mapToGlobal(QPoint(0, 0)), size()); QPoint cursor = event->globalPosition().toPoint(); @@ -467,6 +474,8 @@ void ViewportPanel::mouseMoveEvent(QMouseEvent *event) { void ViewportPanel::mouseReleaseEvent(QMouseEvent *event) { const int pointerButton = event->button() == Qt::RightButton ? rightDragRuntimeButton + : event->button() == Qt::MiddleButton + ? middleDragRuntimeButton : runtimeMouseButton(event->button()); sendPointerEvent(2, static_cast(event->position().x()), static_cast(event->position().y()), pointerButton); @@ -483,6 +492,8 @@ void ViewportPanel::mouseReleaseEvent(QMouseEvent *event) { leftPointerMoved = false; if (event->button() == Qt::RightButton) rightDragRuntimeButton = 0; + if (event->button() == Qt::MiddleButton) + middleDragRuntimeButton = 0; event->accept(); } @@ -624,7 +635,7 @@ void ViewportPanel::startRuntime() { runtimeContext->setEditorSimulationEnabled(false); runtimeContext->setEditorControlMode(0); runtimeContext->setEditorShadingMode(shadingMode); - runtimeContext->setEditorPathTracingPreview(pathTracingPreview); + runtimeContext->setEditorPathTracingPreview(pbrPreview); resizeRuntime(); refreshSceneSnapshot(); if (!selectionToRestore.isEmpty()) { @@ -654,7 +665,7 @@ void ViewportPanel::startRuntime() { "The first viewport frame failed"); return; } - frameTimer->start(pathTracingPreview ? 16 : 1); + frameTimer->start(pbrPreview ? 16 : 1); emit runtimeLoadingFinished(); emit runtimeStartupFinished(true, {}); if (playAfterRuntimeStart) { @@ -1076,8 +1087,21 @@ bool ViewportPanel::pasteRuntimeObject() { objectClipboard.isEmpty()) { return false; } - const int id = - runtimeContext->pasteObjectDefinition(objectClipboard.toStdString()); + QProgressDialog progress("Pasting object…", QString(), 0, 100, this); + progress.setCancelButton(nullptr); + progress.setMinimumDuration(300); + progress.setWindowModality(Qt::WindowModal); + const int id = runtimeContext->pasteObjectDefinition( + objectClipboard.toStdString(), + [&progress](float value, const std::string &status) { + const int percentage = std::clamp( + static_cast(std::round(value * 100.0f)), 0, 100); + progress.setValue(percentage); + progress.setLabelText(QString::fromStdString(status) + + QStringLiteral("… %1%").arg(percentage)); + QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents); + }); + progress.setValue(100); if (id < 0) return false; if (undoStack != nullptr) @@ -1173,7 +1197,6 @@ bool ViewportPanel::applyRuntimeMaterialDirect(int id, const QString &path) { runtimeContext->saveCurrentScene(); refreshSceneSnapshot(); setSceneDirty(true); - reloadRuntime(); return true; } @@ -1383,7 +1406,7 @@ void ViewportPanel::setRuntimeShadingMode(int mode) { } void ViewportPanel::setPathTracingPreview(bool enabled) { - pathTracingPreview = enabled; + pbrPreview = enabled; if (runtimeContext == nullptr) { return; } @@ -1396,7 +1419,7 @@ void ViewportPanel::setPathTracingPreview(bool enabled) { frameTimer->stop(); const bool frameReady = stepRuntime(); if (frameReady && isVisible()) - frameTimer->start(pathTracingPreview ? 16 : 1); + frameTimer->start(pbrPreview ? 16 : 1); emit runtimeLoadingFinished(); if (!enabled && runtimeContext != nullptr) { const std::string error = runtimeContext->getPathTracingError(); diff --git a/editor/views/editor/viewportTools.cpp b/editor/views/editor/viewportTools.cpp index 177f931e..457f1e35 100644 --- a/editor/views/editor/viewportTools.cpp +++ b/editor/views/editor/viewportTools.cpp @@ -51,8 +51,7 @@ ViewportTools::ViewportTools(ViewportPanel *viewport, pauseButton->setToolTip("Pause"); stepButton = new QToolButton(toolbar); stepButton->setObjectName("viewportPlaybackButton"); - stepButton->setIcon( - styling::icon(styling::Icon::SkipForward, "#7E929C")); + stepButton->setIcon(styling::icon(styling::Icon::SkipForward, "#7E929C")); stepButton->setToolTip("Step one frame"); stopButton = new QToolButton(toolbar); stopButton->setObjectName("viewportPlaybackButton"); @@ -73,9 +72,8 @@ ViewportTools::ViewportTools(ViewportPanel *viewport, const QList transformIcons{ styling::Icon::CursorClick, styling::Icon::ArrowsOutCardinal, styling::Icon::ArrowClockwise, styling::Icon::BoundingBox}; - const QList transformColors{ - QColor("#7E929C"), QColor("#849589"), QColor("#A1957D"), - QColor("#71889A")}; + const QList transformColors{QColor("#7E929C"), QColor("#849589"), + QColor("#A1957D"), QColor("#71889A")}; for (int index = 0; index < transformNames.size(); ++index) { auto *button = new QToolButton(toolbar); button->setObjectName("viewportModeButton"); @@ -127,17 +125,15 @@ ViewportTools::ViewportTools(ViewportPanel *viewport, if (manifest.open(QIODevice::ReadOnly | QIODevice::Text)) { const QString contents = QString::fromUtf8(manifest.readAll()); pathTracingProject = contents.contains(QRegularExpression( - QStringLiteral( - R"(default\s*=\s*["']path[\s_-]*tracing["'])"), + QStringLiteral(R"(default\s*=\s*["']path[\s_-]*tracing["'])"), QRegularExpression::CaseInsensitiveOption)); } auto *shadingGroup = new QActionGroup(toolbar); shadingGroup->setExclusive(true); const QStringList shadingNames = - pathTracingProject - ? QStringList{"PBR Preview", "Path Traced"} - : QStringList{"Lit", "Wireframe", "Points"}; + pathTracingProject ? QStringList{"PBR Preview", "Path Traced"} + : QStringList{"Lit", "Wireframe", "Points"}; const QList shadingIcons = pathTracingProject ? QList{styling::Icon::Sphere, @@ -169,8 +165,7 @@ ViewportTools::ViewportTools(ViewportPanel *viewport, auto *fpsButton = new QToolButton(toolbar); fpsButton->setObjectName("viewportOptionButton"); - fpsButton->setIcon( - styling::icon(styling::Icon::Monitor, "#849589")); + fpsButton->setIcon(styling::icon(styling::Icon::Monitor, "#849589")); fpsButton->setCheckable(true); fpsButton->setChecked(true); fpsButton->setToolTip("Toggle frame rate"); @@ -183,8 +178,9 @@ ViewportTools::ViewportTools(ViewportPanel *viewport, layout->addWidget(toolbar); layout->addWidget(viewport, 1); shortcutHint = - new QLabel("Tab Frame · Num 0 Camera · Right-Drag Pan · Middle-Drag " - "Orbit · G Move · R Rotate · S Scale · X Delete", + new QLabel("Tab Frame · Num 0 Camera · Shift+Middle/Right Pan · " + "Middle/Right Orbit · G Move · R Rotate · S Scale · X " + "Delete", this); shortcutHint->setObjectName("viewportShortcutHint"); shortcutHint->setTextInteractionFlags(Qt::NoTextInteraction); @@ -258,9 +254,8 @@ ViewportTools::ViewportTools(ViewportPanel *viewport, this->viewport != nullptr) this->viewport->openRuntimeScene(scenePaths.at(index)); }); - connect(sceneTabs, &QTabBar::tabCloseRequested, this, [this](int index) { - closeSceneTab(index); - }); + connect(sceneTabs, &QTabBar::tabCloseRequested, this, + [this](int index) { closeSceneTab(index); }); connect(viewport, &ViewportPanel::sceneOpened, this, &ViewportTools::openSceneTab); refreshSceneTabs(); @@ -268,8 +263,8 @@ ViewportTools::ViewportTools(ViewportPanel *viewport, } void ViewportTools::refreshSceneTabs() { - const QString current = viewport != nullptr ? viewport->currentRuntimeScene() - : QString(); + const QString current = + viewport != nullptr ? viewport->currentRuntimeScene() : QString(); if (!current.trimmed().isEmpty()) openSceneTab(current); updateSceneTabs(); diff --git a/editor/views/general/contentBrowser.cpp b/editor/views/general/contentBrowser.cpp index 4cf6f7d7..e2ecbd1e 100644 --- a/editor/views/general/contentBrowser.cpp +++ b/editor/views/general/contentBrowser.cpp @@ -610,7 +610,7 @@ void ContentBrowserPanel::createMaterial() { " \"metallic\": 0.0,\n" " \"roughness\": 0.5,\n" " \"ao\": 1.0,\n" - " \"reflectivity\": 0.5,\n" + " \"reflectivity\": 0.0,\n" " \"emissiveColor\": [0.0, 0.0, 0.0, 1.0],\n" " \"emissiveIntensity\": 0.0,\n" " \"normalMapStrength\": 1.0,\n" diff --git a/include/atlas/core/default_shaders.h b/include/atlas/core/default_shaders.h index d992fea7..64156c5c 100644 --- a/include/atlas/core/default_shaders.h +++ b/include/atlas/core/default_shaders.h @@ -7235,25 +7235,33 @@ R"(Data inst, return N; } -bool isOccluded(intersector isect, - primitive_acceleration_structure sceneAS, float3 P, float3 Ng, - float3 L, float maxDistance, thread uint &rng, - constant Material *materials, - constant uint *primitiveObjects, - constant uint *blasPrimitiveOffsets, - constant VertexData *vertices, constant uint *indices, - constant SceneData &sceneData, PT_MATERIAL_TEXTURE_PARAMS) { +float3 traceShadowVisibility(intersector isect, + primitive_acceleration_structure sceneAS, + float3 P, float3 Ng, float3 L, + float maxDistance, thread uint &rng, + constant Material *materials, + constant uint *primitiveObjects, + constant uint *blasPrimitiveOffsets, + constant VertexData *vertices, + constant uint *indices, + constant InstanceData *instanceData, + constant SceneData &sceneData, + PT_MATERIAL_TEXTURE_PARAMS) { float shadowBias = rayOffsetDistance(P); + float3 visibility = float3(1.0); + float causticGain = 1.0; + float3 entryNormal = float3(0.0); + uint dielectricObject = 0xFFFFFFFFu; ray shadowRay; shadowRay.origin = offsetRayOrigin(P, Ng, L); shadowRay.direction = L; shadowRay.min_distance = 0.0; shadowRay.max_distance = max(maxDistance - shadowBias, shadowBias + 1e-4); - for (uint alphaStep = 0; alphaStep < 16; ++alphaStep) { + for (uint alphaStep = 0; alphaStep < 32; ++alphaStep) { auto shadowHit = isect.intersect(shadowRay, sceneAS); if (shadowHit.type == intersection_type::none) { - return false; + return clampLuminance(visibility * causticGain, 2.5); } uint primitiveIndex = @@ -7274,18 +7282,59 @@ bool isOccluded(intersector isect, material, uv, sceneData.materialTextureCount, PT_MATERIAL_TEXTURE_ARGS); if (opacity >= 0.999 || rand(rng) < opacity) { - return true; + float transmission = clamp(material.transmittance, 0.0, 1.0) * + (1.0 - clamp(material.metallic, 0.0, 1.0)); + if (transmission <= 0.001) { + return float3(0.0); + } + + float3 p0 = float3(vertices[i0].position); + float3 p1 = float3(vertices[i1].position); + float3 p2 = float3(vertices[i2].position); + InstanceData inst = instanceData[objectIndex]; + float3x3 normalMatrix = float3x3( + inst.normalCol0.xyz, inst.normalCol1.xyz, inst.normalCol2.xyz); + float3 hitNormal = normalizeOr( + normalMatrix * cross(p1 - p0, p2 - p0), -L); + hitNormal = dot(hitNormal, L) < 0.0 ? hitNormal : -hitNormal; + float ior = max(material.ior, 1.0); + float dielectricF0 = pow((ior - 1.0) / (ior + 1.0), 2.0); + float fresnel = dielectricF0 + + (1.0 - dielectricF0) * + pow5(1.0 - abs(dot(hitNormal, L))); + float3 tint = mix(float3(1.0), + clamp(material.albedo.xyz, float3(0.0), + float3(1.0)), + 0.15); + visibility *= tint * transmission * (1.0 - fresnel); + if (dielectricObject == objectIndex) { + float curvature = + 1.0 - clamp(abs(dot(entryNormal, hitNormal)), 0.0, 1.0); + float smoothness = + 1.0 - clamp(material.roughness, 0.0, 1.0); + float focus = 1.0 + transmission * max(ior - 1.0, 0.0) * + smoothness * smoothness * + (0.35 + curvature * 3.0); + causticGain *= clamp(focus, 1.0, 2.5); + dielectricObject = 0xFFFFFFFFu; + } else { + dielectricObject = objectIndex; + entryNormal = hitNormal; + } + if (luminance(visibility) <= 0.001) { + return float3(0.0); + } } float advance = shadowHit.distance + rayOffsetDistance(shadowRay.origin); shadowRay.origin += shadowRay.direction * advance; shadowRay.max_distance -= advance; if (shadowRay.max_distance <= shadowBias) { - return false; + return clampLuminance(visibility * causticGain, 2.5); } } - return true; + return float3(0.0); } float3 sampleDirectionalLightDirection(DirectionalLightData light, @@ -7340,7 +7389,8 @@ float disneyDiffuseFactor(float NdotV, float NdotL, float LdotH, float fd90 = 0.5 + 2.0 * LdotH * LdotH * roughness; float lightScatter = 1.0 + (fd90 - 1.0) * pow5(1.0 - NdotL); float viewScatter = 1.0 + (fd90 - 1.0) * pow5(1.0 - NdotV); - return lightScatter * viewScatter; + return lightSc)", +R"(atter * viewScatter; } // GGX importance-sampled microfacet half-vector (in local TBN space, Z=up) @@ -7382,8 +7432,8 @@ float3 sampleGGXVNDF(float3 localView, float roughness, float2 u) { // Full Cook-Torrance PBR for a single analytic light float3 evalPBR(float3 albedo, float metallic, float roughness, - float reflectivity, float3 N, float3 V, float3 L, - float3 lightColor, float intensity) { + float reflectivity, float ior, float transmittance, float3 N, + float3 V, float3 L, float3 lightColor, float intensity) { float3 H = normalize(V + L); float NdotL = max(dot(N, L), 0.0); float NdotV = max(dot(N, V), 1e-4); @@ -7391,17 +7441,21 @@ float3 evalPBR(float3 albedo, float metallic, float roughness, float VdotH = max(dot(V, H), 0.0); float clampedRoughness = clamp(roughness, 0.045, 1.0); - float3 baseF0 = mix(float3(0.04), albedo, clamp(metallic, 0.0, 1.0)); + float dielectricF0 = pow((max(ior, 1.0) - 1.0) / + (max(ior, 1.0) + 1.0), + 2.0); + float3 baseF0 = mix(float3(dielectricF0), albedo, + clamp(metallic, 0.0, 1.0)); float3 reflectedColor = mix(float3(1.0), albedo, metallic); float3 F0 = mix(baseF0, reflectedColor, clamp(reflectivity, 0.0, 1.0)); float3 F = F_Schlick(VdotH, F0); float D = D_GGX(NdotH, clampedRoughness); - float G = )", -R"(G_Smith(NdotV, NdotL, clampedRoughness); + float G = G_Smith(NdotV, NdotL, clampedRoughness); float3 specular = (D * G * F) / max(4.0 * NdotV * NdotL, 1e-4); float3 kD = (1.0 - F) * (1.0 - clamp(metallic, 0.0, 1.0)) * - (1.0 - clamp(reflectivity, 0.0, 1.0)); + (1.0 - clamp(reflectivity, 0.0, 1.0)) * + (1.0 - clamp(transmittance, 0.0, 1.0)); float diffuseFactor = disneyDiffuseFactor(NdotV, NdotL, max(dot(L, H), 0.0), clampedRoughness); float3 diffuse = (kD * albedo * diffuseFactor) / M_PI_F; @@ -7462,28 +7516,23 @@ float3 evalDirectLightingPBR(intersector isect, constant uint *blasPrimitiveOffsets, constant VertexData *vertices, constant uint *indices, + constant InstanceData *instanceData, PT_MATERIAL_TEXTURE_PARAMS) { float3 lighting = float3(0.0); - float surfaceOpacity = - clamp(1.0 - transmittance * (1.0 - metallic), 0.0, 1.0); - // Directional if (sceneData.numDirectionalLights > 0) { float3 L = sampleDirectionalLightDirection(dirLight, rng); - float3 c = evalPBR(albedo, metallic, roughness, reflectivity, N, V, L, - dirLight.color, max(dirLight.intensity, 0.0)); + float3 c = evalPBR(albedo, metallic, roughness, reflectivity, ior, + transmittance, N, V, L, dirLight.color, + max(dirLight.intensity, 0.0)); float3 s = evalSubsurface(albedo, N, V, L, dirLight.color, max(dirLight.intensity, 0.0), roughness, sssStrength, sssThickness); - float3 t = - evalTransmission(albedo, N, V, L, dirLight.color, - max(dirLight.intensity, 0.0), roughness, ior) * - transmittance; - if (!isOccluded(isect, sceneAS, P, Ng, L, 1e30, rng, materials, - primitiveObjects, blasPrimitiveOffsets, vertices, - indices, sceneData, PT_MATERIAL_TEXTURE_ARGS)) { - lighting += (c + s) * surfaceOpacity + t; - } + float3 visibility = traceShadowVisibility( + isect, sceneAS, P, Ng, L, 1e30, rng, materials, primitiveObjects, + blasPrimitiveOffsets, vertices, indices, instanceData, sceneData, + PT_MATERIAL_TEXTURE_ARGS); + lighting += (c + s * (1.0 - transmittance)) * visibility; } // Point lights @@ -7498,19 +7547,18 @@ float3 evalDirectLightingPBR(intersector isect, float rangeFade = 1.0 - smoothstep(lightRange * 0.75, lightRange, dist); float atten = rangeFade / max(distSq, 1e-4); float intensity = max(pointLights[i].intensity, 0.0) * atten; - float3 c = evalPBR(albedo, metallic, roughness, reflectivity, N, V, L, - pointLights[i].color, intensity); + float3 c = evalPBR(albedo, metallic, roughness, reflectivity, ior, + transmittance, N, V, L, pointLights[i].color, + intensity); float3 s = - evalSubsurface(albedo, N, V, L, pointLights[i].color, intensity, + evalSubsurface(albedo, N)", +R"(, V, L, pointLights[i].color, intensity, roughness, sssStrength, sssThickness); - float3 t = evalTransmission(albedo, N, V, L, pointLights[i].color, - intensity, roughness, ior) * - transmittance; - if (!isOccluded(isect, sceneAS, P, Ng, L, dist, rng, materials, - primitiveObjects, blasPrimitiveOffsets, vertices, - indices, sceneData, PT_MATERIAL_TEXTURE_ARGS)) { - lighting += (c + s) * surfaceOpacity + t; - } + float3 visibility = traceShadowVisibility( + isect, sceneAS, P, Ng, L, dist, rng, materials, primitiveObjects, + blasPrimitiveOffsets, vertices, indices, instanceData, sceneData, + PT_MATERIAL_TEXTURE_ARGS); + lighting += (c + s * (1.0 - transmittance)) * visibility; } // Spot lights @@ -7529,19 +7577,17 @@ float3 evalDirectLightingPBR(intersector isect, float rangeFade = 1.0 - smoothstep(lightRange * 0.75, lightRange, dist); float atten = rangeFade / max(distSq, 1e-4); float intensity = max(spotLights[i].intensity, 0.0) * atten * spot; - float3 c = evalPBR(albedo, metallic, roughness, reflectivity, N, V, L, - spotLights[i].color, intensity); + float3 c = evalPBR(albedo, metallic, roughness, reflectivity, ior, + transmittance, N, V, L, spotLights[i].color, + intensity); float3 s = evalSubsurface(albedo, N, V, L, spotLights[i].color, intensity, roughness, sssStrength, sssThickness); - float3 t = evalTransmission(albedo, N, V, L, spotLights[i].color, - intensity, roughness, ior) * - transmittance; - if (!isOccluded(isect, sceneAS, P, Ng, L, dist, rng, materials, - primitiveObjects, blasPrimitiveOffsets, vertices, - indices, sceneData, PT_MATERIAL_TEXTURE_ARGS)) { - lighting += (c + s) * surfaceOpacity + t; - } + float3 visibility = traceShadowVisibility( + isect, sceneAS, P, Ng, L, dist, rng, materials, primitiveObjects, + blasPrimitiveOffsets, vertices, indices, instanceData, sceneData, + PT_MATERIAL_TEXTURE_ARGS); + lighting += (c + s * (1.0 - transmittance)) * visibility; } // Area lights @@ -7555,8 +7601,7 @@ float3 evalDirectLightingPBR(intersector isect, float dist = max(length(toLight), 1e-4); float3 L = toLight / dist; float3 lightNorm = - normalize(cross(areaLights[i].right, a)", -R"(reaLights[i].up)); + normalize(cross(areaLights[i].right, areaLights[i].up)); float cosLight = areaLights[i].twoSided > 0.5 ? abs(dot(lightNorm, -L)) : max(dot(lightNorm, -L), 0.0); @@ -7565,19 +7610,17 @@ R"(reaLights[i].up)); float distSq = max(dist * dist, 1e-6); float atten = cosLight / max(distSq * lightPdfArea, 1e-6); float intensity = max(areaLights[i].intensity, 0.0) * atten; - float3 c = evalPBR(albedo, metallic, roughness, reflectivity, N, V, L, - areaLights[i].color, intensity); + float3 c = evalPBR(albedo, metallic, roughness, reflectivity, ior, + transmittance, N, V, L, areaLights[i].color, + intensity); float3 s = evalSubsurface(albedo, N, V, L, areaLights[i].color, intensity, roughness, sssStrength, sssThickness); - float3 t = evalTransmission(albedo, N, V, L, areaLights[i].color, - intensity, roughness, ior) * - transmittance; - if (!isOccluded(isect, sceneAS, P, Ng, L, dist, rng, materials, - primitiveObjects, blasPrimitiveOffsets, vertices, - indices, sceneData, PT_MATERIAL_TEXTURE_ARGS)) { - lighting += (c + s) * surfaceOpacity + t; - } + float3 visibility = traceShadowVisibility( + isect, sceneAS, P, Ng, L, dist, rng, materials, primitiveObjects, + blasPrimitiveOffsets, vertices, indices, instanceData, sceneData, + PT_MATERIAL_TEXTURE_ARGS); + lighting += (c + s * (1.0 - transmittance)) * visibility; } return lighting; @@ -7671,7 +7714,8 @@ float3 sampleRadiance(uint2 gid, uint sampleIndex, uint w, float3x3 normalMatrix = float3x3( inst.normalCol0.xyz, inst.normalCol1.xyz, inst.normalCol2.xyz); geometricNormal = normalizeOr( - cross(p1 - p0, p2 - p0), + normalMatrix *)", +R"( cross(p1 - p0, p2 - p0), normalizeOr(normalMatrix * localN, float3(0.0, 1.0, 0.0))); float alpha = resolveMaterialOpacity( @@ -7729,14 +7773,14 @@ float3 sampleRadiance(uint2 gid, uint sampleIndex, uint w, primaryAlbedo = albedo; primaryNormal = N; primaryPosition = P; - prima)", -R"(ryDepth = length(P - primaryRay.origin); + primaryDepth = length(P - primaryRay.origin); primaryRoughness = roughness; primaryHitDistance = hit.distance; primaryObjectId = surfaceObjectIndex; } - float reflectivity = clamp(mat.reflectivity, 0.0, 1.0); + float reflectivity = clamp(mat.reflectivity, 0.0, 1.0) * + (1.0 - transmittance); float sssStrength = 0.0; float sssThickness = mix(0.25, 1.75, ao); float3 direct = evalDirectLightingPBR( @@ -7744,28 +7788,29 @@ R"(ryDepth = length(P - primaryRay.origin); reflectivity, ior, transmittance, sssStrength, sssThickness, rng, dirLight, sceneData, pointLights, spotLights, areaLights, materials, primitiveObjects, blasPrimitiveOffsets, vertices, indices, - PT_MATERIAL_TEXTURE_ARGS); + instanceData, PT_MATERIAL_TEXTURE_ARGS); radiance += throughput * (direct + emissive); if (depth == 0 && sceneData.ambientIntensity > 0.0) { float aoVisibility = mix(0.2, 1.0, ao); - float3 ambientF0 = mix(float3(0.04), albedo, metallic); + float dielectricF0 = pow((ior - 1.0) / (ior + 1.0), 2.0); + float3 ambientF0 = mix(float3(dielectricF0), albedo, metallic); float3 ambientF = F_Schlick(max(dot(N, V), 0.0), ambientF0); float3 ambientDiffuse = (1.0 - ambientF) * (1.0 - metallic) * albedo * (1.0 - transmittance); - float3 ambientSpecular = - ambientF * mix(1.0, 0.35, roughness); + float3 ambientSpecular = ambientF * mix(1.0, 0.35, roughness) * + (1.0 - transmittance); float3 ambient = (ambientDiffuse + ambientSpecular) * sceneData.ambientColor * sceneData.ambientIntensity * aoVisibility; radiance += throughput * ambient; } - float3 baseF0 = mix(float3(0.04), albedo, metallic); + float dielectricF0 = pow((ior - 1.0) / (ior + 1.0), 2.0); + float3 baseF0 = mix(float3(dielectricF0), albedo, metallic); float3 reflectedColor = mix(float3(1.0), albedo, metallic); float3 F0 = mix(baseF0, reflectedColor, reflectivity); float NdotV = max(dot(N, V), 1e-4); - float dielectricF0 = pow((ior - 1.0) / (ior + 1.0), 2.0); float dielectricFresnel = F_Schlick(NdotV, float3(dielectricF0)).x; float specProb = metallic * mix(0.35, 0.9, 1.0 - roughness) + @@ -7799,11 +7844,15 @@ R"(ryDepth = length(P - primaryRay.origin); normalizeOr(basis * localEnvironmentDirection, N); float NdotEnvironment = dot(N, environmentDirection); if (NdotEnvironment > 0.0 && - dot(Ng, environmentDirection) > 0.0 && - !isOccluded(isect, sceneAS, P, Ng, environmentDirection, 1e30, - rng, materials, primitiveObjects, - blasPrimitiveOffsets, vertices, indices, sceneData, - PT_MATERIAL_TEXTURE_ARGS)) { + dot(Ng, environmentDirection) > 0.0) { + float3 visibility = traceShadowVisibility( + isect, sceneAS, P, Ng, environmentDirection, 1e30, rng, + materials, primitiveObjects, blasPrimitiveOffsets, + vertices, indices, instanceData, sceneData, + PT_MATERIAL_TEXTURE_ARGS); + if (luminance(visibility) <= 0.001) { + visibility = float3(0.0); + } float3 H = normalizeOr(V + environmentDirection, N); float NdotH = max(dot(N, H), 1e-5); float VdotH = max(dot(V, H), 1e-5); @@ -7834,8 +7883,9 @@ R"(ryDepth = length(P - primaryRay.origin); float3 environmentRadiance = skyColor( environmentDirection, 0.0, skybox, sceneData); radiance += throughput * reflectionBsdf * - environmentRadiance * NdotEnvironment * misWeight / - max(environmentPdf, 1e-6); + )", +R"( environmentRadiance * visibility * NdotEnvironment * + misWeight / max(environmentPdf, 1e-6); } } @@ -7888,15 +7938,29 @@ R"(ryDepth = length(P - primaryRay.origin); } else if (choice < specProb + transmitProb && transmitProb > 1e-4) { nextDirection = idealRefractedDirection; - float3 F = F_Schlick(NdotV, float3(dielectricF0)); + float fresnelCosine = NdotV; + if (roughness > 0.025) { + float3 localView = + float3(dot(V, basis[0]), dot(V, basis[1]), dot(V, N)); + float3 localH = sampleGGXVNDF( + localView, roughness, float2(rand(rng), rand(rng))); + float3 H = normalizeOr(basis * localH, N); + float3 roughRefractedDirection = refract(-V, H, eta); + if (dot(roughRefractedDirection, roughRefractedDirection) > + 1e-8 && + dot(roughRefractedDirection, Ng) < 0.0) { + nextDirection = roughRefractedDirection; + fresnelCosine = max(dot(V, H), 0.0); + } + } + float3 F = F_Schlick(fresnelCosine, float3(dielectricF0)); float3 tint = mix(float3(1.0), albedo, 0.15); bounceWeight = (1.0 - F) * tint / max(transmitProb, 1e-4); } else { float3 localDirection = cosineSampleHemisphere(float2(rand(rng), rand(rng))); - nextDirection = norma)", -R"(lizeOr(basis * localDirection, N); + nextDirection = normalizeOr(basis * localDirection, N); float NdotL = max(dot(N, nextDirection), 0.0); float3 H = normalizeOr(V + nextDirection, N); float3 F = F_Schlick(max(dot(V, H), 0.0), F0); @@ -7913,7 +7977,9 @@ R"(lizeOr(basis * localDirection, N); sampledEventWasDelta = false; } - throughput *= max(bounceWeight, float3(0.0)); + bounceWeight = clampLuminance(max(bounceWeight, float3(0.0)), 16.0); + throughput *= bounceWeight; + throughput = clampLuminance(throughput, 32.0); if (depth == 0) { throughput *= max(sceneData.indirectStrength, 0.0); } @@ -7996,7 +8062,8 @@ kernel void main0(texture2d outTex [[texture(0)]], uint spp = max(sceneData.raysPerPixel, 1u); for (uint s = 0; s < spp; ++s) { uint cameraRng = seedBase(gid, w, sceneData.frameIndex, - s + 0x9E3779B9u); + s + 0x9E3779B9u);)", +R"( float2 pixelJitter = float2(rand(cameraRng), rand(cameraRng)) - 0.5; float2 sampleUv = (float2(gid) + 0.5 + pixelJitter) / float2(w, h); @@ -8027,7 +8094,10 @@ kernel void main0(texture2d outTex [[texture(0)]], PT_MATERIAL_TEXTURE_ARGS, skybox, sampleAlbedo, sampleNormal, samplePosition, sampleDepth, sampleRoughness, sampleHitDistance, sampleObjectId); - color += sample; + if (!all(isfinite(sample))) { + sample = float3(0.0); + } + color += clampLuminance(max(sample, float3(0.0)), 12.0); if (s == 0) { primaryAlbedo = sampleAlbedo; primaryNormal = sampleNormal; @@ -8062,7 +8132,7 @@ kernel void main0(texture2d outTex [[texture(0)]], if (frameIndex == 0) prevColor = float4(0, 0, 0, 1); float sampleLuminanceLimit = - historyValid ? max(8.0, luminance(prevColor.xyz) * 6.0 + 2.0) : 128.0; + historyValid ? max(4.0, luminance(prevColor.xyz) * 2.0 + 0.5) : 12.0; color = clampLuminance(color, sampleLuminanceLimit); if (!historyValid) prevColor = float4(color, 1.0); @@ -8073,7 +8143,7 @@ kernel void main0(texture2d outTex [[texture(0)]], float3 clippedHistory = clamp(prevColor.xyz, lower, upper); float3 accum = mix(color, clippedHistory, historyLength / (historyLength + 1.0)); - accum = clampLuminance(accum, 256.0); + accum = clampLuminance(accum, 24.0); constexpr float bloomThreshold = 0.8; constexpr float bloomKnee = 0.35; @@ -8083,8 +8153,7 @@ kernel void main0(texture2d outTex [[texture(0)]], bloomKnee * 2.0); soft = soft * soft / max(bloomKnee * 4.0, 0.00001); float contribution = max(brightness - bloomThreshold, soft) / - )", -R"(max(brightness, 0.00001); + max(brightness, 0.00001); float3 brightColor = accum * contribution; float4 previousClip = cam.prevViewProj * float4(primaryPosition, 1.0); float2 previousUv = previousClip.xy / max(abs(previousClip.w), 0.0001); @@ -8148,6 +8217,29 @@ kernel void main0(texture2d inputTexture [[texture(0)]], bool centerSurface = centerGuide.w > 0.0; float centerNormalLength = dot(centerGuide.xyz, centerGuide.xyz); float centerLuminance = dot(center, float3(0.2126, 0.7152, 0.0722)); + float neighborLuminance = 0.0; + float neighborWeight = 0.0; + for (int i = 1; i < 9; ++i) { + int2 samplePosition = + clamp(int2(gid) + offsets[i] * parameters.stepWidth, int2(0), + int2(width - 1, height - 1)); + float4 sampleGuide = guideTexture.read(uint2(samplePosition)); + bool sampleSurface = sampleGuide.w > 0.0; + if (sampleSurface != centerSurface) + continue; + float sampleLuminance = dot( + inputTexture.read(uint2(samplePosition)).xyz, + float3(0.2126, 0.7152, 0.0722)); + neighborLuminance += sampleLuminance; + neighborWeight += 1.0; + } + if (neighborWeight > 1.0) { + float localLimit = max(3.0, neighborLuminance / neighborWeight * 3.0); + if (centerLuminance > localLimit) { + center *= localLimit / max(centerLuminance, 0.00001); + centerLuminance = localLimit; + } + } float3 filtered = float3(0.0); float totalWeight = 0.0; for (int i = 0; i < 9; ++i) { diff --git a/include/editor/core/themes.h b/include/editor/core/themes.h index 5be376c1..451ecf9b 100644 --- a/include/editor/core/themes.h +++ b/include/editor/core/themes.h @@ -6,22 +6,22 @@ // Source: /Users/maxvdec/Coding/Projects/Atlas/editor/styling/dark.qss inline constexpr const char* DARK_THEME = "* {\n" -" font-family: \"Manrope\";\n" -" font-size: 12px;\n" -" color: #E7ECF3;\n" -" selection-background-color: #647B8D;\n" +" font-family: \"SF Pro Text\", \"Inter\", \"Segoe UI\", \"Arial\";\n" +" font-size: 11px;\n" +" color: #D6D6D6;\n" +" selection-background-color: #4772B3;\n" " selection-color: #FFFFFF;\n" "}\n" "\n" "QWidget {\n" -" background-color: #18191B;\n" -" color: #E7ECF3;\n" +" background-color: #1C1C1C;\n" +" color: #D6D6D6;\n" "}\n" "\n" "QMainWindow,\n" "QDialog,\n" "QFrame {\n" -" background-color: #18191B;\n" +" background-color: #1C1C1C;\n" "}\n" "\n" "QLabel {\n" @@ -33,19 +33,19 @@ inline constexpr const char* DARK_THEME = "}\n" "\n" "QToolTip {\n" -" background-color: #34373A;\n" +" background-color: #303030;\n" " color: #F7F9FC;\n" -" border: 1px solid #505459;\n" -" border-radius: 8px;\n" +" border: 1px solid #505050;\n" +" border-radius: 4px;\n" " padding: 4px 6px;\n" "}\n" "\n" "QGroupBox {\n" -" background-color: #242628;\n" -" border: 1px solid #3A3D40;\n" -" border-radius: 10px;\n" -" margin-top: 14px;\n" -" padding: 8px;\n" +" background-color: #242424;\n" +" border: 1px solid #3B3B3B;\n" +" border-radius: 4px;\n" +" margin-top: 13px;\n" +" padding: 7px;\n" " color: #F1F4F8;\n" " font-weight: 650;\n" "}\n" @@ -56,17 +56,17 @@ inline constexpr const char* DARK_THEME = " left: 9px;\n" " padding: 0 6px;\n" " color: #AEB8C8;\n" -" background-color: #242628;\n" +" background-color: #242424;\n" "}\n" "\n" "QScrollArea,\n" "QAbstractScrollArea {\n" -" background-color: #1E2022;\n" +" background-color: #202020;\n" " border: none;\n" "}\n" "\n" "QAbstractScrollArea::corner {\n" -" background-color: #1E2022;\n" +" background-color: #202020;\n" "}\n" "\n" "QScrollBar:vertical {\n" @@ -85,7 +85,7 @@ inline constexpr const char* DARK_THEME = "\n" "QScrollBar::handle:vertical,\n" "QScrollBar::handle:horizontal {\n" -" background-color: #505357;\n" +" background-color: #4A4A4A;\n" " border-radius: 4px;\n" " min-height: 30px;\n" " min-width: 30px;\n" @@ -93,7 +93,7 @@ inline constexpr const char* DARK_THEME = "\n" "QScrollBar::handle:vertical:hover,\n" "QScrollBar::handle:horizontal:hover {\n" -" background-color: #65696E;\n" +" background-color: #666666;\n" "}\n" "\n" "QScrollBar::add-line,\n" @@ -108,53 +108,53 @@ inline constexpr const char* DARK_THEME = "\n" "QPushButton,\n" "QToolButton {\n" -" background-color: #2B2E31;\n" -" border: 1px solid #45494D;\n" -" border-radius: 8px;\n" -" padding: 4px 8px;\n" -" color: #E9EDF4;\n" -" min-height: 18px;\n" -" font-weight: 550;\n" +" background-color: #303030;\n" +" border: 1px solid #464646;\n" +" border-radius: 3px;\n" +" padding: 3px 8px;\n" +" color: #D8D8D8;\n" +" min-height: 22px;\n" +" font-weight: 500;\n" "}\n" "\n" "QPushButton:hover,\n" "QToolButton:hover {\n" -" background-color: #363A3E;\n" -" border-color: #5A5F65;\n" +" background-color: #3A3A3A;\n" +" border-color: #606060;\n" "}\n" "\n" "QPushButton:pressed,\n" "QToolButton:pressed {\n" -" background-color: #232527;\n" -" border-color: #6F7B84;\n" +" background-color: #242424;\n" +" border-color: #4772B3;\n" "}\n" "\n" "QPushButton:checked,\n" "QToolButton:checked {\n" -" background-color: #3A4248;\n" -" border-color: #6F7B84;\n" +" background-color: #3B5F8A;\n" +" border-color: #5D8BC0;\n" " color: #FFFFFF;\n" "}\n" "\n" "QPushButton:default,\n" "#primaryAction {\n" -" background-color: #596A76;\n" -" border-color: #73838E;\n" +" background-color: #4772B3;\n" +" border-color: #6791C9;\n" " color: #FFFFFF;\n" " font-weight: 650;\n" "}\n" "\n" "QPushButton:default:hover,\n" "#primaryAction:hover {\n" -" background-color: #667985;\n" -" border-color: #82919B;\n" +" background-color: #5682C2;\n" +" border-color: #7AA1D4;\n" "}\n" "\n" "QPushButton:disabled,\n" "QToolButton:disabled {\n" -" background-color: #202224;\n" -" border-color: #303235;\n" -" color: #566174;\n" +" background-color: #242424;\n" +" border-color: #333333;\n" +" color: #686868;\n" "}\n" "\n" "QToolButton::menu-indicator {\n" @@ -170,11 +170,12 @@ inline constexpr const char* DARK_THEME = "QDateEdit,\n" "QTimeEdit,\n" "QDateTimeEdit {\n" -" background-color: #1B1D1F;\n" -" border: 1px solid #3B3E42;\n" -" border-radius: 8px;\n" -" padding: 4px 6px;\n" -" color: #EEF2F7;\n" +" background-color: #181818;\n" +" border: 1px solid #3D3D3D;\n" +" border-radius: 3px;\n" +" padding: 3px 7px;\n" +" color: #E2E2E2;\n" +" min-height: 22px;\n" "}\n" "\n" "QLineEdit:hover,\n" @@ -183,7 +184,7 @@ inline constexpr const char* DARK_THEME = "QComboBox:hover,\n" "QSpinBox:hover,\n" "QDoubleSpinBox:hover {\n" -" border-color: #54585D;\n" +" border-color: #5A5A5A;\n" "}\n" "\n" "QLineEdit:focus,\n" @@ -192,8 +193,8 @@ inline constexpr const char* DARK_THEME = "QComboBox:focus,\n" "QSpinBox:focus,\n" "QDoubleSpinBox:focus {\n" -" background-color: #222426;\n" -" border-color: #71808A;\n" +" background-color: #222222;\n" +" border-color: #5D8BC0;\n" "}\n" "\n" "QLineEdit:disabled,\n" @@ -201,9 +202,9 @@ inline constexpr const char* DARK_THEME = "QComboBox:disabled,\n" "QSpinBox:disabled,\n" "QDoubleSpinBox:disabled {\n" -" background-color: #202224;\n" -" border-color: #303235;\n" -" color: #566174;\n" +" background-color: #202020;\n" +" border-color: #303030;\n" +" color: #666666;\n" "}\n" "\n" "QComboBox {\n" @@ -214,27 +215,27 @@ inline constexpr const char* DARK_THEME = " subcontrol-origin: padding;\n" " subcontrol-position: top right;\n" " width: 22px;\n" -" border-left: 1px solid #3B3E42;\n" +" border-left: 1px solid #3B3B3B;\n" "}\n" "\n" "QComboBox QAbstractItemView {\n" -" background-color: #292C2F;\n" -" border: 1px solid #494D52;\n" -" border-radius: 10px;\n" +" background-color: #292929;\n" +" border: 1px solid #4B4B4B;\n" +" border-radius: 4px;\n" " padding: 3px;\n" -" selection-background-color: #3A4248;\n" +" selection-background-color: #3B5F8A;\n" "}\n" "\n" "QAbstractSpinBox::up-button,\n" "QAbstractSpinBox::down-button {\n" -" background-color: #292C2F;\n" +" background-color: #292929;\n" " border: none;\n" " width: 16px;\n" "}\n" "\n" "QAbstractSpinBox::up-button:hover,\n" "QAbstractSpinBox::down-button:hover {\n" -" background-color: #3E4246;\n" +" background-color: #3E3E3E;\n" "}\n" "\n" "QCheckBox,\n" @@ -247,47 +248,47 @@ inline constexpr const char* DARK_THEME = "QRadioButton::indicator {\n" " width: 15px;\n" " height: 15px;\n" -" background-color: #1B1D1F;\n" -" border: 1px solid #55595D;\n" -" border-radius: 5px;\n" +" background-color: #171717;\n" +" border: 1px solid #555555;\n" +" border-radius: 3px;\n" "}\n" "\n" "QCheckBox::indicator:hover,\n" "QRadioButton::indicator:hover {\n" -" border-color: #747B81;\n" +" border-color: #777777;\n" "}\n" "\n" "QCheckBox::indicator:checked,\n" "QRadioButton::indicator:checked {\n" -" background-color: #71808A;\n" -" border-color: #89969F;\n" +" background-color: #4772B3;\n" +" border-color: #6D98CE;\n" "}\n" "\n" "QSlider::groove:horizontal {\n" -" background-color: #34373A;\n" +" background-color: #353535;\n" " height: 4px;\n" " border-radius: 2px;\n" "}\n" "\n" "QSlider::handle:horizontal {\n" -" background-color: #7D8991;\n" -" border: 2px solid #A8AFB4;\n" +" background-color: #5D8BC0;\n" +" border: 2px solid #A0B9D7;\n" " width: 12px;\n" " margin: -5px 0;\n" " border-radius: 7px;\n" "}\n" "\n" "QProgressBar {\n" -" background-color: #26282B;\n" -" border: 1px solid #42464A;\n" -" border-radius: 6px;\n" +" background-color: #262626;\n" +" border: 1px solid #424242;\n" +" border-radius: 3px;\n" " height: 8px;\n" " text-align: center;\n" "}\n" "\n" "QProgressBar::chunk {\n" -" background-color: #71808A;\n" -" border-radius: 6px;\n" +" background-color: #4772B3;\n" +" border-radius: 3px;\n" "}\n" "\n" "QTreeView,\n" @@ -295,8 +296,8 @@ inline constexpr const char* DARK_THEME = "QListWidget,\n" "QTableView,\n" "QTableWidget {\n" -" background-color: #1E2022;\n" -" alternate-background-color: #232527;\n" +" background-color: #202020;\n" +" alternate-background-color: #242424;\n" " border: none;\n" " color: #DCE3EC;\n" " show-decoration-selected: 1;\n" @@ -308,7 +309,7 @@ inline constexpr const char* DARK_THEME = "QTableView::item,\n" "QTableWidget::item {\n" " border: 1px solid transparent;\n" -" border-radius: 7px;\n" +" border-radius: 2px;\n" " padding: 3px 5px;\n" "}\n" "\n" @@ -317,8 +318,8 @@ inline constexpr const char* DARK_THEME = "QListWidget::item:hover,\n" "QTableView::item:hover,\n" "QTableWidget::item:hover {\n" -" background-color: #2B2E31;\n" -" border-color: #43474B;\n" +" background-color: #303030;\n" +" border-color: #464646;\n" "}\n" "\n" "QTreeView::item:selected,\n" @@ -326,89 +327,89 @@ inline constexpr const char* DARK_THEME = "QListWidget::item:selected,\n" "QTableView::item:selected,\n" "QTableWidget::item:selected {\n" -" background-color: #393F44;\n" -" border-color: #66737C;\n" +" background-color: #3B5F8A;\n" +" border-color: #5D8BC0;\n" " color: #FFFFFF;\n" "}\n" "\n" "QHeaderView {\n" -" background-color: #202224;\n" +" background-color: #202020;\n" "}\n" "\n" "QHeaderView::section {\n" -" background-color: #292C2F;\n" +" background-color: #292929;\n" " border: none;\n" -" border-right: 1px solid #404347;\n" -" border-bottom: 1px solid #404347;\n" +" border-right: 1px solid #404040;\n" +" border-bottom: 1px solid #404040;\n" " padding: 5px 7px;\n" " color: #98A4B7;\n" " font-weight: 600;\n" "}\n" "\n" "QTabWidget::pane {\n" -" background-color: #1E2022;\n" -" border: 1px solid #3B3E42;\n" -" border-radius: 0 0 10px 10px;\n" +" background-color: #202020;\n" +" border: 1px solid #3B3B3B;\n" +" border-radius: 0;\n" "}\n" "\n" "QTabBar::tab {\n" -" background-color: #202224;\n" +" background-color: #252525;\n" " border: none;\n" -" border-right: 1px solid #383B3F;\n" -" border-bottom: 1px solid #3B3E42;\n" +" border-right: 1px solid #393939;\n" +" border-bottom: 1px solid #3B3B3B;\n" " color: #7F8B9D;\n" " min-width: 88px;\n" " padding: 5px 10px;\n" -" margin: 2px 1px;\n" -" border-radius: 8px;\n" +" margin: 1px 0;\n" +" border-radius: 0;\n" "}\n" "\n" "QTabBar::tab:hover {\n" -" background-color: #2B2E31;\n" +" background-color: #303030;\n" " color: #C8D1DF;\n" "}\n" "\n" "QTabBar::tab:selected {\n" -" background-color: #36393C;\n" +" background-color: #333333;\n" " color: #F2F5F9;\n" -" border-bottom: 2px solid #78858E;\n" +" border-bottom: 2px solid #5D8BC0;\n" "}\n" "\n" "QMenuBar#atlasMenuBar {\n" -" background-color: #141517;\n" -" border-bottom: 1px solid #303236;\n" -" padding: 2px 6px;\n" +" background-color: #181818;\n" +" border-bottom: 1px solid #303030;\n" +" padding: 1px 6px;\n" "}\n" "\n" "QMenuBar#atlasMenuBar::item {\n" " background: transparent;\n" " color: #B8C2D0;\n" " padding: 5px 9px;\n" -" border-radius: 7px;\n" +" border-radius: 3px;\n" "}\n" "\n" "QMenuBar#atlasMenuBar::item:selected,\n" "QMenuBar#atlasMenuBar::item:pressed {\n" -" background-color: #34373A;\n" +" background-color: #343434;\n" " color: #FFFFFF;\n" "}\n" "\n" "QMenu {\n" -" background-color: #292C2F;\n" -" border: 1px solid #494D52;\n" -" border-radius: 10px;\n" -" padding: 3px;\n" +" background-color: #292929;\n" +" border: 1px solid #515151;\n" +" border-radius: 4px;\n" +" padding: 4px;\n" "}\n" "\n" "QMenu::item {\n" " background: transparent;\n" -" border-radius: 7px;\n" -" padding: 5px 30px 5px 8px;\n" +" border-radius: 2px;\n" +" padding: 6px 32px 6px 9px;\n" " color: #DCE3EC;\n" "}\n" "\n" "QMenu::item:selected {\n" -" background-color: #3A3E42;\n" +" background-color: #3B5F8A;\n" " color: #FFFFFF;\n" "}\n" "\n" @@ -417,13 +418,13 @@ inline constexpr const char* DARK_THEME = "}\n" "\n" "QMenu::separator {\n" -" background-color: #424549;\n" +" background-color: #424242;\n" " height: 1px;\n" " margin: 5px 8px;\n" "}\n" "\n" "QSplitter::handle {\n" -" background-color: #111214;\n" +" background-color: #111111;\n" "}\n" "\n" "QSplitter::handle:horizontal {\n" @@ -435,28 +436,38 @@ inline constexpr const char* DARK_THEME = "}\n" "\n" "QSplitter::handle:hover {\n" -" background-color: #71889A;\n" +" background-color: #5D8BC0;\n" "}\n" "\n" "QStatusBar {\n" -" background-color: #141517;\n" -" border-top: 1px solid #303236;\n" +" background-color: #181818;\n" +" border-top: 1px solid #303030;\n" " color: #8490A4;\n" "}\n" "\n" "#atlasStatusBar {\n" -" min-height: 21px;\n" +" min-height: 23px;\n" " padding: 0 6px;\n" "}\n" "\n" "#statusRuntimeIcon {\n" -" padding: 0 4px;\n" +" padding: 0;\n" +"}\n" +"\n" +"#statusRuntime {\n" +" background: transparent;\n" +" border-right: 1px solid #343434;\n" +"}\n" +"\n" +"#statusRuntimeText {\n" +" color: #9BA79F;\n" +" font-size: 10px;\n" "}\n" "\n" "#statusRenderer {\n" -" background-color: #2B2E30;\n" -" border: 1px solid #45494D;\n" -" border-radius: 8px;\n" +" background-color: #292929;\n" +" border: 1px solid #454545;\n" +" border-radius: 3px;\n" " color: #B6BCB8;\n" " font-size: 9px;\n" " font-weight: 700;\n" @@ -470,103 +481,180 @@ inline constexpr const char* DARK_THEME = "}\n" "\n" "#workspaceBar {\n" -" background-color: #1E2022;\n" +" background-color: #202020;\n" " border: none;\n" -" border-bottom: 1px solid #3B3E42;\n" -" spacing: 3px;\n" -" padding: 2px 5px 2px 0;\n" +" border-bottom: 1px solid #3B3B3B;\n" +" spacing: 4px;\n" +" padding: 3px 6px 3px 0;\n" +" min-height: 38px;\n" "}\n" "\n" "#workspaceIdentity {\n" " background: transparent;\n" -" border-right: 1px solid #424549;\n" +" border-right: 1px solid #424242;\n" "}\n" "\n" "#workspaceMark {\n" +" background-color: #292929;\n" +" border: 1px solid #3F3F3F;\n" +" border-radius: 4px;\n" +" padding: 3px;\n" +"}\n" +"\n" +"#workspaceIdentityText {\n" " background: transparent;\n" "}\n" "\n" "#workspaceBrand {\n" -" color: #F5F7FA;\n" -" font-size: 12px;\n" -" font-weight: 800;\n" +" color: #8C8C8C;\n" +" font-size: 8px;\n" +" font-weight: 750;\n" "}\n" "\n" "#workspaceProject {\n" -" color: #7F8B9D;\n" -" font-size: 11px;\n" +" color: #EFEFEF;\n" +" font-size: 12px;\n" +" font-weight: 650;\n" +"}\n" +"\n" +"#workspaceSwitcher {\n" +" background-color: #181818;\n" +" border: 1px solid #383838;\n" +" border-radius: 4px;\n" +" margin-left: 7px;\n" "}\n" "\n" "#workspaceModeButton {\n" -" background-color: transparent;\n" +" background-color: #222222;\n" " border: 1px solid transparent;\n" -" border-radius: 9px;\n" -" color: #94A0B2;\n" -" padding: 4px 8px;\n" -" margin: 0 1px;\n" +" border-radius: 3px;\n" +" color: #A0A0A0;\n" +" padding: 3px 9px;\n" +" min-height: 22px;\n" "}\n" "\n" "#workspaceModeButton:hover {\n" -" background-color: #303336;\n" -" color: #DDE4ED;\n" +" background-color: #333333;\n" +" color: #E2E2E2;\n" "}\n" "\n" "#workspaceModeButton:checked {\n" -" background-color: #3A3E42;\n" -" border-color: #5E656B;\n" +" background-color: #3B5F8A;\n" +" border-color: #5D8BC0;\n" " color: #FFFFFF;\n" "}\n" "\n" -"#workspaceUtilityButton {\n" -" background: transparent;\n" -" border-color: transparent;\n" -" min-width: 26px;\n" -" padding: 3px 5px;\n" +"#workspaceCommandButton,\n" +"#workspaceUtilityButton,\n" +"#workspaceBuildButton,\n" +"#workspaceLaunchButton {\n" +" min-width: 42px;\n" +" padding: 3px 9px;\n" +" margin-left: 2px;\n" +" min-height: 24px;\n" +"}\n" +"\n" +"#workspaceCommandButton,\n" +"#workspaceUtilityButton,\n" +"#workspaceBuildButton {\n" +" background-color: #292929;\n" +" border-color: #444444;\n" "}\n" "\n" -"#workspaceBuildButton,\n" "#workspaceLaunchButton {\n" -" min-width: 26px;\n" -" padding: 3px 5px;\n" -" background-color: #292C2F;\n" -" border-color: #45494D;\n" -" margin-left: 3px;\n" +" background-color: #3F684F;\n" +" border-color: #5A8B6B;\n" +" color: #FFFFFF;\n" +" font-weight: 650;\n" "}\n" "\n" +"#workspaceCommandButton:hover,\n" +"#workspaceUtilityButton:hover,\n" "#workspaceBuildButton:hover,\n" "#workspaceLaunchButton:hover {\n" -" background-color: #363A3E;\n" -" border-color: #5A5F65;\n" +" background-color: #3A3A3A;\n" +" border-color: #606060;\n" +"}\n" +"\n" +"#workspaceLaunchButton:hover {\n" +" background-color: #4E7B5E;\n" +" border-color: #6B9B79;\n" +"}\n" +"\n" +"#workspaceContextBar {\n" +" background-color: #292929;\n" +" border: none;\n" +" border-bottom: 1px solid #3C3C3C;\n" +" spacing: 2px;\n" +" padding: 1px 5px 1px 0;\n" +" min-height: 27px;\n" +"}\n" +"\n" +"#workspaceBreadcrumb {\n" +" background: transparent;\n" +" border-right: 1px solid #414141;\n" +"}\n" +"\n" +"#workspaceContextIcon {\n" +" background: transparent;\n" +"}\n" +"\n" +"#workspaceContextTitle {\n" +" color: #E0E0E0;\n" +" font-weight: 650;\n" +"}\n" +"\n" +"#workspaceContextPath {\n" +" color: #7F7F7F;\n" +"}\n" +"\n" +"#workspacePanelButton {\n" +" background-color: transparent;\n" +" border-color: transparent;\n" +" color: #8F8F8F;\n" +" padding: 2px 7px;\n" +" min-height: 20px;\n" +"}\n" +"\n" +"#workspacePanelButton:hover {\n" +" background-color: #363636;\n" +" color: #DADADA;\n" +"}\n" +"\n" +"#workspacePanelButton:checked {\n" +" background-color: #333333;\n" +" border-color: #4A4A4A;\n" +" color: #EAEAEA;\n" "}\n" "\n" "#sceneTabs {\n" -" background-color: #191A1C;\n" -" border-bottom: 1px solid #3B3E42;\n" +" background-color: #181818;\n" +" border-bottom: 1px solid #3B3B3B;\n" "}\n" "\n" "#sceneTabs::tab {\n" -" background-color: #202224;\n" -" border-right: 1px solid #3A3D40;\n" +" background-color: #242424;\n" +" border-right: 1px solid #3A3A3A;\n" " color: #7F8B9D;\n" " min-width: 112px;\n" " padding: 5px 10px;\n" -" margin: 2px;\n" -" border-radius: 8px;\n" +" margin: 0;\n" +" border-radius: 0;\n" "}\n" "\n" "#sceneTabs::tab:selected {\n" -" background-color: #36393C;\n" +" background-color: #333333;\n" " color: #F3F6FA;\n" -" border-bottom: 2px solid #78858E;\n" +" border-bottom: 2px solid #5D8BC0;\n" "}\n" "\n" "#viewportTools {\n" -" background-color: #141517;\n" +" background-color: #151515;\n" "}\n" "\n" "#viewportToolbar {\n" -" background-color: #202224;\n" -" border-bottom: 1px solid #3B3E42;\n" +" background-color: #292929;\n" +" border-bottom: 1px solid #3B3B3B;\n" "}\n" "\n" "#viewportPlaybackButton,\n" @@ -612,16 +700,16 @@ inline constexpr const char* DARK_THEME = "#materialEditorHeader,\n" "#postProcessingToolbar,\n" "#workspaceToolbar {\n" -" background-color: #232527;\n" -" border-bottom: 1px solid #404347;\n" -" padding: 3px;\n" +" background-color: #292929;\n" +" border-bottom: 1px solid #404040;\n" +" padding: 2px;\n" "}\n" "\n" "#panelAddButton,\n" "#materialSaveButton,\n" "#workspaceApplyButton {\n" -" background-color: #303438;\n" -" border-color: #4C5257;\n" +" background-color: #333333;\n" +" border-color: #4C4C4C;\n" " color: #D8DBDE;\n" "}\n" "\n" @@ -651,49 +739,49 @@ inline constexpr const char* DARK_THEME = "}\n" "\n" "QTreeView#hierarchyTree {\n" -" background-color: #1E2022;\n" -" border-top: 1px solid #303236;\n" -" padding: 5px 3px;\n" +" background-color: #202020;\n" +" border-top: 1px solid #303030;\n" +" padding: 3px 2px;\n" "}\n" "\n" "QTreeView#hierarchyTree::item {\n" -" min-height: 26px;\n" -" padding: 3px 7px;\n" +" min-height: 22px;\n" +" padding: 1px 6px;\n" "}\n" "\n" "QTreeView#hierarchyTree::item:hover {\n" -" background-color: #2B2E31;\n" -" border-color: #43474B;\n" +" background-color: #303030;\n" +" border-color: #444444;\n" "}\n" "\n" "QTreeView#hierarchyTree::item:selected {\n" -" background-color: #343C43;\n" -" border-color: #66737C;\n" +" background-color: #3B5F8A;\n" +" border-color: #5D8BC0;\n" " color: #FFFFFF;\n" "}\n" "\n" "QListView#contentGrid {\n" -" background-color: #1A1C1E;\n" -" border-top: 1px solid #303236;\n" -" padding: 6px;\n" +" background-color: #1C1C1C;\n" +" border-top: 1px solid #303030;\n" +" padding: 5px;\n" "}\n" "\n" "QListView#contentGrid::item {\n" -" background-color: #232527;\n" -" border: 1px solid #34373A;\n" -" border-radius: 11px;\n" -" padding: 7px;\n" +" background-color: #252525;\n" +" border: 1px solid #363636;\n" +" border-radius: 4px;\n" +" padding: 6px;\n" " color: #BCC6D4;\n" "}\n" "\n" "QListView#contentGrid::item:hover {\n" -" background-color: #2E3134;\n" -" border-color: #4B5055;\n" +" background-color: #303030;\n" +" border-color: #505050;\n" "}\n" "\n" "QListView#contentGrid::item:selected {\n" -" background-color: #393F44;\n" -" border-color: #66737C;\n" +" background-color: #3B5F8A;\n" +" border-color: #5D8BC0;\n" " color: #FFFFFF;\n" "}\n" "\n" @@ -701,21 +789,21 @@ inline constexpr const char* DARK_THEME = "#inspectorContent,\n" "#materialEditorBody,\n" "#postProcessingBody {\n" -" background-color: #1E2022;\n" +" background-color: #202020;\n" " border: none;\n" "}\n" "\n" "#inspectorHeader {\n" -" background-color: #25272A;\n" -" border: 1px solid #404347;\n" -" border-radius: 12px;\n" -" padding: 6px;\n" +" background-color: #292929;\n" +" border: 1px solid #414141;\n" +" border-radius: 4px;\n" +" padding: 5px;\n" "}\n" "\n" "#inspectorObjectIcon {\n" -" background-color: #34373B;\n" -" border: 1px solid #4E5257;\n" -" border-radius: 10px;\n" +" background-color: #343434;\n" +" border: 1px solid #4E4E4E;\n" +" border-radius: 4px;\n" " padding: 4px;\n" "}\n" "\n" @@ -729,8 +817,8 @@ inline constexpr const char* DARK_THEME = "\n" "#inspectorNameField:hover,\n" "#inspectorNameField:focus {\n" -" background-color: #1E2022;\n" -" border-color: #5A5F65;\n" +" background-color: #202020;\n" +" border-color: #5A5A5A;\n" "}\n" "\n" "#inspectorTypeLabel {\n" @@ -740,16 +828,16 @@ inline constexpr const char* DARK_THEME = "}\n" "\n" "#inspectorComponent {\n" -" background-color: #242628;\n" -" border: 1px solid #3B3E41;\n" -" border-radius: 11px;\n" -" margin-top: 3px;\n" +" background-color: #242424;\n" +" border: 1px solid #3D3D3D;\n" +" border-radius: 3px;\n" +" margin-top: 2px;\n" "}\n" "\n" "#inspectorComponentHeaderRow {\n" -" background-color: #292C2F;\n" -" border-bottom: 1px solid #3B3E41;\n" -" border-radius: 11px 11px 0 0;\n" +" background-color: #2D2D2D;\n" +" border-bottom: 1px solid #3B3B3B;\n" +" border-radius: 3px 3px 0 0;\n" "}\n" "\n" "#inspectorComponentHeader {\n" @@ -758,11 +846,11 @@ inline constexpr const char* DARK_THEME = " color: #E8ECF2;\n" " font-weight: 650;\n" " text-align: left;\n" -" padding: 5px 7px;\n" +" padding: 4px 7px;\n" "}\n" "\n" "#inspectorComponentHeader:hover {\n" -" background-color: #303337;\n" +" background-color: #383838;\n" "}\n" "\n" "#inspectorComponentRemoveButton {\n" @@ -777,8 +865,8 @@ inline constexpr const char* DARK_THEME = "}\n" "\n" "#inspectorComponentBody {\n" -" background-color: #242628;\n" -" padding: 5px;\n" +" background-color: #242424;\n" +" padding: 4px;\n" "}\n" "\n" "#inspectorPropertyRow {\n" @@ -794,9 +882,9 @@ inline constexpr const char* DARK_THEME = "#inspectorVectorField,\n" "#inspectorColorField,\n" "#inspectorNumericField {\n" -" background-color: #1B1D1F;\n" -" border: 1px solid #404448;\n" -" border-radius: 9px;\n" +" background-color: #181818;\n" +" border: 1px solid #3D3D3D;\n" +" border-radius: 3px;\n" "}\n" "\n" "#inspectorVectorField QDoubleSpinBox,\n" @@ -805,13 +893,26 @@ inline constexpr const char* DARK_THEME = " border: none;\n" "}\n" "\n" -"#inspectorAxisLabel {\n" -" background-color: #34373A;\n" -" border-radius: 7px;\n" -" color: #AAB5C5;\n" +"#inspectorAxisX,\n" +"#inspectorAxisY,\n" +"#inspectorAxisZ {\n" +" border-radius: 2px;\n" +" color: #F1F1F1;\n" " font-size: 9px;\n" " font-weight: 750;\n" -" padding: 2px 4px;\n" +" padding: 2px 5px;\n" +"}\n" +"\n" +"#inspectorAxisX {\n" +" background-color: #874747;\n" +"}\n" +"\n" +"#inspectorAxisY {\n" +" background-color: #477451;\n" +"}\n" +"\n" +"#inspectorAxisZ {\n" +" background-color: #435E86;\n" "}\n" "\n" "#inspectorSyncButton,\n" @@ -828,9 +929,9 @@ inline constexpr const char* DARK_THEME = "}\n" "\n" "#inspectorColorSwatch {\n" -" background-color: #292C2F;\n" -" border: 1px solid #4D5155;\n" -" border-radius: 8px;\n" +" background-color: #292929;\n" +" border: 1px solid #4D4D4D;\n" +" border-radius: 3px;\n" " padding: 2px;\n" "}\n" "\n" @@ -841,9 +942,9 @@ inline constexpr const char* DARK_THEME = "}\n" "\n" "#inspectorNestedGroup {\n" -" background-color: #202224;\n" -" border: 1px solid #383B3F;\n" -" border-radius: 10px;\n" +" background-color: #202020;\n" +" border: 1px solid #383838;\n" +" border-radius: 3px;\n" " margin: 3px 0;\n" " padding: 5px;\n" "}\n" @@ -868,21 +969,21 @@ inline constexpr const char* DARK_THEME = "\n" "#inspectorOpenAssetButton,\n" "#inspectorAddComponentButton {\n" -" background-color: #292C2F;\n" -" border: 1px dashed #5A5F65;\n" +" background-color: #292929;\n" +" border: 1px dashed #5A5A5A;\n" " color: #C7D0DD;\n" " padding: 6px;\n" "}\n" "\n" "#inspectorAddComponentButton:hover {\n" -" background-color: #363A3E;\n" -" border-color: #646B70;\n" +" background-color: #363636;\n" +" border-color: #666666;\n" " color: #FFFFFF;\n" "}\n" "\n" "#inspectorAudioControls {\n" -" background-color: #202224;\n" -" border-radius: 9px;\n" +" background-color: #202020;\n" +" border-radius: 3px;\n" "}\n" "\n" "#materialEditorTitle,\n" @@ -904,15 +1005,15 @@ inline constexpr const char* DARK_THEME = "}\n" "\n" "#materialPreview {\n" -" background-color: #141517;\n" -" border: 1px solid #44484C;\n" -" border-radius: 12px;\n" +" background-color: #151515;\n" +" border: 1px solid #444444;\n" +" border-radius: 4px;\n" "}\n" "\n" "#materialColorButton {\n" -" background-color: #292C2F;\n" -" border-color: #45494D;\n" -" border-radius: 9px;\n" +" background-color: #292929;\n" +" border-color: #454545;\n" +" border-radius: 3px;\n" " color: #C9CDD0;\n" " text-align: left;\n" " padding: 4px 8px;\n" @@ -924,16 +1025,16 @@ inline constexpr const char* DARK_THEME = "}\n" "\n" "#materialTextureSlot {\n" -" background-color: #232527;\n" -" border: 1px solid #404448;\n" -" border-radius: 10px;\n" +" background-color: #252525;\n" +" border: 1px solid #404040;\n" +" border-radius: 4px;\n" " padding: 4px;\n" "}\n" "\n" "#materialTexturePreview {\n" -" background-color: #1A1C1E;\n" -" border: 1px solid #494D52;\n" -" border-radius: 9px;\n" +" background-color: #1A1A1A;\n" +" border: 1px solid #494949;\n" +" border-radius: 3px;\n" " color: #9E897D;\n" " font-weight: 750;\n" "}\n" @@ -950,13 +1051,13 @@ inline constexpr const char* DARK_THEME = "#environmentPages,\n" "#environmentWorkspace,\n" "#environmentPage {\n" -" background-color: #1E2022;\n" +" background-color: #202020;\n" " border: none;\n" "}\n" "\n" "QListWidget#environmentCategories {\n" -" background-color: #1B1D1F;\n" -" border-right: 1px solid #3B3E42;\n" +" background-color: #1B1B1B;\n" +" border-right: 1px solid #3B3B3B;\n" " padding: 5px;\n" "}\n" "\n" @@ -966,8 +1067,8 @@ inline constexpr const char* DARK_THEME = "}\n" "\n" "QListWidget#environmentCategories::item:selected {\n" -" background-color: #343C43;\n" -" border-left: 2px solid #8498A8;\n" +" background-color: #3B5F8A;\n" +" border-left: 2px solid #5D8BC0;\n" "}\n" "\n" "#environmentPageTitle {\n" @@ -981,43 +1082,43 @@ inline constexpr const char* DARK_THEME = "}\n" "\n" "QGroupBox#environmentSection {\n" -" background-color: #242628;\n" -" border-color: #3B3E42;\n" +" background-color: #242424;\n" +" border-color: #3B3B3B;\n" "}\n" "\n" "ads--CDockContainerWidget {\n" -" background-color: #111214;\n" +" background-color: #111111;\n" "}\n" "\n" "ads--CDockContainerWidget > QSplitter {\n" -" background-color: #111214;\n" +" background-color: #111111;\n" "}\n" "\n" "ads--CDockContainerWidget ads--CDockSplitter::handle {\n" -" background-color: #111214;\n" +" background-color: #111111;\n" "}\n" "\n" "ads--CDockContainerWidget ads--CDockSplitter::handle:hover {\n" -" background-color: #71889A;\n" +" background-color: #5D8BC0;\n" "}\n" "\n" "ads--CDockAreaWidget {\n" -" background-color: #1E2022;\n" -" border: 1px solid #34373A;\n" +" background-color: #202020;\n" +" border: 1px solid #343434;\n" "}\n" "\n" "ads--CDockAreaWidget[focused=\"true\"] {\n" -" border-color: #50545A;\n" +" border-color: #4A6382;\n" "}\n" "\n" "ads--CDockAreaTitleBar {\n" -" background-color: #202224;\n" -" border-bottom: 1px solid #3B3E42;\n" -" min-height: 26px;\n" +" background-color: #292929;\n" +" border-bottom: 1px solid #3B3B3B;\n" +" min-height: 25px;\n" "}\n" "\n" "ads--CDockAreaWidget[focused=\"true\"] ads--CDockAreaTitleBar {\n" -" background-color: #26282B;\n" +" background-color: #2D2D2D;\n" "}\n" "\n" "#tabsContainerWidget {\n" @@ -1027,37 +1128,37 @@ inline constexpr const char* DARK_THEME = "ads--CTitleBarButton {\n" " background: transparent;\n" " border: none;\n" -" border-radius: 7px;\n" +" border-radius: 2px;\n" " min-width: 22px;\n" " min-height: 22px;\n" " padding: 2px;\n" "}\n" "\n" "ads--CTitleBarButton:hover {\n" -" background-color: #3E4246;\n" +" background-color: #404040;\n" "}\n" "\n" "ads--CDockWidgetTab {\n" -" background-color: #202224;\n" +" background-color: #292929;\n" " border: none;\n" -" border-right: 1px solid #3A3D40;\n" -" border-bottom: 1px solid #3B3E42;\n" +" border-right: 1px solid #3A3A3A;\n" +" border-bottom: 1px solid #3B3B3B;\n" " padding: 1px 5px;\n" -" margin: 2px 1px;\n" -" border-radius: 7px;\n" +" margin: 1px 0;\n" +" border-radius: 0;\n" "}\n" "\n" "ads--CDockWidgetTab:hover {\n" -" background-color: #2C2F32;\n" +" background-color: #333333;\n" "}\n" "\n" "ads--CDockWidgetTab[activeTab=\"true\"] {\n" -" background-color: #373A3D;\n" -" border-bottom: 2px solid #78858E;\n" +" background-color: #333333;\n" +" border-bottom: 2px solid #5D8BC0;\n" "}\n" "\n" "ads--CDockWidgetTab[focused=\"true\"] {\n" -" background-color: #2B2E31;\n" +" background-color: #303030;\n" "}\n" "\n" "ads--CDockWidgetTab #dockWidgetTabLabel {\n" @@ -1093,7 +1194,7 @@ inline constexpr const char* DARK_THEME = "}\n" "\n" "ads--CDockWidget {\n" -" background-color: #1E2022;\n" +" background-color: #202020;\n" "}\n" "\n" "ads--CAutoHideSideBar,\n" diff --git a/include/editor/views/editorWindow.h b/include/editor/views/editorWindow.h index 0bfd1e2a..c320bf2f 100644 --- a/include/editor/views/editorWindow.h +++ b/include/editor/views/editorWindow.h @@ -1,11 +1,11 @@ /* -* editorWindow.h -* As part of the Atlas project -* Created by Max Van den Eynde in 2026 -* -------------------------------------- -* Description: Main View for the editor's window -* Copyright (c) 2026 Max Van den Eynde -*/ + * editorWindow.h + * As part of the Atlas project + * Created by Max Van den Eynde in 2026 + * -------------------------------------- + * Description: Main View for the editor's window + * Copyright (c) 2026 Max Van den Eynde + */ #ifndef ATLAS_EDITORWINDOW_H #define ATLAS_EDITORWINDOW_H @@ -17,9 +17,9 @@ #include "editor/application/dockManager.h" namespace ads { - class CDockManager; - class CDockWidget; -} +class CDockManager; +class CDockWidget; +} // namespace ads class ViewportPanel; class InspectorPanel; @@ -41,15 +41,15 @@ class SplashScreen; class EditorWindow : public QMainWindow { Q_OBJECT -public: - explicit EditorWindow(const QString& projectFile, - QWidget* parent = nullptr); + public: + explicit EditorWindow(const QString &projectFile, + QWidget *parent = nullptr); -signals: - void startupStatusChanged(const QString& status); - void startupReady(bool success, const QString& message); + signals: + void startupStatusChanged(const QString &status); + void startupReady(bool success, const QString &message); -private: + private: void setupWindow(); void setupMenus(); void setupDocks(); @@ -73,24 +73,26 @@ class EditorWindow : public QMainWindow { void takeViewportScreenshot(); void refreshScriptWatcher(); bool contentBrowserHasFocus() const; + void undoActiveEditor(); + void redoActiveEditor(); - EditorDockManager* dockManager = nullptr; - ads::CDockManager* coreManager = nullptr; - ViewportPanel* viewportPanel = nullptr; - InspectorPanel* inspectorPanel = nullptr; - MaterialEditorPanel* materialEditorPanel = nullptr; - GraphiteEditorPanel* graphiteEditorPanel = nullptr; - PostProcessingPanel* postProcessingPanel = nullptr; - HierarchyPanel* hierarchyPanel = nullptr; - ContentBrowserPanel* contentBrowser = nullptr; - ViewportTools* viewportTools = nullptr; - QStackedWidget* workspaceStack = nullptr; - QButtonGroup* workspaceModeGroup = nullptr; - QMenu* viewMenu = nullptr; - QMenu* windowMenu = nullptr; - QTimer* layoutSaveTimer = nullptr; - SplashScreen* assetLoadingSplash = nullptr; - QFileSystemWatcher* scriptWatcher = nullptr; + EditorDockManager *dockManager = nullptr; + ads::CDockManager *coreManager = nullptr; + ViewportPanel *viewportPanel = nullptr; + InspectorPanel *inspectorPanel = nullptr; + MaterialEditorPanel *materialEditorPanel = nullptr; + GraphiteEditorPanel *graphiteEditorPanel = nullptr; + PostProcessingPanel *postProcessingPanel = nullptr; + HierarchyPanel *hierarchyPanel = nullptr; + ContentBrowserPanel *contentBrowser = nullptr; + ViewportTools *viewportTools = nullptr; + QStackedWidget *workspaceStack = nullptr; + QButtonGroup *workspaceModeGroup = nullptr; + QMenu *viewMenu = nullptr; + QMenu *windowMenu = nullptr; + QTimer *layoutSaveTimer = nullptr; + SplashScreen *assetLoadingSplash = nullptr; + QFileSystemWatcher *scriptWatcher = nullptr; QByteArray defaultDockState; QString projectFile; QString projectName; @@ -99,9 +101,9 @@ class EditorWindow : public QMainWindow { bool startupQueued = false; bool startupComplete = false; - void closeEvent(QCloseEvent* event) override; - void showEvent(QShowEvent* event) override; - bool eventFilter(QObject* watched, QEvent* event) override; + void closeEvent(QCloseEvent *event) override; + void showEvent(QShowEvent *event) override; + bool eventFilter(QObject *watched, QEvent *event) override; }; -#endif //ATLAS_EDITORWINDOW_H +#endif // ATLAS_EDITORWINDOW_H diff --git a/include/editor/views/hierarchyPanel.h b/include/editor/views/hierarchyPanel.h index 3ef28597..f9665e3d 100644 --- a/include/editor/views/hierarchyPanel.h +++ b/include/editor/views/hierarchyPanel.h @@ -59,8 +59,7 @@ class HierarchyPanel : public QWidget { void showAddObjectMenu(const QPoint &position); void showContextMenu(const QPoint &position); int selectedObjectId() const; - QString sceneSignature(const QString &sceneName, - const QJsonArray &objects, + QString sceneSignature(const QString &sceneName, const QJsonArray &objects, const QJsonArray &interfaces) const; ViewportPanel *viewport = nullptr; @@ -73,6 +72,7 @@ class HierarchyPanel : public QWidget { QHash specialItems; QString lastStructureSignature; QString selectedSpecialType; + int draggedObjectId = -1; bool applyingSnapshot = false; }; diff --git a/include/editor/views/inspectorView.h b/include/editor/views/inspectorView.h index 873208c1..e66732cd 100644 --- a/include/editor/views/inspectorView.h +++ b/include/editor/views/inspectorView.h @@ -46,6 +46,7 @@ class InspectorPanel : public QWidget { void showCamera(); void showEnvironment(); void showFile(); + void refreshObjectEditors(const QJsonObject &object); void rebuildBody(); void commitHeaderName(); QJsonObject findObject(int id) const; diff --git a/include/editor/views/viewport.h b/include/editor/views/viewport.h index f2979f30..fbef8d62 100644 --- a/include/editor/views/viewport.h +++ b/include/editor/views/viewport.h @@ -66,8 +66,7 @@ class ViewportPanel : public QWidget { int addRuntimeObjectComponent(int id, const QString &type, const QJsonObject &properties); bool removeRuntimeObjectComponent(int id, int componentIndex); - bool controlRuntimeAudio(int id, int componentIndex, - const QString &action); + bool controlRuntimeAudio(int id, int componentIndex, const QString &action); bool setRuntimeObjectParent(int childId, int parentId); bool deleteRuntimeObject(int id); int createRuntimeObject(const QString &type, const QString &name = {}); @@ -178,8 +177,9 @@ class ViewportPanel : public QWidget { int keyboardTransformAxes = 7; int playbackState = 0; int shadingMode = 0; - bool pathTracingPreview = true; + bool pbrPreview = true; int rightDragRuntimeButton = 0; + int middleDragRuntimeButton = 0; }; #endif // ATLAS_VIEWPORT_H diff --git a/include/editor/widgets/scrubbableSpinBox.h b/include/editor/widgets/scrubbableSpinBox.h index e9050584..df07b4cf 100644 --- a/include/editor/widgets/scrubbableSpinBox.h +++ b/include/editor/widgets/scrubbableSpinBox.h @@ -5,11 +5,11 @@ #include #include #include +#include #include -template -class ScrubbableSpinBoxBase : public SpinBox { +template class ScrubbableSpinBoxBase : public SpinBox { public: explicit ScrubbableSpinBoxBase(QWidget *parent = nullptr) : SpinBox(parent) { @@ -29,6 +29,7 @@ class ScrubbableSpinBoxBase : public SpinBox { scrubStartX = mouse->globalPosition().x(); scrubStartValue = this->value(); scrubbing = false; + selectOnRelease = !this->lineEdit()->hasFocus(); } } else if (event->type() == QEvent::MouseMove) { auto *mouse = static_cast(event); @@ -50,8 +51,15 @@ class ScrubbableSpinBoxBase : public SpinBox { auto *mouse = static_cast(event); if (mouse->button() == Qt::LeftButton && scrubbing) { scrubbing = false; + selectOnRelease = false; return true; } + if (mouse->button() == Qt::LeftButton && + (selectOnRelease || !this->lineEdit()->hasSelectedText())) { + selectOnRelease = false; + QTimer::singleShot(0, this->lineEdit(), + [this] { this->lineEdit()->selectAll(); }); + } } return SpinBox::eventFilter(watched, event); } @@ -60,6 +68,7 @@ class ScrubbableSpinBoxBase : public SpinBox { double scrubStartX = 0.0; double scrubStartValue = 0.0; bool scrubbing = false; + bool selectOnRelease = false; }; using ScrubbableDoubleSpinBox = ScrubbableSpinBoxBase; diff --git a/include/photon/illuminate.h b/include/photon/illuminate.h index 2caa9c9d..7c7aa859 100644 --- a/include/photon/illuminate.h +++ b/include/photon/illuminate.h @@ -94,7 +94,7 @@ class PathTracing { std::shared_ptr pathTracingTexturePrev; /** @brief Rays traced per pixel each dispatch. */ - int raysPerPixel = 1; + int raysPerPixel = 2; /** @brief Maximum bounce count for indirect transport. */ int maxBounces = 6; /** @brief Scalar multiplier for indirect lighting contribution. */ diff --git a/photon/path_tracing.cpp b/photon/path_tracing.cpp index 51382d66..7773b0fe 100644 --- a/photon/path_tracing.cpp +++ b/photon/path_tracing.cpp @@ -1128,8 +1128,7 @@ bool photon::PathTracing::render( const int refinementFrame = std::max(frameIndex, 0); const int pixelStride = interactive ? 4 : (refinementFrame < 4 ? 2 : 1); const int effectiveBounces = - interactive ? std::min(this->maxBounces, 1) - : std::min(this->maxBounces, 2 + refinementFrame / 8); + interactive ? std::min(this->maxBounces, 2) : this->maxBounces; pathTracingPipeline->setUniform1i("sceneData.frameIndex", frameIndex); pathTracingPipeline->setUniform1i("sceneData.maxBounces", effectiveBounces); pathTracingPipeline->setUniform1i("sceneData.pixelStride", pixelStride); diff --git a/runtime/lib/context.cpp b/runtime/lib/context.cpp index f8b4e531..237b2748 100644 --- a/runtime/lib/context.cpp +++ b/runtime/lib/context.cpp @@ -4601,8 +4601,9 @@ runtime::makeContextForMetalView(std::string projectFile, void *metalView, #endif } -std::shared_ptr runtime::makeMaterialPreviewContextForMetalView( - std::string projectFile, void *metalView) { +std::shared_ptr +runtime::makeMaterialPreviewContextForMetalView(std::string projectFile, + void *metalView) { #ifdef METAL if (metalView == nullptr) { throw std::runtime_error("Metal view pointer cannot be null"); @@ -5050,7 +5051,8 @@ bool editorObjectWorldBounds(GameObject &object, glm::vec3 &minimum, const auto *coreObject = dynamic_cast(&object); const std::vector copiedVertices = - coreObject == nullptr ? object.getVertices() : std::vector(); + coreObject == nullptr ? object.getVertices() + : std::vector(); const std::vector &vertices = coreObject != nullptr ? coreObject->vertices : copiedVertices; if (vertices.empty()) { @@ -5437,11 +5439,22 @@ bool Context::setObjectProperty(int id, const std::string &component, } else if (property == "rotation") { object->setRotation(Rotation3d{vector.x, vector.y, vector.z}); } else if (property == "scale") { + auto clampComponent = [](float component) { + if (!std::isfinite(component)) + return 1.0f; + if (std::abs(component) >= 0.001f) + return component; + return std::signbit(component) ? -0.001f : 0.001f; + }; + vector.x = clampComponent(vector.x); + vector.y = clampComponent(vector.y); + vector.z = clampComponent(vector.z); object->setScale(vector); } else { return false; } - setJsonProperty(editorObjectSourceData[id], "/" + property, value); + setJsonProperty(editorObjectSourceData[id], "/" + property, + property == "scale" ? vec3ToJson(vector) : value); syncEditorLightObject(*this, *object); applyPropertySyncs(*this, true); return true; @@ -5726,12 +5739,10 @@ bool Context::setMaterialPreviewEnvironment(int mode) { keyIntensity = 5.5f; rimIntensity = 3.0f; } else if (mode == 2) { - colors = {Color{0.52f, 0.76f, 1.0f, 1.0f}, - Color{0.42f, 0.68f, 0.96f, 1.0f}, - Color{0.3f, 0.62f, 1.0f, 1.0f}, - Color{0.72f, 0.78f, 0.82f, 1.0f}, - Color{0.62f, 0.82f, 1.0f, 1.0f}, - Color{0.46f, 0.72f, 0.98f, 1.0f}}; + colors = { + Color{0.52f, 0.76f, 1.0f, 1.0f}, Color{0.42f, 0.68f, 0.96f, 1.0f}, + Color{0.3f, 0.62f, 1.0f, 1.0f}, Color{0.72f, 0.78f, 0.82f, 1.0f}, + Color{0.62f, 0.82f, 1.0f, 1.0f}, Color{0.46f, 0.72f, 0.98f, 1.0f}}; ambient = {0.58f, 0.74f, 1.0f, 1.0f}; key = {1.0f, 0.95f, 0.84f, 1.0f}; rim = {0.42f, 0.7f, 1.0f, 1.0f}; @@ -6332,8 +6343,7 @@ std::string Context::objectDefinitionJson(int id) { } int Context::pasteObjectDefinition(const std::string &definition) { - if (window == nullptr || currentSceneFile.empty() || definition.empty() || - !saveCurrentScene()) { + if (window == nullptr || currentSceneFile.empty() || definition.empty()) { return -1; } try { @@ -6354,9 +6364,6 @@ int Context::pasteObjectDefinition(const std::string &definition) { objectData["position"][2] = objectData["position"][2].get() + 0.5; } - json sceneData = loadJsonFile(currentSceneFile); - if (!sceneData.is_object()) - return -1; const std::string type = normalizeToken(objectData.value("type", std::string())); const bool isLight = @@ -6364,27 +6371,53 @@ int Context::pasteObjectDefinition(const std::string &definition) { type == "spotlight" || type == "directional" || type == "directionallight" || type == "sun" || type == "area" || type == "arealight" || type == "ambient" || type == "ambientlight"; - const char *collection = isLight ? "lights" : "objects"; - if (!sceneData.contains(collection) || - !sceneData[collection].is_array()) { - sceneData[collection] = json::array(); - } - sceneData[collection].push_back(objectData); - std::ofstream output(currentSceneFile, std::ios::trunc); - if (!output.is_open()) - return -1; - output << sceneData.dump(4) << '\n'; - if (!output.good()) + if (!isLight) { + std::vector rigidbodyComponents; + std::vector standardComponents; + std::vector jointComponents; + auto pasted = createRenderable( + *this, objectData, sceneDir.empty() ? projectDir : sceneDir, + rigidbodyComponents, standardComponents, jointComponents); + auto object = std::dynamic_pointer_cast(pasted); + if (object == nullptr) + return -1; + applyPropertySyncs(*this, false); + auto attach = [this](const std::vector &pending) { + for (const PendingComponent &component : pending) { + try { + attachComponent(*this, component); + } catch (const std::exception &error) { + RUNTIME_LOG("Pasted component is waiting for valid " + "values: " + + std::string(error.what())); + } + } + }; + attach(rigidbodyComponents); + attach(standardComponents); + attach(jointComponents); + if (std::dynamic_pointer_cast(pasted) != nullptr) + window->addUIObject(pasted.get()); + else + window->addObject(pasted.get()); + window->selectEditorObject(object.get(), false); + if (!saveCurrentScene()) + return -1; + return static_cast(object->getId()); + } + const std::string creationType = type == "point" ? "pointLight" : type; + const int pastedId = createObject(creationType, name); + GameObject *pasted = findContextObject(*this, pastedId); + if (pasted == nullptr) return -1; - output.close(); - loadScene(*window, sceneData); - auto pasted = objectReferences.find(name); - if (pasted == objectReferences.end()) - pasted = objectReferences.find(normalizeToken(name)); - if (pasted == objectReferences.end() || pasted->second == nullptr) + editorLightSourceData[pastedId] = objectData; + editorObjectSourceData[pastedId] = objectData; + applyTransform(*pasted, objectData); + syncEditorLightObject(*this, *pasted); + window->selectEditorObject(pasted, false); + if (!saveCurrentScene()) return -1; - window->selectEditorObject(pasted->second, false); - return static_cast(pasted->second->getId()); + return pastedId; } catch (const std::exception &error) { RUNTIME_LOG("Could not paste object: " + std::string(error.what())); return -1; diff --git a/shaders/metal/path_tracing/path.metal b/shaders/metal/path_tracing/path.metal index 9503a5bb..9a8f9031 100644 --- a/shaders/metal/path_tracing/path.metal +++ b/shaders/metal/path_tracing/path.metal @@ -612,25 +612,33 @@ float3 resolveShadingNormal(Material mat, float2 uv, float3 localN, return N; } -bool isOccluded(intersector isect, - primitive_acceleration_structure sceneAS, float3 P, float3 Ng, - float3 L, float maxDistance, thread uint &rng, - constant Material *materials, - constant uint *primitiveObjects, - constant uint *blasPrimitiveOffsets, - constant VertexData *vertices, constant uint *indices, - constant SceneData &sceneData, PT_MATERIAL_TEXTURE_PARAMS) { +float3 traceShadowVisibility(intersector isect, + primitive_acceleration_structure sceneAS, + float3 P, float3 Ng, float3 L, + float maxDistance, thread uint &rng, + constant Material *materials, + constant uint *primitiveObjects, + constant uint *blasPrimitiveOffsets, + constant VertexData *vertices, + constant uint *indices, + constant InstanceData *instanceData, + constant SceneData &sceneData, + PT_MATERIAL_TEXTURE_PARAMS) { float shadowBias = rayOffsetDistance(P); + float3 visibility = float3(1.0); + float causticGain = 1.0; + float3 entryNormal = float3(0.0); + uint dielectricObject = 0xFFFFFFFFu; ray shadowRay; shadowRay.origin = offsetRayOrigin(P, Ng, L); shadowRay.direction = L; shadowRay.min_distance = 0.0; shadowRay.max_distance = max(maxDistance - shadowBias, shadowBias + 1e-4); - for (uint alphaStep = 0; alphaStep < 16; ++alphaStep) { + for (uint alphaStep = 0; alphaStep < 32; ++alphaStep) { auto shadowHit = isect.intersect(shadowRay, sceneAS); if (shadowHit.type == intersection_type::none) { - return false; + return clampLuminance(visibility * causticGain, 2.5); } uint primitiveIndex = @@ -651,18 +659,59 @@ bool isOccluded(intersector isect, material, uv, sceneData.materialTextureCount, PT_MATERIAL_TEXTURE_ARGS); if (opacity >= 0.999 || rand(rng) < opacity) { - return true; + float transmission = clamp(material.transmittance, 0.0, 1.0) * + (1.0 - clamp(material.metallic, 0.0, 1.0)); + if (transmission <= 0.001) { + return float3(0.0); + } + + float3 p0 = float3(vertices[i0].position); + float3 p1 = float3(vertices[i1].position); + float3 p2 = float3(vertices[i2].position); + InstanceData inst = instanceData[objectIndex]; + float3x3 normalMatrix = float3x3( + inst.normalCol0.xyz, inst.normalCol1.xyz, inst.normalCol2.xyz); + float3 hitNormal = normalizeOr( + normalMatrix * cross(p1 - p0, p2 - p0), -L); + hitNormal = dot(hitNormal, L) < 0.0 ? hitNormal : -hitNormal; + float ior = max(material.ior, 1.0); + float dielectricF0 = pow((ior - 1.0) / (ior + 1.0), 2.0); + float fresnel = dielectricF0 + + (1.0 - dielectricF0) * + pow5(1.0 - abs(dot(hitNormal, L))); + float3 tint = mix(float3(1.0), + clamp(material.albedo.xyz, float3(0.0), + float3(1.0)), + 0.15); + visibility *= tint * transmission * (1.0 - fresnel); + if (dielectricObject == objectIndex) { + float curvature = + 1.0 - clamp(abs(dot(entryNormal, hitNormal)), 0.0, 1.0); + float smoothness = + 1.0 - clamp(material.roughness, 0.0, 1.0); + float focus = 1.0 + transmission * max(ior - 1.0, 0.0) * + smoothness * smoothness * + (0.35 + curvature * 3.0); + causticGain *= clamp(focus, 1.0, 2.5); + dielectricObject = 0xFFFFFFFFu; + } else { + dielectricObject = objectIndex; + entryNormal = hitNormal; + } + if (luminance(visibility) <= 0.001) { + return float3(0.0); + } } float advance = shadowHit.distance + rayOffsetDistance(shadowRay.origin); shadowRay.origin += shadowRay.direction * advance; shadowRay.max_distance -= advance; if (shadowRay.max_distance <= shadowBias) { - return false; + return clampLuminance(visibility * causticGain, 2.5); } } - return true; + return float3(0.0); } float3 sampleDirectionalLightDirection(DirectionalLightData light, @@ -759,8 +808,8 @@ float3 sampleGGXVNDF(float3 localView, float roughness, float2 u) { // Full Cook-Torrance PBR for a single analytic light float3 evalPBR(float3 albedo, float metallic, float roughness, - float reflectivity, float3 N, float3 V, float3 L, - float3 lightColor, float intensity) { + float reflectivity, float ior, float transmittance, float3 N, + float3 V, float3 L, float3 lightColor, float intensity) { float3 H = normalize(V + L); float NdotL = max(dot(N, L), 0.0); float NdotV = max(dot(N, V), 1e-4); @@ -768,7 +817,11 @@ float3 evalPBR(float3 albedo, float metallic, float roughness, float VdotH = max(dot(V, H), 0.0); float clampedRoughness = clamp(roughness, 0.045, 1.0); - float3 baseF0 = mix(float3(0.04), albedo, clamp(metallic, 0.0, 1.0)); + float dielectricF0 = pow((max(ior, 1.0) - 1.0) / + (max(ior, 1.0) + 1.0), + 2.0); + float3 baseF0 = mix(float3(dielectricF0), albedo, + clamp(metallic, 0.0, 1.0)); float3 reflectedColor = mix(float3(1.0), albedo, metallic); float3 F0 = mix(baseF0, reflectedColor, clamp(reflectivity, 0.0, 1.0)); float3 F = F_Schlick(VdotH, F0); @@ -777,7 +830,8 @@ float3 evalPBR(float3 albedo, float metallic, float roughness, float3 specular = (D * G * F) / max(4.0 * NdotV * NdotL, 1e-4); float3 kD = (1.0 - F) * (1.0 - clamp(metallic, 0.0, 1.0)) * - (1.0 - clamp(reflectivity, 0.0, 1.0)); + (1.0 - clamp(reflectivity, 0.0, 1.0)) * + (1.0 - clamp(transmittance, 0.0, 1.0)); float diffuseFactor = disneyDiffuseFactor(NdotV, NdotL, max(dot(L, H), 0.0), clampedRoughness); float3 diffuse = (kD * albedo * diffuseFactor) / M_PI_F; @@ -838,28 +892,23 @@ float3 evalDirectLightingPBR(intersector isect, constant uint *blasPrimitiveOffsets, constant VertexData *vertices, constant uint *indices, + constant InstanceData *instanceData, PT_MATERIAL_TEXTURE_PARAMS) { float3 lighting = float3(0.0); - float surfaceOpacity = - clamp(1.0 - transmittance * (1.0 - metallic), 0.0, 1.0); - // Directional if (sceneData.numDirectionalLights > 0) { float3 L = sampleDirectionalLightDirection(dirLight, rng); - float3 c = evalPBR(albedo, metallic, roughness, reflectivity, N, V, L, - dirLight.color, max(dirLight.intensity, 0.0)); + float3 c = evalPBR(albedo, metallic, roughness, reflectivity, ior, + transmittance, N, V, L, dirLight.color, + max(dirLight.intensity, 0.0)); float3 s = evalSubsurface(albedo, N, V, L, dirLight.color, max(dirLight.intensity, 0.0), roughness, sssStrength, sssThickness); - float3 t = - evalTransmission(albedo, N, V, L, dirLight.color, - max(dirLight.intensity, 0.0), roughness, ior) * - transmittance; - if (!isOccluded(isect, sceneAS, P, Ng, L, 1e30, rng, materials, - primitiveObjects, blasPrimitiveOffsets, vertices, - indices, sceneData, PT_MATERIAL_TEXTURE_ARGS)) { - lighting += (c + s) * surfaceOpacity + t; - } + float3 visibility = traceShadowVisibility( + isect, sceneAS, P, Ng, L, 1e30, rng, materials, primitiveObjects, + blasPrimitiveOffsets, vertices, indices, instanceData, sceneData, + PT_MATERIAL_TEXTURE_ARGS); + lighting += (c + s * (1.0 - transmittance)) * visibility; } // Point lights @@ -874,19 +923,17 @@ float3 evalDirectLightingPBR(intersector isect, float rangeFade = 1.0 - smoothstep(lightRange * 0.75, lightRange, dist); float atten = rangeFade / max(distSq, 1e-4); float intensity = max(pointLights[i].intensity, 0.0) * atten; - float3 c = evalPBR(albedo, metallic, roughness, reflectivity, N, V, L, - pointLights[i].color, intensity); + float3 c = evalPBR(albedo, metallic, roughness, reflectivity, ior, + transmittance, N, V, L, pointLights[i].color, + intensity); float3 s = evalSubsurface(albedo, N, V, L, pointLights[i].color, intensity, roughness, sssStrength, sssThickness); - float3 t = evalTransmission(albedo, N, V, L, pointLights[i].color, - intensity, roughness, ior) * - transmittance; - if (!isOccluded(isect, sceneAS, P, Ng, L, dist, rng, materials, - primitiveObjects, blasPrimitiveOffsets, vertices, - indices, sceneData, PT_MATERIAL_TEXTURE_ARGS)) { - lighting += (c + s) * surfaceOpacity + t; - } + float3 visibility = traceShadowVisibility( + isect, sceneAS, P, Ng, L, dist, rng, materials, primitiveObjects, + blasPrimitiveOffsets, vertices, indices, instanceData, sceneData, + PT_MATERIAL_TEXTURE_ARGS); + lighting += (c + s * (1.0 - transmittance)) * visibility; } // Spot lights @@ -905,19 +952,17 @@ float3 evalDirectLightingPBR(intersector isect, float rangeFade = 1.0 - smoothstep(lightRange * 0.75, lightRange, dist); float atten = rangeFade / max(distSq, 1e-4); float intensity = max(spotLights[i].intensity, 0.0) * atten * spot; - float3 c = evalPBR(albedo, metallic, roughness, reflectivity, N, V, L, - spotLights[i].color, intensity); + float3 c = evalPBR(albedo, metallic, roughness, reflectivity, ior, + transmittance, N, V, L, spotLights[i].color, + intensity); float3 s = evalSubsurface(albedo, N, V, L, spotLights[i].color, intensity, roughness, sssStrength, sssThickness); - float3 t = evalTransmission(albedo, N, V, L, spotLights[i].color, - intensity, roughness, ior) * - transmittance; - if (!isOccluded(isect, sceneAS, P, Ng, L, dist, rng, materials, - primitiveObjects, blasPrimitiveOffsets, vertices, - indices, sceneData, PT_MATERIAL_TEXTURE_ARGS)) { - lighting += (c + s) * surfaceOpacity + t; - } + float3 visibility = traceShadowVisibility( + isect, sceneAS, P, Ng, L, dist, rng, materials, primitiveObjects, + blasPrimitiveOffsets, vertices, indices, instanceData, sceneData, + PT_MATERIAL_TEXTURE_ARGS); + lighting += (c + s * (1.0 - transmittance)) * visibility; } // Area lights @@ -940,19 +985,17 @@ float3 evalDirectLightingPBR(intersector isect, float distSq = max(dist * dist, 1e-6); float atten = cosLight / max(distSq * lightPdfArea, 1e-6); float intensity = max(areaLights[i].intensity, 0.0) * atten; - float3 c = evalPBR(albedo, metallic, roughness, reflectivity, N, V, L, - areaLights[i].color, intensity); + float3 c = evalPBR(albedo, metallic, roughness, reflectivity, ior, + transmittance, N, V, L, areaLights[i].color, + intensity); float3 s = evalSubsurface(albedo, N, V, L, areaLights[i].color, intensity, roughness, sssStrength, sssThickness); - float3 t = evalTransmission(albedo, N, V, L, areaLights[i].color, - intensity, roughness, ior) * - transmittance; - if (!isOccluded(isect, sceneAS, P, Ng, L, dist, rng, materials, - primitiveObjects, blasPrimitiveOffsets, vertices, - indices, sceneData, PT_MATERIAL_TEXTURE_ARGS)) { - lighting += (c + s) * surfaceOpacity + t; - } + float3 visibility = traceShadowVisibility( + isect, sceneAS, P, Ng, L, dist, rng, materials, primitiveObjects, + blasPrimitiveOffsets, vertices, indices, instanceData, sceneData, + PT_MATERIAL_TEXTURE_ARGS); + lighting += (c + s * (1.0 - transmittance)) * visibility; } return lighting; @@ -1046,7 +1089,7 @@ float3 sampleRadiance(uint2 gid, uint sampleIndex, uint w, float3x3 normalMatrix = float3x3( inst.normalCol0.xyz, inst.normalCol1.xyz, inst.normalCol2.xyz); geometricNormal = normalizeOr( - cross(p1 - p0, p2 - p0), + normalMatrix * cross(p1 - p0, p2 - p0), normalizeOr(normalMatrix * localN, float3(0.0, 1.0, 0.0))); float alpha = resolveMaterialOpacity( @@ -1110,7 +1153,8 @@ float3 sampleRadiance(uint2 gid, uint sampleIndex, uint w, primaryObjectId = surfaceObjectIndex; } - float reflectivity = clamp(mat.reflectivity, 0.0, 1.0); + float reflectivity = clamp(mat.reflectivity, 0.0, 1.0) * + (1.0 - transmittance); float sssStrength = 0.0; float sssThickness = mix(0.25, 1.75, ao); float3 direct = evalDirectLightingPBR( @@ -1118,28 +1162,29 @@ float3 sampleRadiance(uint2 gid, uint sampleIndex, uint w, reflectivity, ior, transmittance, sssStrength, sssThickness, rng, dirLight, sceneData, pointLights, spotLights, areaLights, materials, primitiveObjects, blasPrimitiveOffsets, vertices, indices, - PT_MATERIAL_TEXTURE_ARGS); + instanceData, PT_MATERIAL_TEXTURE_ARGS); radiance += throughput * (direct + emissive); if (depth == 0 && sceneData.ambientIntensity > 0.0) { float aoVisibility = mix(0.2, 1.0, ao); - float3 ambientF0 = mix(float3(0.04), albedo, metallic); + float dielectricF0 = pow((ior - 1.0) / (ior + 1.0), 2.0); + float3 ambientF0 = mix(float3(dielectricF0), albedo, metallic); float3 ambientF = F_Schlick(max(dot(N, V), 0.0), ambientF0); float3 ambientDiffuse = (1.0 - ambientF) * (1.0 - metallic) * albedo * (1.0 - transmittance); - float3 ambientSpecular = - ambientF * mix(1.0, 0.35, roughness); + float3 ambientSpecular = ambientF * mix(1.0, 0.35, roughness) * + (1.0 - transmittance); float3 ambient = (ambientDiffuse + ambientSpecular) * sceneData.ambientColor * sceneData.ambientIntensity * aoVisibility; radiance += throughput * ambient; } - float3 baseF0 = mix(float3(0.04), albedo, metallic); + float dielectricF0 = pow((ior - 1.0) / (ior + 1.0), 2.0); + float3 baseF0 = mix(float3(dielectricF0), albedo, metallic); float3 reflectedColor = mix(float3(1.0), albedo, metallic); float3 F0 = mix(baseF0, reflectedColor, reflectivity); float NdotV = max(dot(N, V), 1e-4); - float dielectricF0 = pow((ior - 1.0) / (ior + 1.0), 2.0); float dielectricFresnel = F_Schlick(NdotV, float3(dielectricF0)).x; float specProb = metallic * mix(0.35, 0.9, 1.0 - roughness) + @@ -1173,11 +1218,15 @@ float3 sampleRadiance(uint2 gid, uint sampleIndex, uint w, normalizeOr(basis * localEnvironmentDirection, N); float NdotEnvironment = dot(N, environmentDirection); if (NdotEnvironment > 0.0 && - dot(Ng, environmentDirection) > 0.0 && - !isOccluded(isect, sceneAS, P, Ng, environmentDirection, 1e30, - rng, materials, primitiveObjects, - blasPrimitiveOffsets, vertices, indices, sceneData, - PT_MATERIAL_TEXTURE_ARGS)) { + dot(Ng, environmentDirection) > 0.0) { + float3 visibility = traceShadowVisibility( + isect, sceneAS, P, Ng, environmentDirection, 1e30, rng, + materials, primitiveObjects, blasPrimitiveOffsets, + vertices, indices, instanceData, sceneData, + PT_MATERIAL_TEXTURE_ARGS); + if (luminance(visibility) <= 0.001) { + visibility = float3(0.0); + } float3 H = normalizeOr(V + environmentDirection, N); float NdotH = max(dot(N, H), 1e-5); float VdotH = max(dot(V, H), 1e-5); @@ -1208,8 +1257,8 @@ float3 sampleRadiance(uint2 gid, uint sampleIndex, uint w, float3 environmentRadiance = skyColor( environmentDirection, 0.0, skybox, sceneData); radiance += throughput * reflectionBsdf * - environmentRadiance * NdotEnvironment * misWeight / - max(environmentPdf, 1e-6); + environmentRadiance * visibility * NdotEnvironment * + misWeight / max(environmentPdf, 1e-6); } } @@ -1262,7 +1311,22 @@ float3 sampleRadiance(uint2 gid, uint sampleIndex, uint w, } else if (choice < specProb + transmitProb && transmitProb > 1e-4) { nextDirection = idealRefractedDirection; - float3 F = F_Schlick(NdotV, float3(dielectricF0)); + float fresnelCosine = NdotV; + if (roughness > 0.025) { + float3 localView = + float3(dot(V, basis[0]), dot(V, basis[1]), dot(V, N)); + float3 localH = sampleGGXVNDF( + localView, roughness, float2(rand(rng), rand(rng))); + float3 H = normalizeOr(basis * localH, N); + float3 roughRefractedDirection = refract(-V, H, eta); + if (dot(roughRefractedDirection, roughRefractedDirection) > + 1e-8 && + dot(roughRefractedDirection, Ng) < 0.0) { + nextDirection = roughRefractedDirection; + fresnelCosine = max(dot(V, H), 0.0); + } + } + float3 F = F_Schlick(fresnelCosine, float3(dielectricF0)); float3 tint = mix(float3(1.0), albedo, 0.15); bounceWeight = (1.0 - F) * tint / max(transmitProb, 1e-4); @@ -1286,7 +1350,9 @@ float3 sampleRadiance(uint2 gid, uint sampleIndex, uint w, sampledEventWasDelta = false; } - throughput *= max(bounceWeight, float3(0.0)); + bounceWeight = clampLuminance(max(bounceWeight, float3(0.0)), 16.0); + throughput *= bounceWeight; + throughput = clampLuminance(throughput, 32.0); if (depth == 0) { throughput *= max(sceneData.indirectStrength, 0.0); } @@ -1400,7 +1466,10 @@ kernel void main0(texture2d outTex [[texture(0)]], PT_MATERIAL_TEXTURE_ARGS, skybox, sampleAlbedo, sampleNormal, samplePosition, sampleDepth, sampleRoughness, sampleHitDistance, sampleObjectId); - color += sample; + if (!all(isfinite(sample))) { + sample = float3(0.0); + } + color += clampLuminance(max(sample, float3(0.0)), 12.0); if (s == 0) { primaryAlbedo = sampleAlbedo; primaryNormal = sampleNormal; @@ -1435,7 +1504,7 @@ kernel void main0(texture2d outTex [[texture(0)]], if (frameIndex == 0) prevColor = float4(0, 0, 0, 1); float sampleLuminanceLimit = - historyValid ? max(8.0, luminance(prevColor.xyz) * 6.0 + 2.0) : 128.0; + historyValid ? max(4.0, luminance(prevColor.xyz) * 2.0 + 0.5) : 12.0; color = clampLuminance(color, sampleLuminanceLimit); if (!historyValid) prevColor = float4(color, 1.0); @@ -1446,7 +1515,7 @@ kernel void main0(texture2d outTex [[texture(0)]], float3 clippedHistory = clamp(prevColor.xyz, lower, upper); float3 accum = mix(color, clippedHistory, historyLength / (historyLength + 1.0)); - accum = clampLuminance(accum, 256.0); + accum = clampLuminance(accum, 24.0); constexpr float bloomThreshold = 0.8; constexpr float bloomKnee = 0.35; diff --git a/shaders/metal/path_tracing/path_denoise.metal b/shaders/metal/path_tracing/path_denoise.metal index b855898c..9b6c5668 100644 --- a/shaders/metal/path_tracing/path_denoise.metal +++ b/shaders/metal/path_tracing/path_denoise.metal @@ -29,6 +29,29 @@ kernel void main0(texture2d inputTexture [[texture(0)]], bool centerSurface = centerGuide.w > 0.0; float centerNormalLength = dot(centerGuide.xyz, centerGuide.xyz); float centerLuminance = dot(center, float3(0.2126, 0.7152, 0.0722)); + float neighborLuminance = 0.0; + float neighborWeight = 0.0; + for (int i = 1; i < 9; ++i) { + int2 samplePosition = + clamp(int2(gid) + offsets[i] * parameters.stepWidth, int2(0), + int2(width - 1, height - 1)); + float4 sampleGuide = guideTexture.read(uint2(samplePosition)); + bool sampleSurface = sampleGuide.w > 0.0; + if (sampleSurface != centerSurface) + continue; + float sampleLuminance = dot( + inputTexture.read(uint2(samplePosition)).xyz, + float3(0.2126, 0.7152, 0.0722)); + neighborLuminance += sampleLuminance; + neighborWeight += 1.0; + } + if (neighborWeight > 1.0) { + float localLimit = max(3.0, neighborLuminance / neighborWeight * 3.0); + if (centerLuminance > localLimit) { + center *= localLimit / max(centerLuminance, 0.00001); + centerLuminance = localLimit; + } + } float3 filtered = float3(0.0); float totalWeight = 0.0; for (int i = 0; i < 9; ++i) {