From 2651bad08087e68729871c95956840eb899a71c9 Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Sun, 29 Mar 2026 15:12:38 +0800 Subject: [PATCH] Refine editor viewport and interaction workflow --- editor/CMakeLists.txt | 47 ++- editor/src/Actions/HierarchyActionRouter.h | 322 +++++++++------ editor/src/Actions/InspectorActionRouter.h | 11 - editor/src/Actions/ProjectActionRouter.h | 54 +-- editor/src/Application.cpp | 5 + .../src/Platform/WindowsProcessDiagnostics.h | 45 ++ editor/src/UI/ContextMenu.h | 123 ++++++ editor/src/UI/Core.h | 41 +- editor/src/UI/ImGuiSession.h | 1 - editor/src/UI/MenuCommand.h | 34 ++ editor/src/UI/StyleTokens.h | 36 +- editor/src/UI/UI.h | 2 + editor/src/UI/Widgets.h | 140 +++++-- editor/src/Viewport/IViewportHostService.h | 5 + .../Viewport/SceneViewportCameraController.h | 22 +- editor/src/Viewport/SceneViewportGrid.cpp | 71 ++++ editor/src/Viewport/SceneViewportGrid.h | 18 + .../SceneViewportInfiniteGridPass.cpp | 389 ++++++++++++++++++ .../Viewport/SceneViewportInfiniteGridPass.h | 42 ++ editor/src/Viewport/SceneViewportMath.h | 48 +++ .../Viewport/SceneViewportOverlayRenderer.cpp | 82 ++++ .../Viewport/SceneViewportOverlayRenderer.h | 18 + editor/src/Viewport/SceneViewportPicker.cpp | 316 ++++++++++++++ editor/src/Viewport/SceneViewportPicker.h | 40 ++ .../SceneViewportSelectionMaskPass.cpp | 339 +++++++++++++++ .../Viewport/SceneViewportSelectionMaskPass.h | 65 +++ .../SceneViewportSelectionOutlinePass.cpp | 386 +++++++++++++++++ .../SceneViewportSelectionOutlinePass.h | 57 +++ .../Viewport/SceneViewportSelectionUtils.h | 161 ++++++++ editor/src/Viewport/ViewportHostService.h | 241 ++++++++++- editor/src/panels/HierarchyPanel.cpp | 43 +- editor/src/panels/InspectorPanel.cpp | 65 ++- editor/src/panels/InspectorPanel.h | 2 + editor/src/panels/ProjectPanel.cpp | 150 +++++-- editor/src/panels/ProjectPanel.h | 6 +- editor/src/panels/SceneViewPanel.cpp | 296 +++---------- editor/src/panels/SceneViewPanel.h | 4 + tests/editor/CMakeLists.txt | 5 + .../test_scene_viewport_camera_controller.cpp | 82 +++- .../test_scene_viewport_overlay_renderer.cpp | 221 ++++++++++ tests/editor/test_scene_viewport_picker.cpp | 182 ++++++++ .../test_scene_viewport_selection_utils.cpp | 241 +++++++++++ 42 files changed, 3888 insertions(+), 570 deletions(-) create mode 100644 editor/src/UI/ContextMenu.h create mode 100644 editor/src/UI/MenuCommand.h create mode 100644 editor/src/Viewport/SceneViewportGrid.cpp create mode 100644 editor/src/Viewport/SceneViewportGrid.h create mode 100644 editor/src/Viewport/SceneViewportInfiniteGridPass.cpp create mode 100644 editor/src/Viewport/SceneViewportInfiniteGridPass.h create mode 100644 editor/src/Viewport/SceneViewportMath.h create mode 100644 editor/src/Viewport/SceneViewportOverlayRenderer.cpp create mode 100644 editor/src/Viewport/SceneViewportOverlayRenderer.h create mode 100644 editor/src/Viewport/SceneViewportPicker.cpp create mode 100644 editor/src/Viewport/SceneViewportPicker.h create mode 100644 editor/src/Viewport/SceneViewportSelectionMaskPass.cpp create mode 100644 editor/src/Viewport/SceneViewportSelectionMaskPass.h create mode 100644 editor/src/Viewport/SceneViewportSelectionOutlinePass.cpp create mode 100644 editor/src/Viewport/SceneViewportSelectionOutlinePass.h create mode 100644 editor/src/Viewport/SceneViewportSelectionUtils.h create mode 100644 tests/editor/test_scene_viewport_overlay_renderer.cpp create mode 100644 tests/editor/test_scene_viewport_picker.cpp create mode 100644 tests/editor/test_scene_viewport_selection_utils.cpp diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 23c1a9a5..302925cf 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -1,9 +1,14 @@ cmake_minimum_required(VERSION 3.15) -project(XCVolumeRendererUI2 VERSION 1.0 LANGUAGES CXX) +project(XCEditor VERSION 1.0 LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) +get_filename_component(XCENGINE_ROOT_DIR "${CMAKE_CURRENT_SOURCE_DIR}/.." ABSOLUTE) +set(XCENGINE_ENGINE_DIR "${XCENGINE_ROOT_DIR}/engine") +set(XCENGINE_ASSIMP_DLL "${XCENGINE_ENGINE_DIR}/third_party/assimp/bin/assimp-vc143-mt.dll") +set(XCENGINE_ASSIMP_LIB "${XCENGINE_ENGINE_DIR}/third_party/assimp/lib/assimp-vc143-mt.lib") + add_definitions(-DUNICODE -D_UNICODE) add_definitions(-DIMGUI_ENABLE_DOCKING) @@ -18,6 +23,37 @@ FetchContent_Declare( FetchContent_MakeAvailable(imgui) +if(NOT TARGET XCEngine) + set(XCENGINE_VULKAN_SDK_HINT "$ENV{VULKAN_SDK}") + if(NOT EXISTS "${XCENGINE_VULKAN_SDK_HINT}/Lib/vulkan-1.lib") + file(GLOB XCENGINE_VULKAN_SDK_DIRS "D:/VulkanSDK/*") + if(XCENGINE_VULKAN_SDK_DIRS) + list(SORT XCENGINE_VULKAN_SDK_DIRS COMPARE NATURAL ORDER DESCENDING) + list(GET XCENGINE_VULKAN_SDK_DIRS 0 XCENGINE_VULKAN_SDK_HINT) + endif() + endif() + + if(EXISTS "${XCENGINE_VULKAN_SDK_HINT}/Lib/vulkan-1.lib") + set(Vulkan_ROOT "${XCENGINE_VULKAN_SDK_HINT}") + list(APPEND CMAKE_PREFIX_PATH "${XCENGINE_VULKAN_SDK_HINT}") + endif() + + find_package(Vulkan REQUIRED) + + add_library(XCEngine STATIC IMPORTED GLOBAL) + + set_target_properties(XCEngine PROPERTIES + IMPORTED_CONFIGURATIONS "Debug;Release;RelWithDebInfo;MinSizeRel" + IMPORTED_LOCATION_DEBUG "${XCENGINE_ENGINE_DIR}/build/Debug/XCEngine.lib" + IMPORTED_LOCATION_RELEASE "${XCENGINE_ENGINE_DIR}/build/Release/XCEngine.lib" + IMPORTED_LOCATION_RELWITHDEBINFO "${XCENGINE_ENGINE_DIR}/build/Release/XCEngine.lib" + IMPORTED_LOCATION_MINSIZEREL "${XCENGINE_ENGINE_DIR}/build/Release/XCEngine.lib" + INTERFACE_INCLUDE_DIRECTORIES + "${XCENGINE_ENGINE_DIR}/include;${XCENGINE_ENGINE_DIR}/include/XCEngine;${XCENGINE_ENGINE_DIR}/src;${XCENGINE_ENGINE_DIR}/third_party;${XCENGINE_ENGINE_DIR}/third_party/GLAD/include;${XCENGINE_ENGINE_DIR}/third_party/stb;${XCENGINE_ENGINE_DIR}/third_party/assimp/include" + INTERFACE_LINK_LIBRARIES + "${XCENGINE_ASSIMP_LIB};opengl32;Vulkan::Vulkan") +endif() + set(IMGUI_SOURCES ${imgui_SOURCE_DIR}/imgui.cpp ${imgui_SOURCE_DIR}/imgui_demo.cpp @@ -42,6 +78,12 @@ add_executable(${PROJECT_NAME} WIN32 src/panels/MenuBar.cpp src/panels/HierarchyPanel.cpp src/panels/SceneViewPanel.cpp + src/Viewport/SceneViewportPicker.cpp + src/Viewport/SceneViewportGrid.cpp + src/Viewport/SceneViewportInfiniteGridPass.cpp + src/Viewport/SceneViewportSelectionMaskPass.cpp + src/Viewport/SceneViewportSelectionOutlinePass.cpp + src/Viewport/SceneViewportOverlayRenderer.cpp src/panels/GameViewPanel.cpp src/panels/InspectorPanel.cpp src/panels/ConsolePanel.cpp @@ -69,6 +111,7 @@ endif() target_link_libraries(${PROJECT_NAME} PRIVATE XCEngine d3d12.lib + Dbghelp.lib dxgi.lib d3dcompiler.lib Ole32.lib @@ -83,6 +126,6 @@ set_target_properties(${PROJECT_NAME} PROPERTIES add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different - ${CMAKE_SOURCE_DIR}/engine/third_party/assimp/bin/assimp-vc143-mt.dll + ${XCENGINE_ASSIMP_DLL} $/assimp-vc143-mt.dll ) diff --git a/editor/src/Actions/HierarchyActionRouter.h b/editor/src/Actions/HierarchyActionRouter.h index 486badcb..00035daa 100644 --- a/editor/src/Actions/HierarchyActionRouter.h +++ b/editor/src/Actions/HierarchyActionRouter.h @@ -7,10 +7,21 @@ #include "Core/IEditorContext.h" #include "UI/PopupState.h" +#include + +#include +#include + namespace XCEngine { namespace Editor { namespace Actions { +inline void TraceHierarchyPopup(const std::string& message) { + XCEngine::Debug::Logger::Get().Info( + XCEngine::Debug::LogCategory::General, + XCEngine::Containers::String(message.c_str())); +} + inline constexpr const char* HierarchyEntityPayloadType() { return "ENTITY_PTR"; } @@ -60,11 +71,64 @@ inline void RequestHierarchyOptionsPopup(UI::DeferredPopupState& optionsPopup) { } inline void RequestHierarchyBackgroundContextPopup(UI::DeferredPopupState& backgroundContextMenu) { - if (ImGui::IsWindowHovered() && ImGui::IsMouseClicked(1) && !ImGui::IsAnyItemHovered()) { + const bool windowHovered = ImGui::IsWindowHovered(); + const bool rightClicked = ImGui::IsMouseClicked(1); + const bool anyItemHovered = ImGui::IsAnyItemHovered(); + if (rightClicked) { + TraceHierarchyPopup( + std::string("Hierarchy background RMB: windowHovered=") + + (windowHovered ? "1" : "0") + + ", anyItemHovered=" + + (anyItemHovered ? "1" : "0")); + } + + if (windowHovered && rightClicked && !anyItemHovered) { + TraceHierarchyPopup("Hierarchy background popup requested"); backgroundContextMenu.RequestOpen(); } } +inline bool AcceptHierarchyEntityDropToRoot(IEditorContext& context) { + if (!ImGui::BeginDragDropTarget()) { + return false; + } + + bool accepted = false; + if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload(HierarchyEntityPayloadType())) { + ::XCEngine::Components::GameObject* sourceGameObject = *(::XCEngine::Components::GameObject**)payload->Data; + if (sourceGameObject && sourceGameObject->GetParent() != nullptr) { + Commands::ReparentEntityPreserveWorldTransform(context, sourceGameObject, 0); + accepted = true; + } + } + ImGui::EndDragDropTarget(); + return accepted; +} + +inline void DrawHierarchyRootDropTarget(IEditorContext& context) { + ImGui::InvisibleButton("##DragTarget", ImVec2(-1, -1)); + AcceptHierarchyEntityDropToRoot(context); +} + +template +inline void DrawHierarchyBackgroundInteraction( + IEditorContext& context, + const UI::InlineTextEditState& renameState) { + const ImVec2 available = ImGui::GetContentRegionAvail(); + const ImVec2 surfaceSize( + ImMax(available.x, 1.0f), + ImMax(available.y, 1.0f)); + + ImGui::InvisibleButton("##HierarchyBackgroundSurface", surfaceSize); + const bool hovered = ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup); + + if (hovered && ImGui::IsMouseClicked(ImGuiMouseButton_Left) && !renameState.IsActive()) { + context.GetSelectionManager().ClearSelection(); + } + + AcceptHierarchyEntityDropToRoot(context); +} + template inline void DrawHierarchySortOptionsPopup( UI::DeferredPopupState& optionsPopup, @@ -103,6 +167,144 @@ inline void DrawHierarchySortOptionsPopup( UI::EndPopup(); } +inline void DrawHierarchyCreateActions(IEditorContext& context, ::XCEngine::Components::GameObject* parent) { + DrawMenuAction(MakeCreateEmptyEntityAction(), [&]() { + TraceHierarchyPopup("Hierarchy create clicked: Empty Object"); + auto* created = Commands::CreateEmptyEntity(context, parent, "Create Entity", "GameObject"); + TraceHierarchyPopup( + std::string("Hierarchy create result: Empty Object, createdId=") + + std::to_string(created ? created->GetID() : 0)); + }); + DrawMenuSeparator(); + DrawMenuAction(MakeCreateCameraEntityAction(), [&]() { + TraceHierarchyPopup("Hierarchy create clicked: Camera"); + auto* created = Commands::CreateCameraEntity(context, parent); + TraceHierarchyPopup( + std::string("Hierarchy create result: Camera, createdId=") + + std::to_string(created ? created->GetID() : 0)); + }); + DrawMenuAction(MakeCreateLightEntityAction(), [&]() { + TraceHierarchyPopup("Hierarchy create clicked: Light"); + auto* created = Commands::CreateLightEntity(context, parent); + TraceHierarchyPopup( + std::string("Hierarchy create result: Light, createdId=") + + std::to_string(created ? created->GetID() : 0)); + }); + DrawMenuSeparator(); + DrawMenuAction(MakeCreateCubeEntityAction(), [&]() { + TraceHierarchyPopup("Hierarchy create clicked: Cube"); + auto* created = Commands::CreateEmptyEntity(context, parent, "Create Cube", "Cube"); + TraceHierarchyPopup( + std::string("Hierarchy create result: Cube, createdId=") + + std::to_string(created ? created->GetID() : 0)); + }); + DrawMenuAction(MakeCreateSphereEntityAction(), [&]() { + TraceHierarchyPopup("Hierarchy create clicked: Sphere"); + auto* created = Commands::CreateEmptyEntity(context, parent, "Create Sphere", "Sphere"); + TraceHierarchyPopup( + std::string("Hierarchy create result: Sphere, createdId=") + + std::to_string(created ? created->GetID() : 0)); + }); + DrawMenuAction(MakeCreatePlaneEntityAction(), [&]() { + TraceHierarchyPopup("Hierarchy create clicked: Plane"); + auto* created = Commands::CreateEmptyEntity(context, parent, "Create Plane", "Plane"); + TraceHierarchyPopup( + std::string("Hierarchy create result: Plane, createdId=") + + std::to_string(created ? created->GetID() : 0)); + }); +} + +inline void HandleHierarchyItemContextRequest( + IEditorContext& context, + ::XCEngine::Components::GameObject* gameObject, + UI::TargetedPopupState<::XCEngine::Components::GameObject*>& itemContextMenu) { + if (!gameObject) { + return; + } + + TraceHierarchyPopup( + std::string("Hierarchy item popup requested for entityId=") + + std::to_string(gameObject->GetID()) + + ", name=" + + gameObject->GetName()); + context.GetSelectionManager().SetSelectedEntity(gameObject->GetID()); + itemContextMenu.RequestOpen(gameObject); +} + +inline void DrawHierarchyBackgroundContextPopup(IEditorContext& context, UI::DeferredPopupState& backgroundContextMenu) { + backgroundContextMenu.ConsumeOpenRequest("HierarchyContextMenu"); + static bool s_lastBackgroundPopupOpen = false; + + if (!UI::BeginPopup("HierarchyContextMenu")) { + if (s_lastBackgroundPopupOpen) { + TraceHierarchyPopup("Hierarchy background popup closed"); + s_lastBackgroundPopupOpen = false; + } + return; + } + + if (!s_lastBackgroundPopupOpen) { + TraceHierarchyPopup("Hierarchy background popup opened"); + s_lastBackgroundPopupOpen = true; + } + + DrawHierarchyCreateActions(context, nullptr); + UI::EndPopup(); +} + +inline void DrawHierarchyContextActions(IEditorContext& context, ::XCEngine::Components::GameObject* gameObject) { + DrawMenuAction(MakeDetachEntityAction(gameObject), [&]() { + Commands::DetachEntity(context, gameObject); + }); + DrawMenuAction(MakeRenameEntityAction(gameObject), [&]() { + RequestEntityRename(context, gameObject); + }); + DrawMenuAction(MakeDeleteEntityAction(gameObject), [&]() { + Commands::DeleteEntity(context, gameObject->GetID()); + }); + DrawMenuSeparator(); + DrawMenuAction(MakeCopyEntityAction(gameObject), [&]() { + Commands::CopyEntity(context, gameObject->GetID()); + }); + DrawMenuAction(MakePasteEntityAction(context), [&]() { + Commands::PasteEntity(context, gameObject->GetID()); + }); + DrawMenuAction(MakeDuplicateEntityAction(gameObject), [&]() { + Commands::DuplicateEntity(context, gameObject->GetID()); + }); + DrawMenuSeparator(); + DrawHierarchyCreateActions(context, gameObject); +} + +inline void DrawHierarchyEntityContextPopup( + IEditorContext& context, + UI::TargetedPopupState<::XCEngine::Components::GameObject*>& itemContextMenu) { + itemContextMenu.ConsumeOpenRequest("HierarchyEntityContextMenu"); + static bool s_lastEntityPopupOpen = false; + + if (!UI::BeginPopup("HierarchyEntityContextMenu")) { + if (s_lastEntityPopupOpen) { + TraceHierarchyPopup("Hierarchy entity popup closed"); + s_lastEntityPopupOpen = false; + } + return; + } + + if (!s_lastEntityPopupOpen) { + TraceHierarchyPopup("Hierarchy entity popup opened"); + s_lastEntityPopupOpen = true; + } + + if (itemContextMenu.HasTarget()) { + DrawHierarchyContextActions(context, itemContextMenu.TargetValue()); + } + UI::EndPopup(); + + if (!ImGui::IsPopupOpen("HierarchyEntityContextMenu") && !itemContextMenu.HasPendingOpenRequest()) { + itemContextMenu.Clear(); + } +} + inline bool BeginHierarchyEntityDrag(::XCEngine::Components::GameObject* gameObject) { if (!gameObject || !ImGui::BeginDragDropSource(ImGuiDragDropFlags_None)) { return false; @@ -131,124 +333,6 @@ inline bool AcceptHierarchyEntityDrop(IEditorContext& context, ::XCEngine::Compo return accepted; } -inline bool AcceptHierarchyEntityDropToRoot(IEditorContext& context) { - if (!ImGui::BeginDragDropTarget()) { - return false; - } - - bool accepted = false; - if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload(HierarchyEntityPayloadType())) { - ::XCEngine::Components::GameObject* sourceGameObject = *(::XCEngine::Components::GameObject**)payload->Data; - if (sourceGameObject && sourceGameObject->GetParent() != nullptr) { - Commands::ReparentEntityPreserveWorldTransform(context, sourceGameObject, 0); - accepted = true; - } - } - ImGui::EndDragDropTarget(); - return accepted; -} - -inline void DrawHierarchyRootDropTarget(IEditorContext& context) { - ImGui::InvisibleButton("##DragTarget", ImVec2(-1, -1)); - AcceptHierarchyEntityDropToRoot(context); -} - -inline void DrawHierarchyCreateActions(IEditorContext& context, ::XCEngine::Components::GameObject* parent) { - DrawMenuAction(MakeCreateEmptyEntityAction(), [&]() { - Commands::CreateEmptyEntity(context, parent, "Create Entity", "GameObject"); - }); - DrawMenuSeparator(); - DrawMenuAction(MakeCreateCameraEntityAction(), [&]() { - Commands::CreateCameraEntity(context, parent); - }); - DrawMenuAction(MakeCreateLightEntityAction(), [&]() { - Commands::CreateLightEntity(context, parent); - }); - DrawMenuSeparator(); - DrawMenuAction(MakeCreateCubeEntityAction(), [&]() { - Commands::CreateEmptyEntity(context, parent, "Create Cube", "Cube"); - }); - DrawMenuAction(MakeCreateSphereEntityAction(), [&]() { - Commands::CreateEmptyEntity(context, parent, "Create Sphere", "Sphere"); - }); - DrawMenuAction(MakeCreatePlaneEntityAction(), [&]() { - Commands::CreateEmptyEntity(context, parent, "Create Plane", "Plane"); - }); -} - -inline void HandleHierarchyItemContextRequest( - IEditorContext& context, - ::XCEngine::Components::GameObject* gameObject, - UI::TargetedPopupState<::XCEngine::Components::GameObject*>& itemContextMenu) { - if (!gameObject) { - return; - } - - context.GetSelectionManager().SetSelectedEntity(gameObject->GetID()); - itemContextMenu.RequestOpen(gameObject); -} - -inline void DrawHierarchyBackgroundContextPopup(IEditorContext& context, UI::DeferredPopupState& backgroundContextMenu) { - backgroundContextMenu.ConsumeOpenRequest("HierarchyContextMenu"); - - if (!UI::BeginPopup("HierarchyContextMenu")) { - return; - } - - DrawHierarchyCreateActions(context, nullptr); - UI::EndPopup(); -} - -inline void DrawHierarchyContextActions(IEditorContext& context, ::XCEngine::Components::GameObject* gameObject) { - if (UI::DrawPopupSubmenuScope("Create", [&]() { - DrawHierarchyCreateActions(context, gameObject); - })) { - } - - DrawMenuAction(MakeCreateChildEntityAction(gameObject), [&]() { - Commands::CreateEmptyEntity(context, gameObject, "Create Child", "GameObject"); - }); - DrawMenuSeparator(); - DrawMenuAction(MakeDetachEntityAction(gameObject), [&]() { - Commands::DetachEntity(context, gameObject); - }); - DrawMenuAction(MakeRenameEntityAction(gameObject), [&]() { - RequestEntityRename(context, gameObject); - }); - DrawMenuAction(MakeDeleteEntityAction(gameObject), [&]() { - Commands::DeleteEntity(context, gameObject->GetID()); - }); - DrawMenuSeparator(); - DrawMenuAction(MakeCopyEntityAction(gameObject), [&]() { - Commands::CopyEntity(context, gameObject->GetID()); - }); - DrawMenuAction(MakePasteEntityAction(context), [&]() { - Commands::PasteEntity(context, gameObject->GetID()); - }); - DrawMenuAction(MakeDuplicateEntityAction(gameObject), [&]() { - Commands::DuplicateEntity(context, gameObject->GetID()); - }); -} - -inline void DrawHierarchyEntityContextPopup( - IEditorContext& context, - UI::TargetedPopupState<::XCEngine::Components::GameObject*>& itemContextMenu) { - itemContextMenu.ConsumeOpenRequest("HierarchyEntityContextMenu"); - - if (!UI::BeginPopup("HierarchyEntityContextMenu")) { - return; - } - - if (itemContextMenu.HasTarget()) { - DrawHierarchyContextActions(context, itemContextMenu.TargetValue()); - } - UI::EndPopup(); - - if (!ImGui::IsPopupOpen("HierarchyEntityContextMenu") && !itemContextMenu.HasPendingOpenRequest()) { - itemContextMenu.Clear(); - } -} - } // namespace Actions } // namespace Editor } // namespace XCEngine diff --git a/editor/src/Actions/InspectorActionRouter.h b/editor/src/Actions/InspectorActionRouter.h index d755b8a4..ad37bbf7 100644 --- a/editor/src/Actions/InspectorActionRouter.h +++ b/editor/src/Actions/InspectorActionRouter.h @@ -90,17 +90,6 @@ inline void DrawInspectorAddComponentPopup( UI::EndTitledPopup(); } -inline bool DrawInspectorComponentMenu( - IEditorContext& context, - ::XCEngine::Components::Component* component, - ::XCEngine::Components::GameObject* gameObject, - const IComponentEditor* editor) { - const bool canRemove = Commands::CanRemoveComponent(component, editor); - return DrawMenuAction(MakeRemoveComponentAction(canRemove), [&]() { - Commands::RemoveComponent(context, component, gameObject, editor); - }); -} - inline void FinalizeInspectorInteractiveChangeIfIdle(IEditorContext& context) { if (context.GetUndoManager().HasPendingInteractiveChange() && !ImGui::IsAnyItemActive()) { context.GetUndoManager().FinalizeInteractiveChange(); diff --git a/editor/src/Actions/ProjectActionRouter.h b/editor/src/Actions/ProjectActionRouter.h index b2a48bcc..4fa9303f 100644 --- a/editor/src/Actions/ProjectActionRouter.h +++ b/editor/src/Actions/ProjectActionRouter.h @@ -15,8 +15,6 @@ inline constexpr const char* ProjectAssetPayloadType() { return "ASSET_ITEM"; } -inline void DrawProjectAssetContextActions(IEditorContext& context, const AssetItemPtr& item); - inline const char* GetDraggedProjectAssetPath() { const ImGuiPayload* payload = ImGui::GetDragDropPayload(); if (!payload || !payload->IsDataType(ProjectAssetPayloadType())) { @@ -71,16 +69,16 @@ inline void HandleProjectBackgroundPrimaryClick( } } +inline void HandleProjectItemSelection(IProjectManager& projectManager, const AssetItemPtr& item) { + projectManager.SetSelectedItem(item); +} + inline void RequestProjectEmptyContextPopup(UI::DeferredPopupState& emptyContextMenu) { if (ImGui::IsWindowHovered() && ImGui::IsMouseClicked(1) && !ImGui::IsAnyItemHovered()) { emptyContextMenu.RequestOpen(); } } -inline void HandleProjectItemSelection(IProjectManager& projectManager, const AssetItemPtr& item) { - projectManager.SetSelectedItem(item); -} - inline void HandleProjectItemContextRequest( IProjectManager& projectManager, const AssetItemPtr& item, @@ -89,50 +87,6 @@ inline void HandleProjectItemContextRequest( itemContextMenu.RequestOpen(item); } -inline void DrawProjectItemContextPopup(IEditorContext& context, UI::TargetedPopupState& itemContextMenu) { - itemContextMenu.ConsumeOpenRequest("ItemContextMenu"); - if (UI::BeginPopup("ItemContextMenu")) { - if (itemContextMenu.HasTarget()) { - DrawProjectAssetContextActions(context, itemContextMenu.TargetValue()); - } - UI::EndPopup(); - } - - if (!ImGui::IsPopupOpen("ItemContextMenu") && !itemContextMenu.HasPendingOpenRequest()) { - itemContextMenu.Clear(); - } -} - -inline void DrawProjectAssetContextActions(IEditorContext& context, const AssetItemPtr& item) { - auto& projectManager = context.GetProjectManager(); - const bool hasTarget = item != nullptr && !item->fullPath.empty(); - - DrawMenuAction(MakeOpenAssetAction(Commands::CanOpenAsset(item)), [&]() { - Commands::OpenAsset(context, item); - }); - DrawMenuSeparator(); - DrawMenuAction(MakeDeleteAssetAction(hasTarget), [&]() { - Commands::DeleteAsset(projectManager, item); - }); -} - -template -inline void DrawProjectEmptyContextPopup( - UI::DeferredPopupState& emptyContextMenu, - CreateFolderFn&& createFolder) { - emptyContextMenu.ConsumeOpenRequest("EmptyContextMenu"); - - if (!UI::BeginPopup("EmptyContextMenu")) { - return; - } - - DrawMenuAction(MakeCreateFolderAction(), [&]() { - createFolder(); - }); - - UI::EndPopup(); -} - } // namespace Actions } // namespace Editor } // namespace XCEngine diff --git a/editor/src/Application.cpp b/editor/src/Application.cpp index 18beaeea..75355bb5 100644 --- a/editor/src/Application.cpp +++ b/editor/src/Application.cpp @@ -135,14 +135,19 @@ bool Application::Initialize(HWND hwnd) { } m_hwnd = hwnd; + logger.Info(Debug::LogCategory::General, "Initializing editor window renderer..."); if (!InitializeWindowRenderer(hwnd)) { return false; } + logger.Info(Debug::LogCategory::General, "Initializing editor context..."); InitializeEditorContext(projectRoot); + logger.Info(Debug::LogCategory::General, "Initializing ImGui backend..."); InitializeImGui(hwnd); + logger.Info(Debug::LogCategory::General, "Attaching editor layer..."); AttachEditorLayer(); + logger.Info(Debug::LogCategory::General, "Editor initialization completed."); m_renderReady = true; return true; } diff --git a/editor/src/Platform/WindowsProcessDiagnostics.h b/editor/src/Platform/WindowsProcessDiagnostics.h index 82c750fa..16eb2b5a 100644 --- a/editor/src/Platform/WindowsProcessDiagnostics.h +++ b/editor/src/Platform/WindowsProcessDiagnostics.h @@ -2,6 +2,7 @@ #include "Platform/Win32Utf8.h" +#include #include #include #include @@ -14,6 +15,49 @@ inline std::string GetExecutableLogPath(const char* fileName) { return GetExecutableDirectoryUtf8() + "\\" + fileName; } +inline void WriteCrashStackTrace(FILE* file) { + if (file == nullptr) { + return; + } + + HANDLE process = GetCurrentProcess(); + SymSetOptions(SYMOPT_LOAD_LINES | SYMOPT_UNDNAME | SYMOPT_DEFERRED_LOADS); + if (!SymInitialize(process, nullptr, TRUE)) { + fprintf(file, "[CRASH] SymInitialize failed: %lu\n", GetLastError()); + return; + } + + void* frames[64] = {}; + const USHORT frameCount = CaptureStackBackTrace(0, 64, frames, nullptr); + + char symbolStorage[sizeof(SYMBOL_INFO) + MAX_SYM_NAME] = {}; + SYMBOL_INFO* symbol = reinterpret_cast(symbolStorage); + symbol->SizeOfStruct = sizeof(SYMBOL_INFO); + symbol->MaxNameLen = MAX_SYM_NAME; + + for (USHORT frameIndex = 0; frameIndex < frameCount; ++frameIndex) { + const DWORD64 address = reinterpret_cast(frames[frameIndex]); + DWORD64 displacement = 0; + IMAGEHLP_LINE64 line = {}; + line.SizeOfStruct = sizeof(line); + DWORD lineDisplacement = 0; + + fprintf(file, "[CRASH] #%u 0x%p", frameIndex, frames[frameIndex]); + + if (SymFromAddr(process, address, &displacement, symbol)) { + fprintf(file, " %s+0x%llX", symbol->Name, static_cast(displacement)); + } + + if (SymGetLineFromAddr64(process, address, &lineDisplacement, &line)) { + fprintf(file, " (%s:%lu)", line.FileName, line.LineNumber); + } + + fprintf(file, "\n"); + } + + SymCleanup(process); +} + inline LONG WINAPI CrashExceptionFilter(EXCEPTION_POINTERS* exceptionPointers) { const std::string logPath = GetExecutableLogPath("crash.log"); @@ -25,6 +69,7 @@ inline LONG WINAPI CrashExceptionFilter(EXCEPTION_POINTERS* exceptionPointers) { "[CRASH] ExceptionCode=0x%08X, Address=0x%p\n", exceptionPointers->ExceptionRecord->ExceptionCode, exceptionPointers->ExceptionRecord->ExceptionAddress); + WriteCrashStackTrace(file); fclose(file); } diff --git a/editor/src/UI/ContextMenu.h b/editor/src/UI/ContextMenu.h new file mode 100644 index 00000000..67733d67 --- /dev/null +++ b/editor/src/UI/ContextMenu.h @@ -0,0 +1,123 @@ +#pragma once + +#include "Core.h" +#include "StyleTokens.h" +#include "Widgets.h" + +#include + +#include +#include + +namespace XCEngine { +namespace Editor { +namespace UI { + +inline void TraceContextMenuSubmenuIfNeeded(const char* label, const std::string& message) { + if (!label || std::string(label) != "Create") { + return; + } + + XCEngine::Debug::Logger::Get().Info( + XCEngine::Debug::LogCategory::General, + XCEngine::Containers::String(message.c_str())); +} + +inline constexpr int ContextMenuLayoutVarCount() { + return 3; +} + +inline void PushContextMenuLayoutStyle() { + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ContextMenuItemSpacing()); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ContextMenuFramePadding()); + ImGui::PushStyleVar(ImGuiStyleVar_SelectableTextAlign, ContextMenuSelectableTextAlign()); +} + +inline void PopContextMenuLayoutStyle() { + ImGui::PopStyleVar(ContextMenuLayoutVarCount()); +} + +inline void PushContextMenuChrome() { + PushPopupChromeStyle(); + PushContextMenuLayoutStyle(); +} + +inline void PopContextMenuChrome() { + PopContextMenuLayoutStyle(); + PopPopupChromeStyle(); +} + +inline bool BeginContextMenu( + const char* id, + ImGuiWindowFlags flags = ImGuiWindowFlags_None) { + PushContextMenuChrome(); + const bool open = ImGui::BeginPopup(id, flags); + if (!open) { + PopContextMenuChrome(); + } + return open; +} + +inline bool BeginContextMenuForLastItem( + const char* id = nullptr, + ImGuiPopupFlags flags = ImGuiPopupFlags_MouseButtonRight) { + PushContextMenuChrome(); + const bool open = ImGui::BeginPopupContextItem(id, flags); + if (!open) { + PopContextMenuChrome(); + } + return open; +} + +inline bool BeginContextMenuForWindow( + const char* id = nullptr, + ImGuiPopupFlags flags = + ImGuiPopupFlags_MouseButtonRight | + ImGuiPopupFlags_NoOpenOverItems | + ImGuiPopupFlags_NoOpenOverExistingPopup) { + PushContextMenuChrome(); + const bool open = ImGui::BeginPopupContextWindow(id, flags); + if (!open) { + PopContextMenuChrome(); + } + return open; +} + +inline void EndContextMenu() { + ImGui::EndPopup(); + PopContextMenuChrome(); +} + +template +inline bool DrawContextSubmenu( + const char* label, + DrawContentFn&& drawContent, + bool enabled = true) { + PushPopupWindowChrome(); + const bool open = ImGui::BeginMenu(label, enabled); + PopPopupWindowChrome(); + if (label && std::string(label) == "Create") { + static bool s_lastOpen = false; + if (open != s_lastOpen) { + TraceContextMenuSubmenuIfNeeded( + label, + std::string("Hierarchy create context submenu ") + (open ? "opened" : "closed")); + s_lastOpen = open; + } + } + if (!open) { + return false; + } + + PushPopupContentChrome(); + PushContextMenuLayoutStyle(); + drawContent(); + PopContextMenuLayoutStyle(); + PopPopupContentChrome(); + ImGui::EndMenu(); + return true; +} + +} // namespace UI +} // namespace Editor +} // namespace XCEngine diff --git a/editor/src/UI/Core.h b/editor/src/UI/Core.h index 2f24bfb7..d8ff8857 100644 --- a/editor/src/UI/Core.h +++ b/editor/src/UI/Core.h @@ -3,6 +3,7 @@ #include "DividerChrome.h" #include "StyleTokens.h" +#include #include namespace XCEngine { @@ -38,27 +39,41 @@ inline void DrawDisclosureArrow(ImDrawList* drawList, const ImVec2& min, const I return; } - const ImVec2 center((min.x + max.x) * 0.5f, (min.y + max.y) * 0.5f); + const ImVec2 center( + static_cast(std::floor((min.x + max.x) * 0.5f)) + 0.5f, + static_cast(std::floor((min.y + max.y) * 0.5f)) + 0.5f); const float width = max.x - min.x; const float height = max.y - min.y; - const float size = (width < height ? width : height) * DisclosureArrowScale(); - if (size <= 0.0f) { + const float radius = (std::max)( + 3.0f, + static_cast(std::floor((width < height ? width : height) * DisclosureArrowScale()))); + const float baseHalfExtent = radius; + const float tipExtent = (std::max)(2.0f, static_cast(std::floor(radius * 0.70f))); + if (baseHalfExtent < 1.0f || tipExtent < 1.0f) { return; } - if (open) { - drawList->AddTriangleFilled( - ImVec2(center.x - size, center.y - size * 0.45f), - ImVec2(center.x + size, center.y - size * 0.45f), - ImVec2(center.x, center.y + size), - color); - return; + ImVec2 points[3] = { + ImVec2(-baseHalfExtent, -tipExtent), + ImVec2(baseHalfExtent, -tipExtent), + ImVec2(0.0f, tipExtent) + }; + + if (!open) { + for (ImVec2& point : points) { + point = ImVec2(point.y, -point.x); + } + } + + for (ImVec2& point : points) { + point.x += center.x; + point.y += center.y; } drawList->AddTriangleFilled( - ImVec2(center.x - size * 0.45f, center.y - size), - ImVec2(center.x - size * 0.45f, center.y + size), - ImVec2(center.x + size, center.y), + points[0], + points[1], + points[2], color); } diff --git a/editor/src/UI/ImGuiSession.h b/editor/src/UI/ImGuiSession.h index b62f20aa..807d4ac5 100644 --- a/editor/src/UI/ImGuiSession.h +++ b/editor/src/UI/ImGuiSession.h @@ -120,7 +120,6 @@ private: } io.FontDefault = uiFont; - atlas->Build(); } std::string m_iniPath; diff --git a/editor/src/UI/MenuCommand.h b/editor/src/UI/MenuCommand.h new file mode 100644 index 00000000..2500e195 --- /dev/null +++ b/editor/src/UI/MenuCommand.h @@ -0,0 +1,34 @@ +#pragma once + +namespace XCEngine { +namespace Editor { +namespace UI { + +enum class MenuCommandKind { + Action, + Separator +}; + +struct MenuCommand { + MenuCommandKind kind = MenuCommandKind::Action; + const char* label = nullptr; + const char* shortcut = nullptr; + bool selected = false; + bool enabled = true; + + static MenuCommand Action( + const char* label, + const char* shortcut = nullptr, + bool selected = false, + bool enabled = true) { + return MenuCommand{ MenuCommandKind::Action, label, shortcut, selected, enabled }; + } + + static MenuCommand Separator() { + return MenuCommand{ MenuCommandKind::Separator, nullptr, nullptr, false, true }; + } +}; + +} // namespace UI +} // namespace Editor +} // namespace XCEngine diff --git a/editor/src/UI/StyleTokens.h b/editor/src/UI/StyleTokens.h index 5bce5594..5c88088a 100644 --- a/editor/src/UI/StyleTokens.h +++ b/editor/src/UI/StyleTokens.h @@ -6,6 +6,8 @@ namespace XCEngine { namespace Editor { namespace UI { +inline float PopupWindowBorderSize(); + inline ImVec2 DockHostFramePadding() { return ImVec2(4.0f, 2.0f); } @@ -246,6 +248,10 @@ inline float CompactNavigationTreeIndentSpacing() { return 14.0f; } +inline float HierarchyTreeIndentSpacing() { + return 18.0f; +} + inline float NavigationTreeIconSize() { return 18.0f; } @@ -263,7 +269,7 @@ inline float NavigationTreePrefixLabelGap() { } inline float DisclosureArrowScale() { - return 0.14f; + return 0.28f; } inline ImVec4 NavigationTreePrefixColor(bool selected = false, bool hovered = false) { @@ -295,7 +301,9 @@ inline TreeViewStyle NavigationTreeStyle() { } inline TreeViewStyle HierarchyTreeStyle() { - return NavigationTreeStyle(); + TreeViewStyle style = NavigationTreeStyle(); + style.indentSpacing = HierarchyTreeIndentSpacing(); + return style; } inline TreeViewStyle ProjectFolderTreeStyle() { @@ -338,6 +346,18 @@ inline float SearchFieldFrameRounding() { return 2.0f; } +inline ImVec2 InlineRenameFieldFramePadding() { + return ImVec2(4.0f, 0.0f); +} + +inline float InlineRenameFieldRounding() { + return 2.0f; +} + +inline float InlineRenameFieldHeight() { + return ImGui::GetFontSize() + InlineRenameFieldFramePadding().y * 2.0f; +} + inline ImU32 PanelDividerColor() { return ImGui::GetColorU32(PanelSplitterIdleColor()); } @@ -476,6 +496,18 @@ inline float PopupFrameRounding() { return 3.0f; } +inline ImVec2 ContextMenuItemSpacing() { + return ImVec2(0.0f, 0.0f); +} + +inline ImVec2 ContextMenuFramePadding() { + return ImVec2(8.0f, 5.0f); +} + +inline ImVec2 ContextMenuSelectableTextAlign() { + return ImVec2(0.0f, 0.5f); +} + inline float PopupSubmenuArrowExtent() { return 8.0f; } diff --git a/editor/src/UI/UI.h b/editor/src/UI/UI.h index df15f5ca..ef79d716 100644 --- a/editor/src/UI/UI.h +++ b/editor/src/UI/UI.h @@ -5,10 +5,12 @@ #include "BuiltInIcons.h" #include "ConsoleFilterState.h" #include "ConsoleLogFormatter.h" +#include "ContextMenu.h" #include "Core.h" #include "DockHostStyle.h" #include "DockTabBarChrome.h" #include "DividerChrome.h" +#include "MenuCommand.h" #include "PanelChrome.h" #include "PopupState.h" #include "PropertyLayout.h" diff --git a/editor/src/UI/Widgets.h b/editor/src/UI/Widgets.h index 58d440d3..d6ba39dc 100644 --- a/editor/src/UI/Widgets.h +++ b/editor/src/UI/Widgets.h @@ -1,8 +1,11 @@ #pragma once #include "Core.h" +#include "MenuCommand.h" #include "StyleTokens.h" +#include + #include #include @@ -10,6 +13,16 @@ namespace XCEngine { namespace Editor { namespace UI { +inline void TracePopupSubmenuIfNeeded(const char* label, const std::string& message) { + if (!label || std::string(label) != "Create") { + return; + } + + XCEngine::Debug::Logger::Get().Info( + XCEngine::Debug::LogCategory::General, + XCEngine::Containers::String(message.c_str())); +} + struct ComponentSectionResult { bool open = false; float contentIndent = 0.0f; @@ -17,7 +30,6 @@ struct ComponentSectionResult { struct AssetTileResult { bool clicked = false; - bool contextRequested = false; bool openRequested = false; bool hovered = false; ImVec2 min = ImVec2(0.0f, 0.0f); @@ -41,29 +53,11 @@ enum class DialogActionResult { Secondary }; -enum class MenuCommandKind { - Action, - Separator -}; - -struct MenuCommand { - MenuCommandKind kind = MenuCommandKind::Action; - const char* label = nullptr; - const char* shortcut = nullptr; - bool selected = false; - bool enabled = true; - - static MenuCommand Action( - const char* label, - const char* shortcut = nullptr, - bool selected = false, - bool enabled = true) { - return MenuCommand{ MenuCommandKind::Action, label, shortcut, selected, enabled }; - } - - static MenuCommand Separator() { - return MenuCommand{ MenuCommandKind::Separator, nullptr, nullptr, false, true }; - } +struct InlineRenameFieldResult { + bool submitted = false; + bool cancelRequested = false; + bool deactivated = false; + bool active = false; }; template @@ -101,11 +95,13 @@ inline bool DrawPopupSubmenuScope(const char* label, DrawContentFn&& drawContent popupOpen, ImGuiSelectableFlags_NoAutoClosePopups, ImVec2(rowWidth, rowHeight))) { + TracePopupSubmenuIfNeeded(label, "Hierarchy create submenu selectable clicked -> OpenPopup"); ImGui::OpenPopup(popupId); } const bool hovered = ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup); if (hovered && !popupOpen) { + TracePopupSubmenuIfNeeded(label, "Hierarchy create submenu hovered -> OpenPopup"); ImGui::OpenPopup(popupId); } @@ -138,6 +134,15 @@ inline bool DrawPopupSubmenuScope(const char* label, DrawContentFn&& drawContent ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoSavedSettings); + if (std::string(label) == "Create") { + static bool s_lastCreateOpen = false; + if (open != s_lastCreateOpen) { + TracePopupSubmenuIfNeeded( + label, + std::string("Hierarchy create submenu popup ") + (open ? "opened" : "closed")); + s_lastCreateOpen = open; + } + } if (!open) { ImGui::PopID(); return false; @@ -146,6 +151,12 @@ inline bool DrawPopupSubmenuScope(const char* label, DrawContentFn&& drawContent drawContent(); const bool popupHovered = ImGui::IsWindowHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup); if (!hovered && !popupHovered && !ImGui::IsWindowAppearing()) { + TracePopupSubmenuIfNeeded( + label, + std::string("Hierarchy create submenu auto-close: rowHovered=") + + (hovered ? "1" : "0") + + ", popupHovered=" + + (popupHovered ? "1" : "0")); ImGui::CloseCurrentPopup(); } EndPopup(); @@ -190,6 +201,50 @@ inline bool ToolbarSearchField( return changed; } +inline InlineRenameFieldResult DrawInlineRenameField( + const char* id, + char* buffer, + size_t bufferSize, + float width = -1.0f, + bool requestFocus = false, + ImGuiInputTextFlags flags = ImGuiInputTextFlags_EnterReturnsTrue | ImGuiInputTextFlags_AutoSelectAll) { + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, InlineRenameFieldFramePadding()); + ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, InlineRenameFieldRounding()); + ImGui::SetNextItemWidth(width); + if (requestFocus) { + ImGui::SetKeyboardFocusHere(); + } + + const bool submitted = ImGui::InputText(id, buffer, bufferSize, flags); + const bool active = ImGui::IsItemActive(); + const bool deactivated = ImGui::IsItemDeactivated(); + const bool cancelRequested = active && ImGui::IsKeyPressed(ImGuiKey_Escape); + ImGui::PopStyleVar(2); + + return InlineRenameFieldResult{ submitted, cancelRequested, deactivated, active }; +} + +inline InlineRenameFieldResult DrawInlineRenameFieldAt( + const char* id, + const ImVec2& screenPos, + char* buffer, + size_t bufferSize, + float width, + bool requestFocus = false, + ImGuiInputTextFlags flags = ImGuiInputTextFlags_EnterReturnsTrue | ImGuiInputTextFlags_AutoSelectAll) { + const ImVec2 restoreCursor = ImGui::GetCursorPos(); + ImGui::SetCursorScreenPos(screenPos); + const InlineRenameFieldResult result = DrawInlineRenameField( + id, + buffer, + bufferSize, + width, + requestFocus, + flags); + ImGui::SetCursorPos(restoreCursor); + return result; +} + inline void DrawToolbarLabel(const char* text) { ImGui::AlignTextToFramePadding(); ImGui::TextColored(HintTextColor(), "%s", text); @@ -271,16 +326,22 @@ inline bool DrawBreadcrumbSegment(const char* label, bool clickable, bool curren const ImVec2 padding = BreadcrumbSegmentPadding(); const ImVec2 textSize = ImGui::CalcTextSize(label); const ImVec2 size(textSize.x + padding.x * 2.0f, BreadcrumbItemHeight()); - bool pressed = false; - bool hovered = false; - DrawBreadcrumbTextItem(label, size, BreadcrumbSegmentTextColor(current, hovered), clickable, &pressed, &hovered); + ImGui::InvisibleButton("##BreadcrumbItem", size); + + const bool hovered = clickable && ImGui::IsItemHovered(); + const bool pressed = clickable && ImGui::IsItemClicked(ImGuiMouseButton_Left); + + const ImVec2 itemMin = ImGui::GetItemRectMin(); + const ImVec2 itemMax = ImGui::GetItemRectMax(); + const float textX = itemMin.x + (size.x - textSize.x) * 0.5f; + const float textY = itemMin.y + (itemMax.y - itemMin.y - textSize.y) * 0.5f; + + ImGui::GetWindowDrawList()->AddText( + ImVec2(textX, textY), + ImGui::GetColorU32(BreadcrumbSegmentTextColor(current, hovered)), + label); if (hovered) { - const ImVec2 itemMin = ImGui::GetItemRectMin(); - const ImVec2 itemMax = ImGui::GetItemRectMax(); - const ImVec2 textOnlySize = ImGui::CalcTextSize(label); - const float textX = itemMin.x + (size.x - textOnlySize.x) * 0.5f; - const float textY = itemMin.y + (itemMax.y - itemMin.y - textOnlySize.y) * 0.5f; ImGui::GetWindowDrawList()->AddText( ImVec2(textX, textY), ImGui::GetColorU32(BreadcrumbSegmentTextColor(current, true)), @@ -301,7 +362,10 @@ inline void DrawToolbarBreadcrumbs( size_t segmentCount, GetNameFn&& getName, NavigateFn&& navigateToSegment) { + const float lineY = ImGui::GetCursorPosY(); + if (segmentCount == 0) { + ImGui::SetCursorPosY(lineY); DrawBreadcrumbSegment(rootLabel, false, true); return; } @@ -309,8 +373,10 @@ inline void DrawToolbarBreadcrumbs( for (size_t i = 0; i < segmentCount; ++i) { if (i > 0) { ImGui::SameLine(0.0f, BreadcrumbSegmentSpacing()); + ImGui::SetCursorPosY(lineY); DrawBreadcrumbSeparator(); ImGui::SameLine(0.0f, BreadcrumbSegmentSpacing()); + ImGui::SetCursorPosY(lineY); } const std::string label = (i == 0 && rootLabel && rootLabel[0] != '\0') @@ -318,6 +384,7 @@ inline void DrawToolbarBreadcrumbs( : getName(i); const bool current = (i + 1 == segmentCount); + ImGui::SetCursorPosY(lineY); ImGui::PushID(static_cast(i)); if (DrawBreadcrumbSegment(label.c_str(), !current, current)) { navigateToSegment(i); @@ -346,7 +413,6 @@ inline AssetTileResult DrawAssetTile( ImGui::InvisibleButton("##AssetBtn", tileSize); const bool clicked = ImGui::IsItemClicked(ImGuiMouseButton_Left); - const bool contextRequested = ImGui::IsItemClicked(ImGuiMouseButton_Right); const bool openRequested = ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(0); const bool hovered = ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem); @@ -388,7 +454,7 @@ inline AssetTileResult DrawAssetTile( ImGui::PopClipRect(); } - return AssetTileResult{ clicked, contextRequested, openRequested, hovered, min, max, labelMin, labelMax }; + return AssetTileResult{ clicked, openRequested, hovered, min, max, labelMin, labelMax }; } template @@ -397,6 +463,7 @@ inline ComponentSectionResult BeginComponentSection( const char* label, DrawMenuFn&& drawMenu, bool defaultOpen = true) { + (void)drawMenu; const ImGuiStyle& style = ImGui::GetStyle(); const ImVec2 framePadding = InspectorSectionFramePadding(); const float availableWidth = ImMax(ImGui::GetContentRegionAvail().x, 1.0f); @@ -453,11 +520,6 @@ inline ComponentSectionResult BeginComponentSection( drawList->PopClipRect(); } - if (BeginPopupContextItem("##ComponentSettings")) { - drawMenu(); - EndPopup(); - } - ImGui::PopID(); return ComponentSectionResult{ open, InspectorSectionContentIndent() }; } diff --git a/editor/src/Viewport/IViewportHostService.h b/editor/src/Viewport/IViewportHostService.h index 9f5e9c80..6d2f6cbc 100644 --- a/editor/src/Viewport/IViewportHostService.h +++ b/editor/src/Viewport/IViewportHostService.h @@ -33,6 +33,7 @@ struct SceneViewportInput { ImVec2 viewportSize = ImVec2(0.0f, 0.0f); ImVec2 mouseDelta = ImVec2(0.0f, 0.0f); float mouseWheel = 0.0f; + float flySpeedDelta = 0.0f; float deltaTime = 0.0f; float moveForward = 0.0f; float moveRight = 0.0f; @@ -65,6 +66,10 @@ public: virtual void BeginFrame() = 0; virtual EditorViewportFrame RequestViewport(EditorViewportKind kind, const ImVec2& requestedSize) = 0; virtual void UpdateSceneViewInput(IEditorContext& context, const SceneViewportInput& input) = 0; + virtual uint64_t PickSceneViewEntity( + IEditorContext& context, + const ImVec2& viewportSize, + const ImVec2& viewportMousePosition) = 0; virtual SceneViewportOverlayData GetSceneViewOverlayData() const = 0; virtual void RenderRequestedViewports( IEditorContext& context, diff --git a/editor/src/Viewport/SceneViewportCameraController.h b/editor/src/Viewport/SceneViewportCameraController.h index ca0a2f98..71b9128f 100644 --- a/editor/src/Viewport/SceneViewportCameraController.h +++ b/editor/src/Viewport/SceneViewportCameraController.h @@ -19,6 +19,7 @@ struct SceneViewportCameraInputState { float panDeltaX = 0.0f; float panDeltaY = 0.0f; float zoomDelta = 0.0f; + float flySpeedDelta = 0.0f; float deltaTime = 0.0f; float moveForward = 0.0f; float moveRight = 0.0f; @@ -32,6 +33,7 @@ public: void Reset() { m_focalPoint = Math::Vector3::Zero(); m_distance = 6.0f; + m_flySpeed = 5.0f; m_yawDegrees = -35.0f; m_pitchDegrees = -20.0f; UpdatePositionFromFocalPoint(); @@ -45,6 +47,10 @@ public: return m_distance; } + float GetFlySpeed() const { + return m_flySpeed; + } + float GetYawDegrees() const { return m_yawDegrees; } @@ -95,22 +101,27 @@ public: const Math::Vector3 up = GetUp(); const float worldUnitsPerPixel = ComputeWorldUnitsPerPixel(input.viewportHeight); const Math::Vector3 delta = - ((right * input.panDeltaX) + (up * -input.panDeltaY)) * worldUnitsPerPixel; + ((right * -input.panDeltaX) + (up * input.panDeltaY)) * worldUnitsPerPixel; m_focalPoint += delta; m_position += delta; } + if (std::abs(input.flySpeedDelta) > Math::EPSILON) { + const float speedFactor = std::pow(1.20f, input.flySpeedDelta); + m_flySpeed = std::clamp(m_flySpeed * speedFactor, 0.5f, 500.0f); + } + if (input.deltaTime > 0.0f && (std::abs(input.moveForward) > Math::EPSILON || std::abs(input.moveRight) > Math::EPSILON || std::abs(input.moveUp) > Math::EPSILON)) { const Math::Vector3 movement = - GetForward() * -input.moveForward + - GetRight() * -input.moveRight + + GetForward() * input.moveForward + + GetRight() * input.moveRight + Math::Vector3::Up() * input.moveUp; if (movement.SqrMagnitude() > Math::EPSILON) { const float speedMultiplier = input.fastMove ? 4.0f : 1.0f; - const float flySpeed = (std::max)(5.0f, m_distance * 2.0f) * speedMultiplier; + const float flySpeed = m_flySpeed * speedMultiplier; const Math::Vector3 delta = Math::Vector3::Normalize(movement) * flySpeed * input.deltaTime; m_position += delta; m_focalPoint += delta; @@ -135,7 +146,7 @@ public: private: void ApplyRotationDelta(float deltaX, float deltaY) { m_yawDegrees += deltaX * 0.30f; - m_pitchDegrees = std::clamp(m_pitchDegrees + deltaY * 0.20f, -89.0f, 89.0f); + m_pitchDegrees = std::clamp(m_pitchDegrees - deltaY * 0.20f, -89.0f, 89.0f); } Math::Vector3 GetRight() const { @@ -166,6 +177,7 @@ private: Math::Vector3 m_position = Math::Vector3::Zero(); Math::Vector3 m_focalPoint = Math::Vector3::Zero(); float m_distance = 6.0f; + float m_flySpeed = 5.0f; float m_yawDegrees = -35.0f; float m_pitchDegrees = -20.0f; }; diff --git a/editor/src/Viewport/SceneViewportGrid.cpp b/editor/src/Viewport/SceneViewportGrid.cpp new file mode 100644 index 00000000..0934f0d1 --- /dev/null +++ b/editor/src/Viewport/SceneViewportGrid.cpp @@ -0,0 +1,71 @@ +#include "SceneViewportGrid.h" + +#include +#include + +namespace XCEngine { +namespace Editor { + +namespace { + +constexpr float kCameraHeightScaleFactor = 0.50f; +constexpr float kTransitionStart = 0.65f; +constexpr float kTransitionEnd = 0.95f; +constexpr float kMinimumVerticalViewComponent = 0.15f; + +float SnapGridSpacing(float targetSpacing) { + const float clampedTarget = (std::max)(targetSpacing, 0.02f); + const float exponent = std::floor(std::log10(clampedTarget)); + return std::pow(10.0f, exponent); +} + +float ComputeTransitionBlend(float targetSpacing, float baseScale) { + const float normalizedSpacing = (std::max)(targetSpacing / (std::max)(baseScale, 1e-4f), 1.0f); + const float transitionPosition = std::log10(normalizedSpacing); + const float t = + (transitionPosition - kTransitionStart) / + (kTransitionEnd - kTransitionStart); + const float saturated = (std::clamp)(t, 0.0f, 1.0f); + return saturated * saturated * (3.0f - 2.0f * saturated); +} + +float ComputeViewDistanceToGridPlane(const SceneViewportOverlayData& overlay) { + const float cameraHeight = std::abs(overlay.cameraPosition.y); + const Math::Vector3 forward = overlay.cameraForward.Normalized(); + + const bool lookingTowardGrid = + (overlay.cameraPosition.y >= 0.0f && forward.y < 0.0f) || + (overlay.cameraPosition.y < 0.0f && forward.y > 0.0f); + if (!lookingTowardGrid) { + return cameraHeight; + } + + const float verticalViewComponent = (std::max)(std::abs(forward.y), kMinimumVerticalViewComponent); + return cameraHeight / verticalViewComponent; +} + +} // namespace + +SceneGridParameters BuildSceneGridParameters(const SceneViewportOverlayData& overlay) { + SceneGridParameters parameters = {}; + if (!overlay.valid) { + return parameters; + } + + const float cameraHeight = std::abs(overlay.cameraPosition.y); + const float viewDistance = ComputeViewDistanceToGridPlane(overlay); + // Keep grid density stable while orbiting/looking around. Rotation should + // only affect how much of the infinite grid is visible, not its base LOD. + const float targetSpacing = (std::max)(cameraHeight * kCameraHeightScaleFactor, 0.1f); + + parameters.valid = true; + parameters.baseScale = SnapGridSpacing(targetSpacing); + parameters.transitionBlend = ComputeTransitionBlend(targetSpacing, parameters.baseScale); + parameters.fadeDistance = (std::max)( + parameters.baseScale * 320.0f, + viewDistance * 80.0f); + return parameters; +} + +} // namespace Editor +} // namespace XCEngine diff --git a/editor/src/Viewport/SceneViewportGrid.h b/editor/src/Viewport/SceneViewportGrid.h new file mode 100644 index 00000000..3e831800 --- /dev/null +++ b/editor/src/Viewport/SceneViewportGrid.h @@ -0,0 +1,18 @@ +#pragma once + +#include "IViewportHostService.h" + +namespace XCEngine { +namespace Editor { + +struct SceneGridParameters { + bool valid = false; + float baseScale = 1.0f; + float transitionBlend = 0.0f; + float fadeDistance = 500.0f; +}; + +SceneGridParameters BuildSceneGridParameters(const SceneViewportOverlayData& overlay); + +} // namespace Editor +} // namespace XCEngine diff --git a/editor/src/Viewport/SceneViewportInfiniteGridPass.cpp b/editor/src/Viewport/SceneViewportInfiniteGridPass.cpp new file mode 100644 index 00000000..d6bacb8e --- /dev/null +++ b/editor/src/Viewport/SceneViewportInfiniteGridPass.cpp @@ -0,0 +1,389 @@ +#include "SceneViewportInfiniteGridPass.h" + +#include "SceneViewportGrid.h" +#include "SceneViewportMath.h" + +#include +#include +#include + +#include + +namespace XCEngine { +namespace Editor { + +namespace { + +const char kInfiniteGridHlsl[] = R"( +cbuffer GridConstants : register(b0) { + float4x4 gViewProjectionMatrix; + float4 gCameraPositionAndScale; + float4 gCameraRightAndFade; + float4 gCameraUpAndTanHalfFov; + float4 gCameraForwardAndAspect; + float4 gViewportNearFar; + float4 gGridTransition; +}; + +struct VSOutput { + float4 position : SV_POSITION; +}; + +VSOutput MainVS(uint vertexId : SV_VertexID) { + static const float2 positions[3] = { + float2(-1.0, -1.0), + float2(-1.0, 3.0), + float2( 3.0, -1.0) + }; + + VSOutput output; + output.position = float4(positions[vertexId], 0.0, 1.0); + return output; +} + +float PristineGridLine(float2 uv) { + float2 deriv = max(fwidth(uv), float2(1e-6, 1e-6)); + float2 uvMod = frac(uv); + float2 uvDist = min(uvMod, 1.0 - uvMod); + float2 distInPixels = uvDist / deriv; + float2 lineAlpha = 1.0 - smoothstep(0.0, 1.0, distInPixels); + float density = max(deriv.x, deriv.y); + float densityFade = 1.0 - smoothstep(0.5, 1.0, density); + return max(lineAlpha.x, lineAlpha.y) * densityFade; +} + +float AxisLineAA(float coord, float deriv) { + float distInPixels = abs(coord) / max(deriv, 1e-6); + return 1.0 - smoothstep(0.0, 1.5, distInPixels); +} + +struct GridLayer { + float minor; + float major; +}; + +GridLayer SampleGridLayer(float2 worldPos2D, float baseScale) { + GridLayer layer; + const float2 gridCoord1 = worldPos2D / baseScale; + const float2 gridCoord10 = worldPos2D / (baseScale * 10.0); + const float grid1 = PristineGridLine(gridCoord1); + const float grid10 = PristineGridLine(gridCoord10); + const float2 deriv1 = fwidth(gridCoord1); + const float lodFactor = smoothstep(0.3, 0.6, max(deriv1.x, deriv1.y)); + + layer.major = max(grid10, grid1 * 0.35); + layer.minor = grid1 * (1.0 - lodFactor); + return layer; +} + +struct PSOutput { + float4 color : SV_TARGET0; + float depth : SV_Depth; +}; + +PSOutput MainPS(VSOutput input) { + const float2 viewportSize = max(gViewportNearFar.xy, float2(1.0, 1.0)); + const float scale = max(gCameraPositionAndScale.w, 1e-4); + const float fadeDistance = max(gCameraRightAndFade.w, scale * 10.0); + const float tanHalfFov = max(gCameraUpAndTanHalfFov.w, 1e-4); + const float aspect = max(gCameraForwardAndAspect.w, 1e-4); + const float transitionBlend = saturate(gGridTransition.x); + + const float2 ndc = float2( + (input.position.x / viewportSize.x) * 2.0 - 1.0, + 1.0 - (input.position.y / viewportSize.y) * 2.0); + + const float3 cameraPosition = gCameraPositionAndScale.xyz; + const float3 rayDirection = normalize( + gCameraForwardAndAspect.xyz + + ndc.x * aspect * tanHalfFov * gCameraRightAndFade.xyz + + ndc.y * tanHalfFov * gCameraUpAndTanHalfFov.xyz); + + if (abs(rayDirection.y) < 1e-5) { + discard; + } + + const float t = -cameraPosition.y / rayDirection.y; + if (t <= gViewportNearFar.z || t >= gViewportNearFar.w) { + discard; + } + + const float3 worldPosition = cameraPosition + rayDirection * t; + const float4 clipPosition = mul(gViewProjectionMatrix, float4(worldPosition, 1.0)); + if (clipPosition.w <= 1e-6) { + discard; + } + + const float depth = clipPosition.z / clipPosition.w; + if (depth <= 0.0 || depth >= 1.0) { + discard; + } + + const float radialFade = + 1.0 - smoothstep(fadeDistance * 0.3, fadeDistance, length(worldPosition - cameraPosition)); + const float normalFade = smoothstep(0.0, 0.15, abs(rayDirection.y)); + const float fadeFactor = radialFade * normalFade; + if (fadeFactor < 1e-3) { + discard; + } + + const float2 worldPos2D = worldPosition.xz; + const GridLayer baseLayer = SampleGridLayer(worldPos2D, scale); + const GridLayer nextLayer = SampleGridLayer(worldPos2D, scale * 10.0); + const float minorGridIntensity = lerp(baseLayer.minor, nextLayer.minor, transitionBlend); + const float majorGridIntensity = lerp(baseLayer.major, nextLayer.major, transitionBlend); + float3 finalColor = float3(0.56, 0.56, 0.56); + float finalAlpha = max( + 0.13 * minorGridIntensity * fadeFactor, + 0.28 * majorGridIntensity * fadeFactor); + + const float2 worldDeriv = max(fwidth(worldPos2D), float2(1e-6, 1e-6)); + const float xAxisAlpha = AxisLineAA(worldPos2D.y, worldDeriv.y) * fadeFactor; + const float zAxisAlpha = AxisLineAA(worldPos2D.x, worldDeriv.x) * fadeFactor; + + const float axisAlpha = max(xAxisAlpha, zAxisAlpha); + finalAlpha = max(finalAlpha, 0.34 * saturate(axisAlpha)); + + if (finalAlpha < 1e-3) { + discard; + } + + PSOutput output; + output.color = float4(finalColor, finalAlpha); + output.depth = depth; + return output; +} +)"; + +struct GridConstants { + Math::Matrix4x4 viewProjection = Math::Matrix4x4::Identity(); + Math::Vector4 cameraPositionAndScale = Math::Vector4::Zero(); + Math::Vector4 cameraRightAndFade = Math::Vector4::Zero(); + Math::Vector4 cameraUpAndTanHalfFov = Math::Vector4::Zero(); + Math::Vector4 cameraForwardAndAspect = Math::Vector4::Zero(); + Math::Vector4 viewportNearFar = Math::Vector4::Zero(); + Math::Vector4 gridTransition = Math::Vector4::Zero(); +}; + +} // namespace + +void SceneViewportInfiniteGridPass::Shutdown() { + DestroyResources(); +} + +bool SceneViewportInfiniteGridPass::Render( + const Rendering::RenderContext& renderContext, + const Rendering::RenderSurface& surface, + const SceneViewportOverlayData& overlay) { + if (!overlay.valid || !renderContext.IsValid() || renderContext.backendType != RHI::RHIType::D3D12) { + return false; + } + + if (!EnsureInitialized(renderContext)) { + return false; + } + + const std::vector& colorAttachments = surface.GetColorAttachments(); + if (colorAttachments.empty() || colorAttachments[0] == nullptr || surface.GetDepthAttachment() == nullptr) { + return false; + } + + const SceneGridParameters parameters = BuildSceneGridParameters(overlay); + if (!parameters.valid) { + return false; + } + + const Math::Matrix4x4 viewProjection = + BuildSceneViewportProjectionMatrix( + overlay, + static_cast(surface.GetWidth()), + static_cast(surface.GetHeight())) * + BuildSceneViewportViewMatrix(overlay); + + const float aspect = surface.GetHeight() > 0 + ? static_cast(surface.GetWidth()) / static_cast(surface.GetHeight()) + : 1.0f; + + GridConstants constants = {}; + constants.viewProjection = viewProjection.Transpose(); + constants.cameraPositionAndScale = Math::Vector4(overlay.cameraPosition, parameters.baseScale); + constants.cameraRightAndFade = Math::Vector4(overlay.cameraRight, parameters.fadeDistance); + constants.cameraUpAndTanHalfFov = Math::Vector4( + overlay.cameraUp, + std::tan(overlay.verticalFovDegrees * Math::DEG_TO_RAD * 0.5f)); + constants.cameraForwardAndAspect = Math::Vector4(overlay.cameraForward, aspect); + constants.viewportNearFar = Math::Vector4( + static_cast(surface.GetWidth()), + static_cast(surface.GetHeight()), + overlay.nearClipPlane, + overlay.farClipPlane); + constants.gridTransition = Math::Vector4(parameters.transitionBlend, 0.0f, 0.0f, 0.0f); + + m_constantSet->WriteConstant(0, &constants, sizeof(constants)); + + RHI::RHICommandList* commandList = renderContext.commandList; + RHI::RHIResourceView* renderTarget = colorAttachments[0]; + commandList->SetRenderTargets(1, &renderTarget, surface.GetDepthAttachment()); + + const RHI::Viewport viewport = { + 0.0f, + 0.0f, + static_cast(surface.GetWidth()), + static_cast(surface.GetHeight()), + 0.0f, + 1.0f + }; + const RHI::Rect scissorRect = { + 0, + 0, + static_cast(surface.GetWidth()), + static_cast(surface.GetHeight()) + }; + + commandList->SetViewport(viewport); + commandList->SetScissorRect(scissorRect); + commandList->SetPrimitiveTopology(RHI::PrimitiveTopology::TriangleList); + commandList->SetPipelineState(m_pipelineState); + + RHI::RHIDescriptorSet* descriptorSets[] = { m_constantSet }; + commandList->SetGraphicsDescriptorSets(0, 1, descriptorSets, m_pipelineLayout); + commandList->Draw(3, 1, 0, 0); + return true; +} + +bool SceneViewportInfiniteGridPass::EnsureInitialized(const Rendering::RenderContext& renderContext) { + if (m_pipelineState != nullptr && + m_pipelineLayout != nullptr && + m_constantPool != nullptr && + m_constantSet != nullptr && + m_device == renderContext.device && + m_backendType == renderContext.backendType) { + return true; + } + + DestroyResources(); + return CreateResources(renderContext); +} + +bool SceneViewportInfiniteGridPass::CreateResources(const Rendering::RenderContext& renderContext) { + if (!renderContext.IsValid() || renderContext.backendType != RHI::RHIType::D3D12) { + return false; + } + + m_device = renderContext.device; + m_backendType = renderContext.backendType; + + RHI::DescriptorSetLayoutBinding constantBinding = {}; + constantBinding.binding = 0; + constantBinding.type = static_cast(RHI::DescriptorType::CBV); + constantBinding.count = 1; + + RHI::DescriptorSetLayoutDesc constantLayout = {}; + constantLayout.bindings = &constantBinding; + constantLayout.bindingCount = 1; + + RHI::RHIPipelineLayoutDesc pipelineLayoutDesc = {}; + pipelineLayoutDesc.setLayouts = &constantLayout; + pipelineLayoutDesc.setLayoutCount = 1; + m_pipelineLayout = m_device->CreatePipelineLayout(pipelineLayoutDesc); + if (m_pipelineLayout == nullptr) { + DestroyResources(); + return false; + } + + RHI::DescriptorPoolDesc constantPoolDesc = {}; + constantPoolDesc.type = RHI::DescriptorHeapType::CBV_SRV_UAV; + constantPoolDesc.descriptorCount = 1; + constantPoolDesc.shaderVisible = false; + m_constantPool = m_device->CreateDescriptorPool(constantPoolDesc); + if (m_constantPool == nullptr) { + DestroyResources(); + return false; + } + + m_constantSet = m_constantPool->AllocateSet(constantLayout); + if (m_constantSet == nullptr) { + DestroyResources(); + return false; + } + + RHI::GraphicsPipelineDesc pipelineDesc = {}; + pipelineDesc.pipelineLayout = m_pipelineLayout; + pipelineDesc.topologyType = static_cast(RHI::PrimitiveTopologyType::Triangle); + pipelineDesc.renderTargetCount = 1; + pipelineDesc.renderTargetFormats[0] = static_cast(RHI::Format::R8G8B8A8_UNorm); + pipelineDesc.depthStencilFormat = static_cast(RHI::Format::D24_UNorm_S8_UInt); + pipelineDesc.sampleCount = 1; + + pipelineDesc.rasterizerState.fillMode = static_cast(RHI::FillMode::Solid); + pipelineDesc.rasterizerState.cullMode = static_cast(RHI::CullMode::None); + pipelineDesc.rasterizerState.frontFace = static_cast(RHI::FrontFace::CounterClockwise); + pipelineDesc.rasterizerState.depthClipEnable = true; + + pipelineDesc.blendState.blendEnable = true; + pipelineDesc.blendState.srcBlend = static_cast(RHI::BlendFactor::SrcAlpha); + pipelineDesc.blendState.dstBlend = static_cast(RHI::BlendFactor::InvSrcAlpha); + pipelineDesc.blendState.srcBlendAlpha = static_cast(RHI::BlendFactor::One); + pipelineDesc.blendState.dstBlendAlpha = static_cast(RHI::BlendFactor::InvSrcAlpha); + pipelineDesc.blendState.blendOp = static_cast(RHI::BlendOp::Add); + pipelineDesc.blendState.blendOpAlpha = static_cast(RHI::BlendOp::Add); + pipelineDesc.blendState.colorWriteMask = 0xF; + + pipelineDesc.depthStencilState.depthTestEnable = true; + pipelineDesc.depthStencilState.depthWriteEnable = false; + pipelineDesc.depthStencilState.depthFunc = static_cast(RHI::ComparisonFunc::LessEqual); + + pipelineDesc.vertexShader.source.assign( + kInfiniteGridHlsl, + kInfiniteGridHlsl + std::strlen(kInfiniteGridHlsl)); + pipelineDesc.vertexShader.sourceLanguage = RHI::ShaderLanguage::HLSL; + pipelineDesc.vertexShader.entryPoint = L"MainVS"; + pipelineDesc.vertexShader.profile = L"vs_5_0"; + + pipelineDesc.fragmentShader.source.assign( + kInfiniteGridHlsl, + kInfiniteGridHlsl + std::strlen(kInfiniteGridHlsl)); + pipelineDesc.fragmentShader.sourceLanguage = RHI::ShaderLanguage::HLSL; + pipelineDesc.fragmentShader.entryPoint = L"MainPS"; + pipelineDesc.fragmentShader.profile = L"ps_5_0"; + + m_pipelineState = m_device->CreatePipelineState(pipelineDesc); + if (m_pipelineState == nullptr || !m_pipelineState->IsValid()) { + DestroyResources(); + return false; + } + + return true; +} + +void SceneViewportInfiniteGridPass::DestroyResources() { + if (m_pipelineState != nullptr) { + m_pipelineState->Shutdown(); + delete m_pipelineState; + m_pipelineState = nullptr; + } + + if (m_constantSet != nullptr) { + m_constantSet->Shutdown(); + delete m_constantSet; + m_constantSet = nullptr; + } + + if (m_constantPool != nullptr) { + m_constantPool->Shutdown(); + delete m_constantPool; + m_constantPool = nullptr; + } + + if (m_pipelineLayout != nullptr) { + m_pipelineLayout->Shutdown(); + delete m_pipelineLayout; + m_pipelineLayout = nullptr; + } + + m_device = nullptr; + m_backendType = RHI::RHIType::D3D12; +} + +} // namespace Editor +} // namespace XCEngine diff --git a/editor/src/Viewport/SceneViewportInfiniteGridPass.h b/editor/src/Viewport/SceneViewportInfiniteGridPass.h new file mode 100644 index 00000000..a9c0eadf --- /dev/null +++ b/editor/src/Viewport/SceneViewportInfiniteGridPass.h @@ -0,0 +1,42 @@ +#pragma once + +#include "IViewportHostService.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace XCEngine { +namespace Editor { + +class SceneViewportInfiniteGridPass { +public: + ~SceneViewportInfiniteGridPass() = default; + + void Shutdown(); + + bool Render( + const Rendering::RenderContext& renderContext, + const Rendering::RenderSurface& surface, + const SceneViewportOverlayData& overlay); + +private: + bool EnsureInitialized(const Rendering::RenderContext& renderContext); + bool CreateResources(const Rendering::RenderContext& renderContext); + void DestroyResources(); + +private: + RHI::RHIDevice* m_device = nullptr; + RHI::RHIType m_backendType = RHI::RHIType::D3D12; + RHI::RHIPipelineLayout* m_pipelineLayout = nullptr; + RHI::RHIPipelineState* m_pipelineState = nullptr; + RHI::RHIDescriptorPool* m_constantPool = nullptr; + RHI::RHIDescriptorSet* m_constantSet = nullptr; +}; + +} // namespace Editor +} // namespace XCEngine diff --git a/editor/src/Viewport/SceneViewportMath.h b/editor/src/Viewport/SceneViewportMath.h new file mode 100644 index 00000000..1ca7d6e7 --- /dev/null +++ b/editor/src/Viewport/SceneViewportMath.h @@ -0,0 +1,48 @@ +#pragma once + +#include "IViewportHostService.h" + +#include + +namespace XCEngine { +namespace Editor { + +inline Math::Matrix4x4 BuildSceneViewportViewMatrix(const SceneViewportOverlayData& overlay) { + const Math::Vector3 right = overlay.cameraRight.Normalized(); + const Math::Vector3 up = overlay.cameraUp.Normalized(); + const Math::Vector3 forward = overlay.cameraForward.Normalized(); + + Math::Matrix4x4 view = Math::Matrix4x4::Identity(); + view.m[0][0] = right.x; + view.m[0][1] = right.y; + view.m[0][2] = right.z; + view.m[0][3] = -Math::Vector3::Dot(right, overlay.cameraPosition); + + view.m[1][0] = up.x; + view.m[1][1] = up.y; + view.m[1][2] = up.z; + view.m[1][3] = -Math::Vector3::Dot(up, overlay.cameraPosition); + + view.m[2][0] = forward.x; + view.m[2][1] = forward.y; + view.m[2][2] = forward.z; + view.m[2][3] = -Math::Vector3::Dot(forward, overlay.cameraPosition); + return view; +} + +inline Math::Matrix4x4 BuildSceneViewportProjectionMatrix( + const SceneViewportOverlayData& overlay, + float viewportWidth, + float viewportHeight) { + const float aspect = viewportHeight > 0.0f + ? viewportWidth / viewportHeight + : 1.0f; + return Math::Matrix4x4::Perspective( + overlay.verticalFovDegrees * Math::DEG_TO_RAD, + aspect, + overlay.nearClipPlane, + overlay.farClipPlane); +} + +} // namespace Editor +} // namespace XCEngine diff --git a/editor/src/Viewport/SceneViewportOverlayRenderer.cpp b/editor/src/Viewport/SceneViewportOverlayRenderer.cpp new file mode 100644 index 00000000..a46202fb --- /dev/null +++ b/editor/src/Viewport/SceneViewportOverlayRenderer.cpp @@ -0,0 +1,82 @@ +#include "SceneViewportOverlayRenderer.h" +#include "SceneViewportMath.h" + +namespace XCEngine { +namespace Editor { + +namespace { + +Math::Matrix4x4 BuildOverlayViewMatrix(const SceneViewportOverlayData& overlay) { + return BuildSceneViewportViewMatrix(overlay); +} + +void DrawAxisLabel(ImDrawList* drawList, const ImVec2& position, const char* label, ImU32 color) { + if (drawList == nullptr || label == nullptr) { + return; + } + + const ImVec2 labelSize = ImGui::CalcTextSize(label); + drawList->AddText( + ImVec2(position.x - labelSize.x * 0.5f, position.y - labelSize.y * 0.5f), + color, + label); +} + +void DrawSceneAxisWidget( + ImDrawList* drawList, + const SceneViewportOverlayData& overlay, + const ImVec2& viewportMin, + const ImVec2& viewportMax) { + if (drawList == nullptr || !overlay.valid) { + return; + } + + const Math::Matrix4x4 view = BuildOverlayViewMatrix(overlay); + const ImVec2 center(viewportMax.x - 52.0f, viewportMin.y + 52.0f); + const float radius = 25.0f; + + drawList->AddCircleFilled(center, radius + 12.0f, IM_COL32(17, 19, 22, 178), 24); + drawList->AddCircle(center, radius + 12.0f, IM_COL32(255, 255, 255, 30), 24, 1.0f); + + struct AxisLine { + Math::Vector3 axis; + const char* label; + ImU32 color; + }; + + const AxisLine axes[] = { + { Math::Vector3::Right(), "x", IM_COL32(239, 83, 80, 255) }, + { Math::Vector3::Up(), "y", IM_COL32(102, 187, 106, 255) }, + { Math::Vector3::Forward(), "z", IM_COL32(66, 165, 245, 255) } + }; + + for (const AxisLine& axis : axes) { + const Math::Vector3 viewAxis = view.MultiplyVector(axis.axis); + const ImVec2 end( + center.x + viewAxis.x * radius, + center.y - viewAxis.y * radius); + drawList->AddLine(center, end, axis.color, 2.0f); + drawList->AddCircleFilled(end, 6.0f, axis.color, 16); + DrawAxisLabel(drawList, end, axis.label, IM_COL32(245, 245, 245, 255)); + } +} + +} // namespace + +void DrawSceneViewportOverlay( + ImDrawList* drawList, + const SceneViewportOverlayData& overlay, + const ImVec2& viewportMin, + const ImVec2& viewportMax, + const ImVec2& viewportSize) { + if (drawList == nullptr || !overlay.valid || viewportSize.x <= 1.0f || viewportSize.y <= 1.0f) { + return; + } + + drawList->PushClipRect(viewportMin, viewportMax, true); + DrawSceneAxisWidget(drawList, overlay, viewportMin, viewportMax); + drawList->PopClipRect(); +} + +} // namespace Editor +} // namespace XCEngine diff --git a/editor/src/Viewport/SceneViewportOverlayRenderer.h b/editor/src/Viewport/SceneViewportOverlayRenderer.h new file mode 100644 index 00000000..887d3c5a --- /dev/null +++ b/editor/src/Viewport/SceneViewportOverlayRenderer.h @@ -0,0 +1,18 @@ +#pragma once + +#include "IViewportHostService.h" + +#include + +namespace XCEngine { +namespace Editor { + +void DrawSceneViewportOverlay( + ImDrawList* drawList, + const SceneViewportOverlayData& overlay, + const ImVec2& viewportMin, + const ImVec2& viewportMax, + const ImVec2& viewportSize); + +} // namespace Editor +} // namespace XCEngine diff --git a/editor/src/Viewport/SceneViewportPicker.cpp b/editor/src/Viewport/SceneViewportPicker.cpp new file mode 100644 index 00000000..247c7a75 --- /dev/null +++ b/editor/src/Viewport/SceneViewportPicker.cpp @@ -0,0 +1,316 @@ +#include "SceneViewportPicker.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace XCEngine { +namespace Editor { + +namespace { + +using XCEngine::Components::GameObject; +using XCEngine::Math::Ray; +using XCEngine::Math::Vector2; +using XCEngine::Math::Vector3; +using XCEngine::Resources::Mesh; +using XCEngine::Resources::MeshSection; + +bool IsViewportPositionValid(const Vector2& viewportSize, const Vector2& viewportPosition) { + return viewportSize.x > 1.0f && + viewportSize.y > 1.0f && + viewportPosition.x >= 0.0f && + viewportPosition.y >= 0.0f && + viewportPosition.x <= viewportSize.x && + viewportPosition.y <= viewportSize.y; +} + +bool ReadVertexPosition(const Mesh& mesh, uint32_t vertexIndex, Vector3& outPosition) { + const uint32_t vertexStride = mesh.GetVertexStride(); + const uint8_t* vertexData = static_cast(mesh.GetVertexData()); + if (vertexData == nullptr || vertexStride < sizeof(Vector3) || vertexIndex >= mesh.GetVertexCount()) { + return false; + } + + const size_t byteOffset = static_cast(vertexIndex) * vertexStride; + if (byteOffset + sizeof(Vector3) > mesh.GetVertexDataSize()) { + return false; + } + + std::memcpy(&outPosition, vertexData + byteOffset, sizeof(Vector3)); + return true; +} + +bool ReadIndex(const Mesh& mesh, uint32_t indexOffset, uint32_t& outIndex) { + if (indexOffset >= mesh.GetIndexCount() || mesh.GetIndexData() == nullptr) { + return false; + } + + if (mesh.IsUse32BitIndex()) { + const auto* indices = static_cast(mesh.GetIndexData()); + outIndex = indices[indexOffset]; + return true; + } + + const auto* indices = static_cast(mesh.GetIndexData()); + outIndex = static_cast(indices[indexOffset]); + return true; +} + +bool IntersectTriangle( + const Ray& ray, + const Vector3& v0, + const Vector3& v1, + const Vector3& v2, + float& outT) { + const Vector3 edge1 = v1 - v0; + const Vector3 edge2 = v2 - v0; + const Vector3 p = Vector3::Cross(ray.direction, edge2); + const float determinant = Vector3::Dot(edge1, p); + + if (std::abs(determinant) <= Math::EPSILON) { + return false; + } + + const float invDeterminant = 1.0f / determinant; + const Vector3 t = ray.origin - v0; + const float u = Vector3::Dot(t, p) * invDeterminant; + if (u < 0.0f || u > 1.0f) { + return false; + } + + const Vector3 q = Vector3::Cross(t, edge1); + const float v = Vector3::Dot(ray.direction, q) * invDeterminant; + if (v < 0.0f || (u + v) > 1.0f) { + return false; + } + + const float triangleT = Vector3::Dot(edge2, q) * invDeterminant; + if (triangleT <= Math::EPSILON) { + return false; + } + + outT = triangleT; + return true; +} + +bool IntersectIndexedSection( + const Mesh& mesh, + const Ray& localRay, + uint32_t startIndex, + uint32_t indexCount, + float& outClosestT) { + bool hit = false; + + for (uint32_t triangleIndex = 0; triangleIndex + 2 < indexCount; triangleIndex += 3) { + uint32_t i0 = 0; + uint32_t i1 = 0; + uint32_t i2 = 0; + if (!ReadIndex(mesh, startIndex + triangleIndex, i0) || + !ReadIndex(mesh, startIndex + triangleIndex + 1, i1) || + !ReadIndex(mesh, startIndex + triangleIndex + 2, i2)) { + continue; + } + + Vector3 v0 = Vector3::Zero(); + Vector3 v1 = Vector3::Zero(); + Vector3 v2 = Vector3::Zero(); + if (!ReadVertexPosition(mesh, i0, v0) || + !ReadVertexPosition(mesh, i1, v1) || + !ReadVertexPosition(mesh, i2, v2)) { + continue; + } + + float triangleT = 0.0f; + if (!IntersectTriangle(localRay, v0, v1, v2, triangleT)) { + continue; + } + + outClosestT = (std::min)(outClosestT, triangleT); + hit = true; + } + + return hit; +} + +bool IntersectNonIndexedSection( + const Mesh& mesh, + const Ray& localRay, + uint32_t startVertex, + uint32_t vertexCount, + float& outClosestT) { + bool hit = false; + + for (uint32_t vertexOffset = 0; vertexOffset + 2 < vertexCount; vertexOffset += 3) { + Vector3 v0 = Vector3::Zero(); + Vector3 v1 = Vector3::Zero(); + Vector3 v2 = Vector3::Zero(); + if (!ReadVertexPosition(mesh, startVertex + vertexOffset, v0) || + !ReadVertexPosition(mesh, startVertex + vertexOffset + 1, v1) || + !ReadVertexPosition(mesh, startVertex + vertexOffset + 2, v2)) { + continue; + } + + float triangleT = 0.0f; + if (!IntersectTriangle(localRay, v0, v1, v2, triangleT)) { + continue; + } + + outClosestT = (std::min)(outClosestT, triangleT); + hit = true; + } + + return hit; +} + +bool IntersectMesh( + const Mesh& mesh, + const Ray& localRay, + float& outClosestT) { + outClosestT = Math::FLOAT_MAX; + + Math::Box localBounds(mesh.GetBounds().center, mesh.GetBounds().extents); + float boundsT = 0.0f; + if (!localBounds.Intersects(localRay, boundsT)) { + return false; + } + + bool hit = false; + const auto& sections = mesh.GetSections(); + + if (mesh.GetIndexCount() > 0) { + if (sections.Empty()) { + return IntersectIndexedSection(mesh, localRay, 0, mesh.GetIndexCount(), outClosestT); + } + + for (size_t sectionIndex = 0; sectionIndex < sections.Size(); ++sectionIndex) { + const MeshSection& section = sections[sectionIndex]; + if (section.indexCount == 0) { + continue; + } + + if (IntersectIndexedSection(mesh, localRay, section.startIndex, section.indexCount, outClosestT)) { + hit = true; + } + } + return hit; + } + + if (sections.Empty()) { + return IntersectNonIndexedSection(mesh, localRay, 0, mesh.GetVertexCount(), outClosestT); + } + + for (size_t sectionIndex = 0; sectionIndex < sections.Size(); ++sectionIndex) { + const MeshSection& section = sections[sectionIndex]; + if (section.vertexCount == 0) { + continue; + } + + if (IntersectNonIndexedSection(mesh, localRay, section.baseVertex, section.vertexCount, outClosestT)) { + hit = true; + } + } + + return hit; +} + +void PickRecursive( + GameObject* gameObject, + const Ray& worldRay, + SceneViewportPickResult& bestResult) { + if (gameObject == nullptr || !gameObject->IsActiveInHierarchy()) { + return; + } + + auto* meshFilter = gameObject->GetComponent(); + auto* meshRenderer = gameObject->GetComponent(); + if (meshFilter != nullptr && + meshRenderer != nullptr && + meshFilter->IsEnabled() && + meshRenderer->IsEnabled()) { + Mesh* mesh = meshFilter->GetMesh(); + if (mesh != nullptr && mesh->IsValid() && mesh->GetVertexCount() > 0) { + const Math::Matrix4x4 worldToLocal = gameObject->GetTransform()->GetWorldToLocalMatrix(); + Ray localRay( + worldToLocal.MultiplyPoint(worldRay.origin), + worldToLocal.MultiplyVector(worldRay.direction)); + + float localHitT = Math::FLOAT_MAX; + if (IntersectMesh(*mesh, localRay, localHitT)) { + const Vector3 localHitPoint = localRay.GetPoint(localHitT); + const Vector3 worldHitPoint = gameObject->GetTransform()->GetLocalToWorldMatrix().MultiplyPoint(localHitPoint); + const float distanceSq = (worldHitPoint - worldRay.origin).SqrMagnitude(); + if (!bestResult.hit || distanceSq < bestResult.distanceSq) { + bestResult.entityId = gameObject->GetID(); + bestResult.worldPosition = worldHitPoint; + bestResult.distanceSq = distanceSq; + bestResult.hit = true; + } + } + } + } + + for (size_t childIndex = 0; childIndex < gameObject->GetChildCount(); ++childIndex) { + PickRecursive(gameObject->GetChild(childIndex), worldRay, bestResult); + } +} + +} // namespace + +bool BuildSceneViewportRay( + const SceneViewportOverlayData& overlay, + const Vector2& viewportSize, + const Vector2& viewportPosition, + Ray& outRay) { + if (!overlay.valid || !IsViewportPositionValid(viewportSize, viewportPosition)) { + return false; + } + + const float aspect = viewportSize.y > 0.0f + ? viewportSize.x / viewportSize.y + : 1.0f; + const float tanHalfFov = std::tan(overlay.verticalFovDegrees * Math::DEG_TO_RAD * 0.5f); + const float ndcX = (viewportPosition.x / viewportSize.x) * 2.0f - 1.0f; + const float ndcY = 1.0f - (viewportPosition.y / viewportSize.y) * 2.0f; + + const Vector3 direction = Math::Vector3::Normalize( + overlay.cameraForward.Normalized() + + overlay.cameraRight.Normalized() * (ndcX * aspect * tanHalfFov) + + overlay.cameraUp.Normalized() * (ndcY * tanHalfFov)); + + if (direction.SqrMagnitude() <= Math::EPSILON) { + return false; + } + + outRay = Ray(overlay.cameraPosition, direction); + return true; +} + +SceneViewportPickResult PickSceneViewportEntity(const SceneViewportPickRequest& request) { + SceneViewportPickResult result = {}; + if (request.scene == nullptr) { + return result; + } + + Ray worldRay; + if (!BuildSceneViewportRay(request.overlay, request.viewportSize, request.viewportPosition, worldRay)) { + return result; + } + + const std::vector roots = request.scene->GetRootGameObjects(); + for (GameObject* root : roots) { + PickRecursive(root, worldRay, result); + } + + return result; +} + +} // namespace Editor +} // namespace XCEngine diff --git a/editor/src/Viewport/SceneViewportPicker.h b/editor/src/Viewport/SceneViewportPicker.h new file mode 100644 index 00000000..87506ddb --- /dev/null +++ b/editor/src/Viewport/SceneViewportPicker.h @@ -0,0 +1,40 @@ +#pragma once + +#include "IViewportHostService.h" + +#include +#include + +#include + +namespace XCEngine { +namespace Components { +class Scene; +} // namespace Components + +namespace Editor { + +struct SceneViewportPickRequest { + const Components::Scene* scene = nullptr; + SceneViewportOverlayData overlay = {}; + Math::Vector2 viewportSize = Math::Vector2::Zero(); + Math::Vector2 viewportPosition = Math::Vector2::Zero(); +}; + +struct SceneViewportPickResult { + uint64_t entityId = 0; + Math::Vector3 worldPosition = Math::Vector3::Zero(); + float distanceSq = Math::FLOAT_MAX; + bool hit = false; +}; + +bool BuildSceneViewportRay( + const SceneViewportOverlayData& overlay, + const Math::Vector2& viewportSize, + const Math::Vector2& viewportPosition, + Math::Ray& outRay); + +SceneViewportPickResult PickSceneViewportEntity(const SceneViewportPickRequest& request); + +} // namespace Editor +} // namespace XCEngine diff --git a/editor/src/Viewport/SceneViewportSelectionMaskPass.cpp b/editor/src/Viewport/SceneViewportSelectionMaskPass.cpp new file mode 100644 index 00000000..db4fcce3 --- /dev/null +++ b/editor/src/Viewport/SceneViewportSelectionMaskPass.cpp @@ -0,0 +1,339 @@ +#include "SceneViewportSelectionMaskPass.h" + +#include +#include +#include +#include + +#include +#include + +namespace XCEngine { +namespace Editor { + +namespace { + +const char kSelectionMaskHlsl[] = R"( +cbuffer PerObjectConstants : register(b0) { + float4x4 gProjectionMatrix; + float4x4 gViewMatrix; + float4x4 gModelMatrix; +}; + +struct VSInput { + float3 position : POSITION; +}; + +struct PSInput { + float4 position : SV_POSITION; +}; + +PSInput MainVS(VSInput input) { + PSInput output; + float4 positionWS = mul(gModelMatrix, float4(input.position, 1.0)); + float4 positionVS = mul(gViewMatrix, positionWS); + output.position = mul(gProjectionMatrix, positionVS); + return output; +} + +float4 MainPS(PSInput input) : SV_TARGET { + return float4(1.0, 1.0, 1.0, 1.0); +} +)"; + +RHI::InputLayoutDesc BuildInputLayout() { + RHI::InputLayoutDesc inputLayout = {}; + + RHI::InputElementDesc position = {}; + position.semanticName = "POSITION"; + position.semanticIndex = 0; + position.format = static_cast(RHI::Format::R32G32B32_Float); + position.inputSlot = 0; + position.alignedByteOffset = static_cast(offsetof(Resources::StaticMeshVertex, position)); + inputLayout.elements.push_back(position); + return inputLayout; +} + +RHI::GraphicsPipelineDesc CreatePipelineDesc(RHI::RHIPipelineLayout* pipelineLayout) { + RHI::GraphicsPipelineDesc pipelineDesc = {}; + pipelineDesc.pipelineLayout = pipelineLayout; + pipelineDesc.topologyType = static_cast(RHI::PrimitiveTopologyType::Triangle); + pipelineDesc.renderTargetCount = 1; + pipelineDesc.renderTargetFormats[0] = static_cast(RHI::Format::R8G8B8A8_UNorm); + pipelineDesc.depthStencilFormat = static_cast(RHI::Format::D24_UNorm_S8_UInt); + pipelineDesc.sampleCount = 1; + pipelineDesc.inputLayout = BuildInputLayout(); + + pipelineDesc.rasterizerState.fillMode = static_cast(RHI::FillMode::Solid); + pipelineDesc.rasterizerState.cullMode = static_cast(RHI::CullMode::None); + pipelineDesc.rasterizerState.frontFace = static_cast(RHI::FrontFace::CounterClockwise); + pipelineDesc.rasterizerState.depthClipEnable = true; + + pipelineDesc.blendState.blendEnable = false; + pipelineDesc.blendState.colorWriteMask = static_cast(RHI::ColorWriteMask::All); + + pipelineDesc.depthStencilState.depthTestEnable = true; + pipelineDesc.depthStencilState.depthWriteEnable = false; + pipelineDesc.depthStencilState.depthFunc = static_cast(RHI::ComparisonFunc::LessEqual); + + pipelineDesc.vertexShader.source.assign( + kSelectionMaskHlsl, + kSelectionMaskHlsl + std::strlen(kSelectionMaskHlsl)); + pipelineDesc.vertexShader.sourceLanguage = RHI::ShaderLanguage::HLSL; + pipelineDesc.vertexShader.entryPoint = L"MainVS"; + pipelineDesc.vertexShader.profile = L"vs_5_0"; + + pipelineDesc.fragmentShader.source.assign( + kSelectionMaskHlsl, + kSelectionMaskHlsl + std::strlen(kSelectionMaskHlsl)); + pipelineDesc.fragmentShader.sourceLanguage = RHI::ShaderLanguage::HLSL; + pipelineDesc.fragmentShader.entryPoint = L"MainPS"; + pipelineDesc.fragmentShader.profile = L"ps_5_0"; + return pipelineDesc; +} + +} // namespace + +void SceneViewportSelectionMaskPass::Shutdown() { + DestroyResources(); +} + +bool SceneViewportSelectionMaskPass::Render( + const Rendering::RenderContext& renderContext, + const Rendering::RenderSurface& surface, + const Rendering::RenderCameraData& cameraData, + const std::vector& renderables) { + if (!renderContext.IsValid() || + renderContext.backendType != RHI::RHIType::D3D12 || + renderables.empty()) { + return false; + } + + if (!EnsureInitialized(renderContext)) { + return false; + } + + const std::vector& colorAttachments = surface.GetColorAttachments(); + if (colorAttachments.empty() || colorAttachments[0] == nullptr || surface.GetDepthAttachment() == nullptr) { + return false; + } + + RHI::RHICommandList* commandList = renderContext.commandList; + RHI::RHIResourceView* renderTarget = colorAttachments[0]; + commandList->SetRenderTargets(1, &renderTarget, surface.GetDepthAttachment()); + + const RHI::Viewport viewport = { + 0.0f, + 0.0f, + static_cast(surface.GetWidth()), + static_cast(surface.GetHeight()), + 0.0f, + 1.0f + }; + const RHI::Rect scissorRect = { + 0, + 0, + static_cast(surface.GetWidth()), + static_cast(surface.GetHeight()) + }; + + commandList->SetViewport(viewport); + commandList->SetScissorRect(scissorRect); + commandList->SetPrimitiveTopology(RHI::PrimitiveTopology::TriangleList); + commandList->SetPipelineState(m_pipelineState); + + bool drewAnything = false; + for (const Rendering::VisibleRenderItem& renderable : renderables) { + drewAnything = DrawRenderable(renderContext, cameraData, renderable) || drewAnything; + } + + return drewAnything; +} + +bool SceneViewportSelectionMaskPass::EnsureInitialized(const Rendering::RenderContext& renderContext) { + if (m_pipelineLayout != nullptr && + m_pipelineState != nullptr && + m_device == renderContext.device && + m_backendType == renderContext.backendType) { + return true; + } + + DestroyResources(); + return CreateResources(renderContext); +} + +bool SceneViewportSelectionMaskPass::CreateResources(const Rendering::RenderContext& renderContext) { + if (!renderContext.IsValid() || renderContext.backendType != RHI::RHIType::D3D12) { + return false; + } + + m_device = renderContext.device; + m_backendType = renderContext.backendType; + + RHI::DescriptorSetLayoutBinding constantBinding = {}; + constantBinding.binding = 0; + constantBinding.type = static_cast(RHI::DescriptorType::CBV); + constantBinding.count = 1; + + RHI::DescriptorSetLayoutDesc constantLayout = {}; + constantLayout.bindings = &constantBinding; + constantLayout.bindingCount = 1; + + RHI::RHIPipelineLayoutDesc pipelineLayoutDesc = {}; + pipelineLayoutDesc.setLayouts = &constantLayout; + pipelineLayoutDesc.setLayoutCount = 1; + m_pipelineLayout = m_device->CreatePipelineLayout(pipelineLayoutDesc); + if (m_pipelineLayout == nullptr) { + DestroyResources(); + return false; + } + + m_pipelineState = m_device->CreatePipelineState(CreatePipelineDesc(m_pipelineLayout)); + if (m_pipelineState == nullptr || !m_pipelineState->IsValid()) { + if (m_pipelineState != nullptr) { + m_pipelineState->Shutdown(); + delete m_pipelineState; + m_pipelineState = nullptr; + } + DestroyResources(); + return false; + } + + return true; +} + +void SceneViewportSelectionMaskPass::DestroyResources() { + m_resourceCache.Shutdown(); + + for (auto& descriptorSetEntry : m_perObjectSets) { + DestroyOwnedDescriptorSet(descriptorSetEntry.second); + } + m_perObjectSets.clear(); + + if (m_pipelineState != nullptr) { + m_pipelineState->Shutdown(); + delete m_pipelineState; + m_pipelineState = nullptr; + } + + if (m_pipelineLayout != nullptr) { + m_pipelineLayout->Shutdown(); + delete m_pipelineLayout; + m_pipelineLayout = nullptr; + } + + m_device = nullptr; + m_backendType = RHI::RHIType::D3D12; +} + +RHI::RHIDescriptorSet* SceneViewportSelectionMaskPass::GetOrCreatePerObjectSet(uint64_t objectId) { + const auto existing = m_perObjectSets.find(objectId); + if (existing != m_perObjectSets.end()) { + return existing->second.set; + } + + RHI::DescriptorPoolDesc poolDesc = {}; + poolDesc.type = RHI::DescriptorHeapType::CBV_SRV_UAV; + poolDesc.descriptorCount = 1; + poolDesc.shaderVisible = false; + + OwnedDescriptorSet descriptorSet = {}; + descriptorSet.pool = m_device->CreateDescriptorPool(poolDesc); + if (descriptorSet.pool == nullptr) { + return nullptr; + } + + RHI::DescriptorSetLayoutBinding binding = {}; + binding.binding = 0; + binding.type = static_cast(RHI::DescriptorType::CBV); + binding.count = 1; + + RHI::DescriptorSetLayoutDesc layout = {}; + layout.bindings = &binding; + layout.bindingCount = 1; + descriptorSet.set = descriptorSet.pool->AllocateSet(layout); + if (descriptorSet.set == nullptr) { + DestroyOwnedDescriptorSet(descriptorSet); + return nullptr; + } + + const auto result = m_perObjectSets.emplace(objectId, descriptorSet); + return result.first->second.set; +} + +void SceneViewportSelectionMaskPass::DestroyOwnedDescriptorSet(OwnedDescriptorSet& descriptorSet) { + if (descriptorSet.set != nullptr) { + descriptorSet.set->Shutdown(); + delete descriptorSet.set; + descriptorSet.set = nullptr; + } + + if (descriptorSet.pool != nullptr) { + descriptorSet.pool->Shutdown(); + delete descriptorSet.pool; + descriptorSet.pool = nullptr; + } +} + +bool SceneViewportSelectionMaskPass::DrawRenderable( + const Rendering::RenderContext& renderContext, + const Rendering::RenderCameraData& cameraData, + const Rendering::VisibleRenderItem& renderable) { + const Rendering::RenderResourceCache::CachedMesh* cachedMesh = + m_resourceCache.GetOrCreateMesh(m_device, renderable.mesh); + if (cachedMesh == nullptr || cachedMesh->vertexBufferView == nullptr) { + return false; + } + + RHI::RHICommandList* commandList = renderContext.commandList; + + RHI::RHIResourceView* vertexBuffers[] = { cachedMesh->vertexBufferView }; + const uint64_t offsets[] = { 0 }; + const uint32_t strides[] = { cachedMesh->vertexStride }; + commandList->SetVertexBuffers(0, 1, vertexBuffers, offsets, strides); + if (cachedMesh->indexBufferView != nullptr) { + commandList->SetIndexBuffer(cachedMesh->indexBufferView, 0); + } + + const PerObjectConstants constants = { + cameraData.projection, + cameraData.view, + renderable.localToWorld.Transpose() + }; + + const uint64_t objectId = renderable.gameObject != nullptr ? renderable.gameObject->GetID() : 0u; + RHI::RHIDescriptorSet* constantSet = GetOrCreatePerObjectSet(objectId); + if (constantSet == nullptr) { + return false; + } + + constantSet->WriteConstant(0, &constants, sizeof(constants)); + RHI::RHIDescriptorSet* descriptorSets[] = { constantSet }; + commandList->SetGraphicsDescriptorSets(0, 1, descriptorSets, m_pipelineLayout); + + if (renderable.hasSection) { + const auto& sections = renderable.mesh->GetSections(); + if (renderable.sectionIndex >= sections.Size()) { + return false; + } + + const Resources::MeshSection& section = sections[renderable.sectionIndex]; + if (cachedMesh->indexBufferView != nullptr && section.indexCount > 0) { + commandList->DrawIndexed(section.indexCount, 1, section.startIndex, 0, 0); + } else if (section.vertexCount > 0) { + commandList->Draw(section.vertexCount, 1, section.baseVertex, 0); + } + return true; + } + + if (cachedMesh->indexBufferView != nullptr && cachedMesh->indexCount > 0) { + commandList->DrawIndexed(cachedMesh->indexCount, 1, 0, 0, 0); + } else if (cachedMesh->vertexCount > 0) { + commandList->Draw(cachedMesh->vertexCount, 1, 0, 0); + } + + return true; +} + +} // namespace Editor +} // namespace XCEngine diff --git a/editor/src/Viewport/SceneViewportSelectionMaskPass.h b/editor/src/Viewport/SceneViewportSelectionMaskPass.h new file mode 100644 index 00000000..cfe42c7c --- /dev/null +++ b/editor/src/Viewport/SceneViewportSelectionMaskPass.h @@ -0,0 +1,65 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace XCEngine { +namespace Editor { + +class SceneViewportSelectionMaskPass { +public: + ~SceneViewportSelectionMaskPass() = default; + + void Shutdown(); + + bool Render( + const Rendering::RenderContext& renderContext, + const Rendering::RenderSurface& surface, + const Rendering::RenderCameraData& cameraData, + const std::vector& renderables); + +private: + struct OwnedDescriptorSet { + RHI::RHIDescriptorPool* pool = nullptr; + RHI::RHIDescriptorSet* set = nullptr; + }; + + struct PerObjectConstants { + Math::Matrix4x4 projection = Math::Matrix4x4::Identity(); + Math::Matrix4x4 view = Math::Matrix4x4::Identity(); + Math::Matrix4x4 model = Math::Matrix4x4::Identity(); + }; + + bool EnsureInitialized(const Rendering::RenderContext& renderContext); + bool CreateResources(const Rendering::RenderContext& renderContext); + void DestroyResources(); + RHI::RHIDescriptorSet* GetOrCreatePerObjectSet(uint64_t objectId); + void DestroyOwnedDescriptorSet(OwnedDescriptorSet& descriptorSet); + bool DrawRenderable( + const Rendering::RenderContext& renderContext, + const Rendering::RenderCameraData& cameraData, + const Rendering::VisibleRenderItem& renderable); + + RHI::RHIDevice* m_device = nullptr; + RHI::RHIType m_backendType = RHI::RHIType::D3D12; + RHI::RHIPipelineLayout* m_pipelineLayout = nullptr; + RHI::RHIPipelineState* m_pipelineState = nullptr; + std::unordered_map m_perObjectSets; + Rendering::RenderResourceCache m_resourceCache; +}; + +} // namespace Editor +} // namespace XCEngine diff --git a/editor/src/Viewport/SceneViewportSelectionOutlinePass.cpp b/editor/src/Viewport/SceneViewportSelectionOutlinePass.cpp new file mode 100644 index 00000000..2247218b --- /dev/null +++ b/editor/src/Viewport/SceneViewportSelectionOutlinePass.cpp @@ -0,0 +1,386 @@ +#include "SceneViewportSelectionOutlinePass.h" + +#include +#include + +#include + +namespace XCEngine { +namespace Editor { + +namespace { + +constexpr float kOutlineWidthPixels = 2.0f; + +const char kSelectionOutlineCompositeHlsl[] = R"( +cbuffer OutlineConstants : register(b0) { + float4 gViewportSizeAndTexelSize; + float4 gOutlineColorAndWidth; +}; + +Texture2D gSelectionMask : register(t0); +SamplerState gMaskSampler : register(s0); + +struct VSOutput { + float4 position : SV_POSITION; +}; + +VSOutput MainVS(uint vertexId : SV_VertexID) { + static const float2 positions[3] = { + float2(-1.0, -1.0), + float2(-1.0, 3.0), + float2( 3.0, -1.0) + }; + + VSOutput output; + output.position = float4(positions[vertexId], 0.0, 1.0); + return output; +} + +float SampleMask(float2 uv) { + return gSelectionMask.SampleLevel(gMaskSampler, uv, 0).r; +} + +float4 MainPS(VSOutput input) : SV_TARGET { + const float2 viewportSize = max(gViewportSizeAndTexelSize.xy, float2(1.0, 1.0)); + const float2 texelSize = max(gViewportSizeAndTexelSize.zw, float2(1e-6, 1e-6)); + const float outlineWidth = max(gOutlineColorAndWidth.w, 1.0); + + const float2 uv = input.position.xy / viewportSize; + const float centerMask = SampleMask(uv); + if (centerMask > 0.001) { + discard; + } + + float outline = 0.0; + [unroll] + for (int y = -2; y <= 2; ++y) { + [unroll] + for (int x = -2; x <= 2; ++x) { + if (x == 0 && y == 0) { + continue; + } + + const float distancePixels = length(float2((float)x, (float)y)); + if (distancePixels > outlineWidth) { + continue; + } + + const float sampleMask = SampleMask(uv + float2((float)x, (float)y) * texelSize); + const float weight = saturate(1.0 - ((distancePixels - 1.0) / max(outlineWidth, 1.0))); + outline = max(outline, sampleMask * weight); + } + } + + if (outline <= 0.001) { + discard; + } + + return float4(gOutlineColorAndWidth.rgb, gOutlineColorAndWidth.a * outline); +} +)"; + +} // namespace + +void SceneViewportSelectionOutlinePass::Shutdown() { + DestroyResources(); +} + +bool SceneViewportSelectionOutlinePass::Render( + const Rendering::RenderContext& renderContext, + const Rendering::RenderSurface& surface, + RHI::RHIResourceView* maskTextureView) { + if (!renderContext.IsValid() || + renderContext.backendType != RHI::RHIType::D3D12 || + maskTextureView == nullptr) { + return false; + } + + if (!EnsureInitialized(renderContext)) { + return false; + } + + const std::vector& colorAttachments = surface.GetColorAttachments(); + if (colorAttachments.empty() || colorAttachments[0] == nullptr) { + return false; + } + + OutlineConstants constants = {}; + constants.viewportSizeAndTexelSize = Math::Vector4( + static_cast(surface.GetWidth()), + static_cast(surface.GetHeight()), + surface.GetWidth() > 0 ? 1.0f / static_cast(surface.GetWidth()) : 0.0f, + surface.GetHeight() > 0 ? 1.0f / static_cast(surface.GetHeight()) : 0.0f); + constants.outlineColorAndWidth = Math::Vector4(1.0f, 0.4f, 0.0f, kOutlineWidthPixels); + m_constantSet->WriteConstant(0, &constants, sizeof(constants)); + m_textureSet->Update(0, maskTextureView); + + RHI::RHICommandList* commandList = renderContext.commandList; + RHI::RHIResourceView* renderTarget = colorAttachments[0]; + commandList->SetRenderTargets(1, &renderTarget, nullptr); + + const RHI::Viewport viewport = { + 0.0f, + 0.0f, + static_cast(surface.GetWidth()), + static_cast(surface.GetHeight()), + 0.0f, + 1.0f + }; + const RHI::Rect scissorRect = { + 0, + 0, + static_cast(surface.GetWidth()), + static_cast(surface.GetHeight()) + }; + + commandList->SetViewport(viewport); + commandList->SetScissorRect(scissorRect); + commandList->SetPrimitiveTopology(RHI::PrimitiveTopology::TriangleList); + commandList->SetPipelineState(m_pipelineState); + + RHI::RHIDescriptorSet* descriptorSets[] = { m_constantSet, m_textureSet, m_samplerSet }; + commandList->SetGraphicsDescriptorSets(0, 3, descriptorSets, m_pipelineLayout); + commandList->Draw(3, 1, 0, 0); + return true; +} + +bool SceneViewportSelectionOutlinePass::EnsureInitialized(const Rendering::RenderContext& renderContext) { + if (m_pipelineLayout != nullptr && + m_pipelineState != nullptr && + m_constantPool != nullptr && + m_constantSet != nullptr && + m_texturePool != nullptr && + m_textureSet != nullptr && + m_samplerPool != nullptr && + m_samplerSet != nullptr && + m_sampler != nullptr && + m_device == renderContext.device && + m_backendType == renderContext.backendType) { + return true; + } + + DestroyResources(); + return CreateResources(renderContext); +} + +bool SceneViewportSelectionOutlinePass::CreateResources(const Rendering::RenderContext& renderContext) { + if (!renderContext.IsValid() || renderContext.backendType != RHI::RHIType::D3D12) { + return false; + } + + m_device = renderContext.device; + m_backendType = renderContext.backendType; + + RHI::DescriptorSetLayoutBinding setBindings[3] = {}; + setBindings[0].binding = 0; + setBindings[0].type = static_cast(RHI::DescriptorType::CBV); + setBindings[0].count = 1; + setBindings[1].binding = 0; + setBindings[1].type = static_cast(RHI::DescriptorType::SRV); + setBindings[1].count = 1; + setBindings[2].binding = 0; + setBindings[2].type = static_cast(RHI::DescriptorType::Sampler); + setBindings[2].count = 1; + + RHI::DescriptorSetLayoutDesc constantLayout = {}; + constantLayout.bindings = &setBindings[0]; + constantLayout.bindingCount = 1; + + RHI::DescriptorSetLayoutDesc textureLayout = {}; + textureLayout.bindings = &setBindings[1]; + textureLayout.bindingCount = 1; + + RHI::DescriptorSetLayoutDesc samplerLayout = {}; + samplerLayout.bindings = &setBindings[2]; + samplerLayout.bindingCount = 1; + + RHI::DescriptorSetLayoutDesc setLayouts[3] = {}; + setLayouts[0] = constantLayout; + setLayouts[1] = textureLayout; + setLayouts[2] = samplerLayout; + + RHI::RHIPipelineLayoutDesc pipelineLayoutDesc = {}; + pipelineLayoutDesc.setLayouts = setLayouts; + pipelineLayoutDesc.setLayoutCount = 3; + m_pipelineLayout = m_device->CreatePipelineLayout(pipelineLayoutDesc); + if (m_pipelineLayout == nullptr) { + DestroyResources(); + return false; + } + + RHI::DescriptorPoolDesc constantPoolDesc = {}; + constantPoolDesc.type = RHI::DescriptorHeapType::CBV_SRV_UAV; + constantPoolDesc.descriptorCount = 1; + constantPoolDesc.shaderVisible = false; + m_constantPool = m_device->CreateDescriptorPool(constantPoolDesc); + if (m_constantPool == nullptr) { + DestroyResources(); + return false; + } + + m_constantSet = m_constantPool->AllocateSet(constantLayout); + if (m_constantSet == nullptr) { + DestroyResources(); + return false; + } + + RHI::DescriptorPoolDesc texturePoolDesc = {}; + texturePoolDesc.type = RHI::DescriptorHeapType::CBV_SRV_UAV; + texturePoolDesc.descriptorCount = 1; + texturePoolDesc.shaderVisible = true; + m_texturePool = m_device->CreateDescriptorPool(texturePoolDesc); + if (m_texturePool == nullptr) { + DestroyResources(); + return false; + } + + m_textureSet = m_texturePool->AllocateSet(textureLayout); + if (m_textureSet == nullptr) { + DestroyResources(); + return false; + } + + RHI::DescriptorPoolDesc samplerPoolDesc = {}; + samplerPoolDesc.type = RHI::DescriptorHeapType::Sampler; + samplerPoolDesc.descriptorCount = 1; + samplerPoolDesc.shaderVisible = true; + m_samplerPool = m_device->CreateDescriptorPool(samplerPoolDesc); + if (m_samplerPool == nullptr) { + DestroyResources(); + return false; + } + + m_samplerSet = m_samplerPool->AllocateSet(samplerLayout); + if (m_samplerSet == nullptr) { + DestroyResources(); + return false; + } + + RHI::SamplerDesc samplerDesc = {}; + samplerDesc.filter = static_cast(RHI::FilterMode::Linear); + samplerDesc.addressU = static_cast(RHI::TextureAddressMode::Clamp); + samplerDesc.addressV = static_cast(RHI::TextureAddressMode::Clamp); + samplerDesc.addressW = static_cast(RHI::TextureAddressMode::Clamp); + samplerDesc.mipLodBias = 0.0f; + samplerDesc.maxAnisotropy = 1; + samplerDesc.comparisonFunc = static_cast(RHI::ComparisonFunc::Always); + samplerDesc.minLod = 0.0f; + samplerDesc.maxLod = 1.0f; + m_sampler = m_device->CreateSampler(samplerDesc); + if (m_sampler == nullptr) { + DestroyResources(); + return false; + } + m_samplerSet->UpdateSampler(0, m_sampler); + + RHI::GraphicsPipelineDesc pipelineDesc = {}; + pipelineDesc.pipelineLayout = m_pipelineLayout; + pipelineDesc.topologyType = static_cast(RHI::PrimitiveTopologyType::Triangle); + pipelineDesc.renderTargetCount = 1; + pipelineDesc.renderTargetFormats[0] = static_cast(RHI::Format::R8G8B8A8_UNorm); + pipelineDesc.depthStencilFormat = static_cast(RHI::Format::Unknown); + pipelineDesc.sampleCount = 1; + + pipelineDesc.rasterizerState.fillMode = static_cast(RHI::FillMode::Solid); + pipelineDesc.rasterizerState.cullMode = static_cast(RHI::CullMode::None); + pipelineDesc.rasterizerState.frontFace = static_cast(RHI::FrontFace::CounterClockwise); + pipelineDesc.rasterizerState.depthClipEnable = true; + + pipelineDesc.blendState.blendEnable = true; + pipelineDesc.blendState.srcBlend = static_cast(RHI::BlendFactor::SrcAlpha); + pipelineDesc.blendState.dstBlend = static_cast(RHI::BlendFactor::InvSrcAlpha); + pipelineDesc.blendState.srcBlendAlpha = static_cast(RHI::BlendFactor::One); + pipelineDesc.blendState.dstBlendAlpha = static_cast(RHI::BlendFactor::InvSrcAlpha); + pipelineDesc.blendState.blendOp = static_cast(RHI::BlendOp::Add); + pipelineDesc.blendState.blendOpAlpha = static_cast(RHI::BlendOp::Add); + pipelineDesc.blendState.colorWriteMask = static_cast(RHI::ColorWriteMask::All); + + pipelineDesc.depthStencilState.depthTestEnable = false; + pipelineDesc.depthStencilState.depthWriteEnable = false; + pipelineDesc.depthStencilState.depthFunc = static_cast(RHI::ComparisonFunc::Always); + + pipelineDesc.vertexShader.source.assign( + kSelectionOutlineCompositeHlsl, + kSelectionOutlineCompositeHlsl + std::strlen(kSelectionOutlineCompositeHlsl)); + pipelineDesc.vertexShader.sourceLanguage = RHI::ShaderLanguage::HLSL; + pipelineDesc.vertexShader.entryPoint = L"MainVS"; + pipelineDesc.vertexShader.profile = L"vs_5_0"; + + pipelineDesc.fragmentShader.source.assign( + kSelectionOutlineCompositeHlsl, + kSelectionOutlineCompositeHlsl + std::strlen(kSelectionOutlineCompositeHlsl)); + pipelineDesc.fragmentShader.sourceLanguage = RHI::ShaderLanguage::HLSL; + pipelineDesc.fragmentShader.entryPoint = L"MainPS"; + pipelineDesc.fragmentShader.profile = L"ps_5_0"; + + m_pipelineState = m_device->CreatePipelineState(pipelineDesc); + if (m_pipelineState == nullptr || !m_pipelineState->IsValid()) { + DestroyResources(); + return false; + } + + return true; +} + +void SceneViewportSelectionOutlinePass::DestroyResources() { + if (m_pipelineState != nullptr) { + m_pipelineState->Shutdown(); + delete m_pipelineState; + m_pipelineState = nullptr; + } + + if (m_sampler != nullptr) { + m_sampler->Shutdown(); + delete m_sampler; + m_sampler = nullptr; + } + + if (m_samplerSet != nullptr) { + m_samplerSet->Shutdown(); + delete m_samplerSet; + m_samplerSet = nullptr; + } + + if (m_samplerPool != nullptr) { + m_samplerPool->Shutdown(); + delete m_samplerPool; + m_samplerPool = nullptr; + } + + if (m_textureSet != nullptr) { + m_textureSet->Shutdown(); + delete m_textureSet; + m_textureSet = nullptr; + } + + if (m_texturePool != nullptr) { + m_texturePool->Shutdown(); + delete m_texturePool; + m_texturePool = nullptr; + } + + if (m_constantSet != nullptr) { + m_constantSet->Shutdown(); + delete m_constantSet; + m_constantSet = nullptr; + } + + if (m_constantPool != nullptr) { + m_constantPool->Shutdown(); + delete m_constantPool; + m_constantPool = nullptr; + } + + if (m_pipelineLayout != nullptr) { + m_pipelineLayout->Shutdown(); + delete m_pipelineLayout; + m_pipelineLayout = nullptr; + } + + m_device = nullptr; + m_backendType = RHI::RHIType::D3D12; +} + +} // namespace Editor +} // namespace XCEngine diff --git a/editor/src/Viewport/SceneViewportSelectionOutlinePass.h b/editor/src/Viewport/SceneViewportSelectionOutlinePass.h new file mode 100644 index 00000000..48a6d088 --- /dev/null +++ b/editor/src/Viewport/SceneViewportSelectionOutlinePass.h @@ -0,0 +1,57 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace XCEngine { +namespace Editor { + +class SceneViewportSelectionOutlinePass { +public: + ~SceneViewportSelectionOutlinePass() = default; + + void Shutdown(); + + bool Render( + const Rendering::RenderContext& renderContext, + const Rendering::RenderSurface& surface, + RHI::RHIResourceView* maskTextureView); + +private: + struct OutlineConstants { + Math::Vector4 viewportSizeAndTexelSize = Math::Vector4::Zero(); + Math::Vector4 outlineColorAndWidth = Math::Vector4::Zero(); + }; + + bool EnsureInitialized(const Rendering::RenderContext& renderContext); + bool CreateResources(const Rendering::RenderContext& renderContext); + void DestroyResources(); + + RHI::RHIDevice* m_device = nullptr; + RHI::RHIType m_backendType = RHI::RHIType::D3D12; + RHI::RHIPipelineLayout* m_pipelineLayout = nullptr; + RHI::RHIPipelineState* m_pipelineState = nullptr; + RHI::RHIDescriptorPool* m_constantPool = nullptr; + RHI::RHIDescriptorSet* m_constantSet = nullptr; + RHI::RHIDescriptorPool* m_texturePool = nullptr; + RHI::RHIDescriptorSet* m_textureSet = nullptr; + RHI::RHIDescriptorPool* m_samplerPool = nullptr; + RHI::RHIDescriptorSet* m_samplerSet = nullptr; + RHI::RHISampler* m_sampler = nullptr; +}; + +} // namespace Editor +} // namespace XCEngine diff --git a/editor/src/Viewport/SceneViewportSelectionUtils.h b/editor/src/Viewport/SceneViewportSelectionUtils.h new file mode 100644 index 00000000..8575fabe --- /dev/null +++ b/editor/src/Viewport/SceneViewportSelectionUtils.h @@ -0,0 +1,161 @@ +#pragma once + +#include "IViewportHostService.h" +#include "SceneViewportMath.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace XCEngine { +namespace Editor { + +inline Rendering::RenderCameraData BuildSceneViewportCameraData( + const SceneViewportOverlayData& overlay, + uint32_t viewportWidth, + uint32_t viewportHeight) { + Rendering::RenderCameraData cameraData = {}; + cameraData.viewportWidth = viewportWidth; + cameraData.viewportHeight = viewportHeight; + cameraData.worldPosition = overlay.cameraPosition; + cameraData.clearFlags = Rendering::RenderClearFlags::None; + + const Math::Matrix4x4 view = BuildSceneViewportViewMatrix(overlay); + const Math::Matrix4x4 projection = BuildSceneViewportProjectionMatrix( + overlay, + static_cast(viewportWidth), + static_cast(viewportHeight)); + + cameraData.view = view.Transpose(); + cameraData.projection = projection.Transpose(); + cameraData.viewProjection = (projection * view).Transpose(); + return cameraData; +} + +inline Rendering::RenderCameraData BuildSceneViewportCameraData( + const Components::CameraComponent& camera, + uint32_t viewportWidth, + uint32_t viewportHeight) { + Rendering::RenderCameraData cameraData = {}; + cameraData.viewportWidth = viewportWidth; + cameraData.viewportHeight = viewportHeight; + cameraData.worldPosition = camera.transform().GetPosition(); + cameraData.clearFlags = Rendering::RenderClearFlags::None; + + const Math::Matrix4x4 view = camera.transform().GetWorldToLocalMatrix(); + const float aspect = viewportHeight > 0 + ? static_cast(viewportWidth) / static_cast(viewportHeight) + : 1.0f; + + Math::Matrix4x4 projection = Math::Matrix4x4::Identity(); + if (camera.GetProjectionType() == Components::CameraProjectionType::Perspective) { + projection = Math::Matrix4x4::Perspective( + camera.GetFieldOfView() * Math::DEG_TO_RAD, + aspect, + camera.GetNearClipPlane(), + camera.GetFarClipPlane()); + } else { + const float orthoSize = camera.GetOrthographicSize(); + projection = Math::Matrix4x4::Orthographic( + -orthoSize * aspect, + orthoSize * aspect, + -orthoSize, + orthoSize, + camera.GetNearClipPlane(), + camera.GetFarClipPlane()); + } + + cameraData.view = view.Transpose(); + cameraData.projection = projection.Transpose(); + cameraData.viewProjection = (projection * view).Transpose(); + return cameraData; +} + +inline std::vector CollectSceneViewportSelectionRenderables( + const Components::Scene& scene, + const std::vector& selectedEntityIds, + const Math::Vector3& cameraPosition) { + std::vector renderables; + std::unordered_set visitedEntityIds; + visitedEntityIds.reserve(selectedEntityIds.size()); + + for (uint64_t entityId : selectedEntityIds) { + if (entityId == 0 || !visitedEntityIds.insert(entityId).second) { + continue; + } + + Components::GameObject* gameObject = scene.FindByID(entityId); + if (gameObject == nullptr || !gameObject->IsActiveInHierarchy()) { + continue; + } + + auto* meshFilter = gameObject->GetComponent(); + auto* meshRenderer = gameObject->GetComponent(); + if (meshFilter == nullptr || + meshRenderer == nullptr || + !meshFilter->IsEnabled() || + !meshRenderer->IsEnabled()) { + continue; + } + + Resources::Mesh* mesh = meshFilter->GetMesh(); + if (mesh == nullptr || !mesh->IsValid()) { + continue; + } + + const Math::Matrix4x4 localToWorld = gameObject->GetTransform()->GetLocalToWorldMatrix(); + const Math::Vector3 worldPosition = localToWorld.GetTranslation(); + const float cameraDistanceSq = (worldPosition - cameraPosition).SqrMagnitude(); + const auto& sections = mesh->GetSections(); + + if (!sections.Empty()) { + for (size_t sectionIndex = 0; sectionIndex < sections.Size(); ++sectionIndex) { + const Resources::MeshSection& section = sections[sectionIndex]; + + Rendering::VisibleRenderItem renderable = {}; + renderable.gameObject = gameObject; + renderable.meshFilter = meshFilter; + renderable.meshRenderer = meshRenderer; + renderable.mesh = mesh; + renderable.materialIndex = section.materialID; + renderable.sectionIndex = static_cast(sectionIndex); + renderable.hasSection = true; + renderable.material = Rendering::ResolveMaterial(meshRenderer, mesh, section.materialID); + renderable.renderQueue = Rendering::ResolveMaterialRenderQueue(renderable.material); + renderable.cameraDistanceSq = cameraDistanceSq; + renderable.localToWorld = localToWorld; + renderables.push_back(renderable); + } + + continue; + } + + Rendering::VisibleRenderItem renderable = {}; + renderable.gameObject = gameObject; + renderable.meshFilter = meshFilter; + renderable.meshRenderer = meshRenderer; + renderable.mesh = mesh; + renderable.materialIndex = 0; + renderable.sectionIndex = 0; + renderable.hasSection = false; + renderable.material = Rendering::ResolveMaterial(meshRenderer, mesh, 0); + renderable.renderQueue = Rendering::ResolveMaterialRenderQueue(renderable.material); + renderable.cameraDistanceSq = cameraDistanceSq; + renderable.localToWorld = localToWorld; + renderables.push_back(renderable); + } + + return renderables; +} + +} // namespace Editor +} // namespace XCEngine diff --git a/editor/src/Viewport/ViewportHostService.h b/editor/src/Viewport/ViewportHostService.h index cd755db3..690ac2b1 100644 --- a/editor/src/Viewport/ViewportHostService.h +++ b/editor/src/Viewport/ViewportHostService.h @@ -4,7 +4,12 @@ #include "Core/ISceneManager.h" #include "Core/ISelectionManager.h" #include "IViewportHostService.h" +#include "SceneViewportPicker.h" #include "SceneViewportCameraController.h" +#include "SceneViewportInfiniteGridPass.h" +#include "SceneViewportSelectionMaskPass.h" +#include "SceneViewportSelectionOutlinePass.h" +#include "SceneViewportSelectionUtils.h" #include "UI/ImGuiBackendBridge.h" #include @@ -27,6 +32,12 @@ namespace XCEngine { namespace Editor { +namespace { + +constexpr bool kDebugSceneSelectionMask = false; + +} // namespace + class ViewportHostService : public IViewportHostService { public: void Initialize(UI::ImGuiBackendBridge& backend, RHI::RHIDevice* device) { @@ -44,6 +55,9 @@ public: m_sceneViewCamera = {}; m_device = nullptr; m_backend = nullptr; + m_sceneGridPass.Shutdown(); + m_sceneSelectionMaskPass.Shutdown(); + m_sceneSelectionOutlinePass.Shutdown(); m_sceneRenderer.reset(); } @@ -91,6 +105,7 @@ public: SceneViewportCameraInputState controllerInput = {}; controllerInput.viewportHeight = input.viewportSize.y; controllerInput.zoomDelta = input.hovered ? input.mouseWheel : 0.0f; + controllerInput.flySpeedDelta = input.hovered ? input.flySpeedDelta : 0.0f; controllerInput.deltaTime = input.deltaTime; controllerInput.moveForward = input.moveForward; controllerInput.moveRight = input.moveRight; @@ -116,6 +131,27 @@ public: ApplySceneViewCameraController(); } + uint64_t PickSceneViewEntity( + IEditorContext& context, + const ImVec2& viewportSize, + const ImVec2& viewportMousePosition) override { + if (!EnsureSceneViewCamera()) { + return 0; + } + + const Components::Scene* scene = context.GetSceneManager().GetScene(); + if (scene == nullptr) { + return 0; + } + + SceneViewportPickRequest request = {}; + request.scene = scene; + request.overlay = GetSceneViewOverlayData(); + request.viewportSize = Math::Vector2(viewportSize.x, viewportSize.y); + request.viewportPosition = Math::Vector2(viewportMousePosition.x, viewportMousePosition.y); + return PickSceneViewportEntity(request).entityId; + } + SceneViewportOverlayData GetSceneViewOverlayData() const override { SceneViewportOverlayData data = {}; if (m_sceneViewCamera.gameObject == nullptr || m_sceneViewCamera.camera == nullptr) { @@ -159,7 +195,7 @@ public: continue; } - RenderViewportEntry(entry, scene, renderContext); + RenderViewportEntry(entry, context, scene, renderContext); } } @@ -175,10 +211,14 @@ private: RHI::RHIResourceView* colorView = nullptr; RHI::RHITexture* depthTexture = nullptr; RHI::RHIResourceView* depthView = nullptr; + RHI::RHITexture* selectionMaskTexture = nullptr; + RHI::RHIResourceView* selectionMaskView = nullptr; + RHI::RHIResourceView* selectionMaskShaderView = nullptr; D3D12_CPU_DESCRIPTOR_HANDLE imguiCpuHandle = {}; D3D12_GPU_DESCRIPTOR_HANDLE imguiGpuHandle = {}; ImTextureID textureId = {}; RHI::ResourceStates colorState = RHI::ResourceStates::Common; + RHI::ResourceStates selectionMaskState = RHI::ResourceStates::Common; std::string statusText; }; @@ -277,6 +317,10 @@ private: entry.colorView != nullptr && entry.depthTexture != nullptr && entry.depthView != nullptr && + (entry.kind != EditorViewportKind::Scene || + (entry.selectionMaskTexture != nullptr && + entry.selectionMaskView != nullptr && + entry.selectionMaskShaderView != nullptr)) && entry.textureId != ImTextureID{}) { return true; } @@ -338,6 +382,40 @@ private: return false; } + if (entry.kind == EditorViewportKind::Scene) { + RHI::TextureDesc selectionMaskDesc = {}; + selectionMaskDesc.width = entry.width; + selectionMaskDesc.height = entry.height; + selectionMaskDesc.depth = 1; + selectionMaskDesc.mipLevels = 1; + selectionMaskDesc.arraySize = 1; + selectionMaskDesc.format = static_cast(RHI::Format::R8G8B8A8_UNorm); + selectionMaskDesc.textureType = static_cast(RHI::TextureType::Texture2D); + selectionMaskDesc.sampleCount = 1; + selectionMaskDesc.sampleQuality = 0; + selectionMaskDesc.flags = 0; + entry.selectionMaskTexture = m_device->CreateTexture(selectionMaskDesc); + if (entry.selectionMaskTexture == nullptr) { + DestroyViewportResources(entry); + return false; + } + + RHI::ResourceViewDesc selectionMaskViewDesc = {}; + selectionMaskViewDesc.format = static_cast(RHI::Format::R8G8B8A8_UNorm); + selectionMaskViewDesc.dimension = RHI::ResourceViewDimension::Texture2D; + entry.selectionMaskView = m_device->CreateRenderTargetView(entry.selectionMaskTexture, selectionMaskViewDesc); + if (entry.selectionMaskView == nullptr) { + DestroyViewportResources(entry); + return false; + } + + entry.selectionMaskShaderView = m_device->CreateShaderResourceView(entry.selectionMaskTexture, selectionMaskViewDesc); + if (entry.selectionMaskShaderView == nullptr) { + DestroyViewportResources(entry); + return false; + } + } + m_backend->AllocateTextureDescriptor(&entry.imguiCpuHandle, &entry.imguiGpuHandle); if (entry.imguiCpuHandle.ptr == 0 || entry.imguiGpuHandle.ptr == 0) { DestroyViewportResources(entry); @@ -357,6 +435,7 @@ private: entry.textureId = static_cast(entry.imguiGpuHandle.ptr); entry.colorState = RHI::ResourceStates::Common; + entry.selectionMaskState = RHI::ResourceStates::Common; return true; } @@ -369,8 +448,18 @@ private: return surface; } + Rendering::RenderSurface BuildSelectionMaskSurface(const ViewportEntry& entry) const { + Rendering::RenderSurface surface(entry.width, entry.height); + surface.SetColorAttachment(entry.selectionMaskView); + surface.SetDepthAttachment(entry.depthView); + surface.SetColorStateBefore(entry.selectionMaskState); + surface.SetColorStateAfter(RHI::ResourceStates::PixelShaderResource); + return surface; + } + void RenderViewportEntry( ViewportEntry& entry, + IEditorContext& context, const Components::Scene* scene, const Rendering::RenderContext& renderContext) { if (entry.colorView == nullptr || entry.depthView == nullptr) { @@ -378,12 +467,6 @@ private: return; } - if (scene == nullptr) { - entry.statusText = "No active scene"; - ClearViewport(entry, renderContext, 0.07f, 0.08f, 0.10f, 1.0f); - return; - } - Rendering::RenderSurface surface = BuildSurface(entry); if (entry.kind == EditorViewportKind::Scene) { @@ -394,14 +477,130 @@ private: } ApplySceneViewCameraController(); - if (!m_sceneRenderer->Render(*scene, m_sceneViewCamera.camera, renderContext, surface)) { + if (scene == nullptr) { + entry.statusText = "No active scene"; + ClearViewport(entry, renderContext, 0.07f, 0.08f, 0.10f, 1.0f); + } else if (!m_sceneRenderer->Render(*scene, m_sceneViewCamera.camera, renderContext, surface)) { entry.statusText = "Scene renderer failed"; ClearViewport(entry, renderContext, 0.18f, 0.07f, 0.07f, 1.0f); return; + } else { + entry.colorState = RHI::ResourceStates::PixelShaderResource; + entry.statusText.clear(); } - entry.colorState = RHI::ResourceStates::PixelShaderResource; - entry.statusText.clear(); + const SceneViewportOverlayData overlay = GetSceneViewOverlayData(); + if (overlay.valid) { + RHI::RHICommandList* commandList = renderContext.commandList; + bool hasSelection = false; + Rendering::RenderCameraData cameraData = {}; + std::vector selectionRenderables; + + if (scene != nullptr) { + selectionRenderables = CollectSceneViewportSelectionRenderables( + *scene, + context.GetSelectionManager().GetSelectedEntities(), + overlay.cameraPosition); + if (!selectionRenderables.empty()) { + hasSelection = true; + cameraData = BuildSceneViewportCameraData( + *m_sceneViewCamera.camera, + entry.width, + entry.height); + } + } + + commandList->TransitionBarrier( + entry.colorView, + entry.colorState, + RHI::ResourceStates::RenderTarget); + entry.colorState = RHI::ResourceStates::RenderTarget; + + if (kDebugSceneSelectionMask && hasSelection) { + const float debugClearColor[4] = { 0.0f, 0.0f, 0.0f, 1.0f }; + RHI::RHIResourceView* colorView = entry.colorView; + commandList->SetRenderTargets(1, &colorView, entry.depthView); + commandList->ClearRenderTarget(colorView, debugClearColor); + + if (!m_sceneSelectionMaskPass.Render( + renderContext, + surface, + cameraData, + selectionRenderables) && + entry.statusText.empty()) { + entry.statusText = "Scene selection mask debug pass failed"; + } + + commandList->TransitionBarrier( + entry.colorView, + entry.colorState, + RHI::ResourceStates::PixelShaderResource); + entry.colorState = RHI::ResourceStates::PixelShaderResource; + return; + } + + if (hasSelection) { + if (entry.selectionMaskView == nullptr || entry.selectionMaskShaderView == nullptr) { + entry.statusText = entry.statusText.empty() + ? "Scene selection mask target is unavailable" + : entry.statusText; + } else { + Rendering::RenderSurface selectionMaskSurface = BuildSelectionMaskSurface(entry); + commandList->TransitionBarrier( + entry.selectionMaskView, + entry.selectionMaskState, + RHI::ResourceStates::RenderTarget); + entry.selectionMaskState = RHI::ResourceStates::RenderTarget; + + const float maskClearColor[4] = { 0.0f, 0.0f, 0.0f, 0.0f }; + RHI::RHIResourceView* maskView = entry.selectionMaskView; + commandList->SetRenderTargets(1, &maskView, entry.depthView); + commandList->ClearRenderTarget(maskView, maskClearColor); + + if (!m_sceneSelectionMaskPass.Render( + renderContext, + selectionMaskSurface, + cameraData, + selectionRenderables) && + entry.statusText.empty()) { + entry.statusText = "Scene selection mask pass failed"; + } + + commandList->TransitionBarrier( + entry.selectionMaskView, + entry.selectionMaskState, + RHI::ResourceStates::PixelShaderResource); + entry.selectionMaskState = RHI::ResourceStates::PixelShaderResource; + } + } + + if (!m_sceneGridPass.Render(renderContext, surface, overlay)) { + entry.statusText = entry.statusText.empty() + ? "Scene grid pass failed" + : entry.statusText; + } + + if (hasSelection && + !m_sceneSelectionOutlinePass.Render( + renderContext, + surface, + entry.selectionMaskShaderView) && + entry.statusText.empty()) { + entry.statusText = "Scene selection outline pass failed"; + } + + commandList->TransitionBarrier( + entry.colorView, + entry.colorState, + RHI::ResourceStates::PixelShaderResource); + entry.colorState = RHI::ResourceStates::PixelShaderResource; + } + return; + } + + if (scene == nullptr) { + entry.statusText = "No active scene"; + ClearViewport(entry, renderContext, 0.07f, 0.08f, 0.10f, 1.0f); return; } @@ -456,6 +655,24 @@ private: m_backend->FreeTextureDescriptor(entry.imguiCpuHandle, entry.imguiGpuHandle); } + if (entry.selectionMaskShaderView != nullptr) { + entry.selectionMaskShaderView->Shutdown(); + delete entry.selectionMaskShaderView; + entry.selectionMaskShaderView = nullptr; + } + + if (entry.selectionMaskView != nullptr) { + entry.selectionMaskView->Shutdown(); + delete entry.selectionMaskView; + entry.selectionMaskView = nullptr; + } + + if (entry.selectionMaskTexture != nullptr) { + entry.selectionMaskTexture->Shutdown(); + delete entry.selectionMaskTexture; + entry.selectionMaskTexture = nullptr; + } + if (entry.depthView != nullptr) { entry.depthView->Shutdown(); delete entry.depthView; @@ -486,6 +703,7 @@ private: entry.imguiGpuHandle = {}; entry.textureId = {}; entry.colorState = RHI::ResourceStates::Common; + entry.selectionMaskState = RHI::ResourceStates::Common; } UI::ImGuiBackendBridge* m_backend = nullptr; @@ -493,6 +711,9 @@ private: std::unique_ptr m_sceneRenderer; std::array m_entries = {}; SceneViewCameraState m_sceneViewCamera; + SceneViewportInfiniteGridPass m_sceneGridPass; + SceneViewportSelectionMaskPass m_sceneSelectionMaskPass; + SceneViewportSelectionOutlinePass m_sceneSelectionOutlinePass; }; } // namespace Editor diff --git a/editor/src/panels/HierarchyPanel.cpp b/editor/src/panels/HierarchyPanel.cpp index 6278ecb3..32969d5f 100644 --- a/editor/src/panels/HierarchyPanel.cpp +++ b/editor/src/panels/HierarchyPanel.cpp @@ -111,11 +111,20 @@ void HierarchyPanel::Render() { RenderEntity(gameObject); } - Actions::HandleHierarchyBackgroundPrimaryClick(*m_context, m_renameState); - Actions::RequestHierarchyBackgroundContextPopup(m_backgroundContextMenu); + Actions::DrawHierarchyBackgroundInteraction(*m_context, m_renameState); Actions::DrawHierarchyEntityContextPopup(*m_context, m_itemContextMenu); - Actions::DrawHierarchyBackgroundContextPopup(*m_context, m_backgroundContextMenu); - Actions::DrawHierarchyRootDropTarget(*m_context); + static bool s_backgroundContextOpen = false; + if (UI::BeginContextMenuForLastItem("##HierarchyBackgroundContext")) { + if (!s_backgroundContextOpen) { + Actions::TraceHierarchyPopup("Hierarchy background popup opened via background surface"); + s_backgroundContextOpen = true; + } + Actions::DrawHierarchyCreateActions(*m_context, nullptr); + UI::EndContextMenu(); + } else if (s_backgroundContextOpen) { + Actions::TraceHierarchyPopup("Hierarchy background popup closed"); + s_backgroundContextOpen = false; + } } ImGui::PopStyleColor(2); } @@ -126,22 +135,16 @@ void HierarchyPanel::RenderEntity(::XCEngine::Components::GameObject* gameObject ImGui::PushID(static_cast(gameObject->GetID())); if (m_renameState.IsEditing(gameObject->GetID())) { - if (m_renameState.ConsumeFocusRequest()) { - ImGui::SetKeyboardFocusHere(); - } - - ImGui::SetNextItemWidth(-1); - if (ImGui::InputText( - "##Rename", - m_renameState.Buffer(), - m_renameState.BufferSize(), - ImGuiInputTextFlags_EnterReturnsTrue | ImGuiInputTextFlags_AutoSelectAll)) { - CommitRename(); - } + const UI::InlineRenameFieldResult renameField = UI::DrawInlineRenameField( + "##Rename", + m_renameState.Buffer(), + m_renameState.BufferSize(), + -1.0f, + m_renameState.ConsumeFocusRequest()); - if (ImGui::IsItemActive() && ImGui::IsKeyPressed(ImGuiKey_Escape)) { + if (renameField.cancelRequested) { CancelRename(); - } else if (!ImGui::IsItemActive() && ImGui::IsMouseClicked(0)) { + } else if (renameField.submitted || renameField.deactivated) { CommitRename(); } } else { @@ -161,10 +164,6 @@ void HierarchyPanel::RenderEntity(::XCEngine::Components::GameObject* gameObject if (node.secondaryClicked) { Actions::HandleHierarchyItemContextRequest(*m_context, gameObject, m_itemContextMenu); } - - if (node.doubleClicked) { - BeginRename(gameObject); - } }; nodeDefinition.callbacks.onRenderExtras = [this, gameObject]() { Actions::BeginHierarchyEntityDrag(gameObject); diff --git a/editor/src/panels/InspectorPanel.cpp b/editor/src/panels/InspectorPanel.cpp index 32303719..80c8b4f0 100644 --- a/editor/src/panels/InspectorPanel.cpp +++ b/editor/src/panels/InspectorPanel.cpp @@ -16,6 +16,32 @@ namespace XCEngine { namespace Editor { +namespace { + +template +void QueueDeferredAction(std::function& pendingAction, Fn&& fn) { + if (!pendingAction) { + pendingAction = std::forward(fn); + } +} + +void DrawInspectorComponentContextMenu( + IEditorContext& context, + ::XCEngine::Components::Component* component, + ::XCEngine::Components::GameObject* gameObject, + std::function& pendingAction) { + auto* contextPtr = &context; + IComponentEditor* editor = ComponentEditorRegistry::Get().FindEditor(component); + const bool canRemove = Commands::CanRemoveComponent(component, editor); + Actions::DrawMenuAction(Actions::MakeRemoveComponentAction(canRemove), [&]() { + QueueDeferredAction(pendingAction, [contextPtr, component, gameObject, editor]() { + Commands::RemoveComponent(*contextPtr, component, gameObject, editor); + }); + }); +} + +} // namespace + InspectorPanel::InspectorPanel() : Panel("Inspector") {} InspectorPanel::~InspectorPanel() { @@ -91,14 +117,24 @@ void InspectorPanel::RenderGameObject(::XCEngine::Components::GameObject* gameOb } auto components = gameObject->GetComponents<::XCEngine::Components::Component>(); + m_deferredContextAction = {}; for (auto* component : components) { RenderComponent(component, gameObject); + if (m_deferredContextAction) { + break; + } } Actions::DrawInspectorAddComponentButton(m_addComponentPopup, gameObject != nullptr); Actions::DrawInspectorAddComponentPopup(*m_context, m_addComponentPopup, gameObject); Actions::FinalizeInspectorInteractiveChangeIfIdle(*m_context); + + if (m_deferredContextAction) { + auto deferredAction = std::move(m_deferredContextAction); + m_deferredContextAction = {}; + deferredAction(); + } } void InspectorPanel::RenderEmptyState(const char* title, const char* subtitle) { @@ -119,30 +155,29 @@ void InspectorPanel::RenderComponent(::XCEngine::Components::Component* componen IComponentEditor* editor = ComponentEditorRegistry::Get().FindEditor(component); const std::string name = component->GetName(); - bool removed = false; const UI::ComponentSectionResult section = UI::BeginComponentSection( (void*)typeid(*component).hash_code(), - name.c_str(), - [&]() { - removed = Actions::DrawInspectorComponentMenu(*m_context, component, gameObject, editor); - }); + name.c_str()); - if (removed) { - return; + if (UI::BeginContextMenuForLastItem("##InspectorComponentContext")) { + DrawInspectorComponentContextMenu(*m_context, component, gameObject, m_deferredContextAction); + UI::EndContextMenu(); } if (section.open) { ImGui::Indent(section.contentIndent); - ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, UI::InspectorComponentControlSpacing()); - if (editor) { - if (editor->Render(component, &m_context->GetUndoManager())) { - m_context->GetSceneManager().MarkSceneDirty(); + if (!m_deferredContextAction) { + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, UI::InspectorComponentControlSpacing()); + if (editor) { + if (editor->Render(component, &m_context->GetUndoManager())) { + m_context->GetSceneManager().MarkSceneDirty(); + } + } else { + UI::DrawHintText("No registered editor for this component"); } - } else { - UI::DrawHintText("No registered editor for this component"); + ImGui::PopStyleVar(); } - ImGui::PopStyleVar(); - + UI::EndComponentSection(section); } } diff --git a/editor/src/panels/InspectorPanel.h b/editor/src/panels/InspectorPanel.h index fb51e3b4..5c4d2e4b 100644 --- a/editor/src/panels/InspectorPanel.h +++ b/editor/src/panels/InspectorPanel.h @@ -4,6 +4,7 @@ #include "UI/PopupState.h" #include +#include namespace XCEngine { namespace Components { @@ -31,6 +32,7 @@ private: uint64_t m_selectionHandlerId = 0; uint64_t m_selectedEntityId = 0; UI::DeferredPopupState m_addComponentPopup; + std::function m_deferredContextAction; }; } diff --git a/editor/src/panels/ProjectPanel.cpp b/editor/src/panels/ProjectPanel.cpp index 50e58cf2..e35bd20d 100644 --- a/editor/src/panels/ProjectPanel.cpp +++ b/editor/src/panels/ProjectPanel.cpp @@ -16,6 +16,13 @@ namespace Editor { namespace { +template +void QueueDeferredAction(std::function& pendingAction, Fn&& fn) { + if (!pendingAction) { + pendingAction = std::forward(fn); + } +} + void DrawProjectFolderTreePrefix(const UI::TreeNodePrefixContext& context) { if (!context.drawList) { return; @@ -186,6 +193,7 @@ void ProjectPanel::Render() { auto& manager = m_context->GetProjectManager(); BeginAssetDragDropFrame(); + m_deferredContextAction = {}; RenderToolbar(); ImGui::PushStyleColor(ImGuiCol_ChildBg, UI::ProjectBrowserSurfaceColor()); @@ -215,6 +223,12 @@ void ProjectPanel::Render() { FinalizeAssetDragDrop(manager); ImGui::PopStyleColor(); + + if (m_deferredContextAction) { + auto deferredAction = std::move(m_deferredContextAction); + m_deferredContextAction = {}; + deferredAction(); + } } void ProjectPanel::RenderToolbar() { @@ -246,6 +260,7 @@ void ProjectPanel::RenderToolbar() { } void ProjectPanel::RenderFolderTreePane(IProjectManager& manager) { + auto* managerPtr = &manager; ImGui::PushStyleColor(ImGuiCol_ChildBg, UI::ProjectNavigationPaneBackgroundColor()); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, UI::ProjectNavigationPanePadding()); const bool open = ImGui::BeginChild("ProjectFolderTree", ImVec2(m_navigationWidth, 0.0f), false); @@ -266,6 +281,17 @@ void ProjectPanel::RenderFolderTreePane(IProjectManager& manager) { UI::DrawEmptyState("No Assets Folder"); } + if (UI::BeginContextMenuForWindow("##ProjectFolderTreeContext")) { + Actions::DrawMenuAction(Actions::MakeCreateFolderAction(), [&]() { + QueueDeferredAction(m_deferredContextAction, [this, managerPtr]() { + if (AssetItemPtr createdFolder = Commands::CreateFolder(*managerPtr, "New Folder")) { + BeginRename(createdFolder); + } + }); + }); + UI::EndContextMenu(); + } + ImGui::EndChild(); } @@ -298,9 +324,6 @@ void ProjectPanel::RenderFolderTreeNode( manager.NavigateToFolder(folder); } - if (node.secondaryClicked) { - Actions::HandleProjectItemContextRequest(manager, folder, m_itemContextMenu); - } }; const UI::TreeNodeResult node = UI::DrawTreeNode( @@ -323,6 +346,7 @@ void ProjectPanel::RenderFolderTreeNode( } void ProjectPanel::RenderBrowserPane(IProjectManager& manager) { + auto* managerPtr = &manager; ImGui::PushStyleColor(ImGuiCol_ChildBg, UI::ProjectBrowserPaneBackgroundColor()); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f)); const bool open = ImGui::BeginChild("ProjectBrowser", ImVec2(0.0f, 0.0f), false); @@ -361,7 +385,9 @@ void ProjectPanel::RenderBrowserPane(IProjectManager& manager) { } const float tileWidth = UI::AssetTileSize().x; + const float tileHeight = UI::AssetTileSize().y; const float spacing = UI::AssetGridSpacing().x; + const float rowSpacing = UI::AssetGridSpacing().y; const float panelWidth = ImGui::GetContentRegionAvail().x; int columns = static_cast((panelWidth + spacing) / (tileWidth + spacing)); if (columns < 1) { @@ -369,26 +395,33 @@ void ProjectPanel::RenderBrowserPane(IProjectManager& manager) { } AssetItemPtr pendingSelection; - AssetItemPtr pendingContextTarget; AssetItemPtr pendingOpenTarget; const std::string selectedItemPath = manager.GetSelectedItemPath(); + const ImVec2 gridOrigin = ImGui::GetCursorPos(); for (int visibleIndex = 0; visibleIndex < static_cast(visibleItems.size()); ++visibleIndex) { - if (visibleIndex > 0 && visibleIndex % columns != 0) { - ImGui::SameLine(); - } + const int column = visibleIndex % columns; + const int row = visibleIndex / columns; + ImGui::SetCursorPos(ImVec2( + gridOrigin.x + column * (tileWidth + spacing), + gridOrigin.y + row * (tileHeight + rowSpacing))); const AssetItemPtr& item = visibleItems[visibleIndex]; const AssetItemInteraction interaction = RenderAssetItem(item, selectedItemPath == item->fullPath); if (interaction.clicked) { pendingSelection = item; } - if (interaction.contextRequested) { - pendingContextTarget = item; - } if (interaction.openRequested) { pendingOpenTarget = item; break; } + if (m_deferredContextAction) { + break; + } + } + + if (!visibleItems.empty()) { + const int rowCount = (static_cast(visibleItems.size()) + columns - 1) / columns; + ImGui::SetCursorPosY(gridOrigin.y + rowCount * tileHeight + (rowCount - 1) * rowSpacing); } if (visibleItems.empty() && !search.empty()) { @@ -397,29 +430,41 @@ void ProjectPanel::RenderBrowserPane(IProjectManager& manager) { "No assets match the current search"); } - Actions::HandleProjectBackgroundPrimaryClick(manager, m_renameState); - if (pendingSelection) { - manager.SetSelectedItem(pendingSelection); - } - if (pendingContextTarget) { - Actions::HandleProjectItemContextRequest(manager, pendingContextTarget, m_itemContextMenu); - } - if (pendingOpenTarget) { - Actions::OpenProjectAsset(*m_context, pendingOpenTarget); - } - Actions::DrawProjectItemContextPopup(*m_context, m_itemContextMenu); - Actions::RequestProjectEmptyContextPopup(m_emptyContextMenu); - Actions::DrawProjectEmptyContextPopup(m_emptyContextMenu, [&]() { - if (AssetItemPtr createdFolder = Commands::CreateFolder(manager, "New Folder")) { - BeginRename(createdFolder); + if (!m_deferredContextAction) { + Actions::HandleProjectBackgroundPrimaryClick(manager, m_renameState); + if (pendingSelection) { + manager.SetSelectedItem(pendingSelection); } - }); + if (pendingOpenTarget) { + Actions::OpenProjectAsset(*m_context, pendingOpenTarget); + } + } + + if (UI::BeginContextMenuForWindow("##ProjectBrowserContext")) { + Actions::DrawMenuAction(Actions::MakeCreateFolderAction(), [&]() { + QueueDeferredAction(m_deferredContextAction, [this, managerPtr]() { + if (AssetItemPtr createdFolder = Commands::CreateFolder(*managerPtr, "New Folder")) { + BeginRename(createdFolder); + } + }); + }); + if (manager.CanNavigateBack()) { + Actions::DrawMenuSeparator(); + Actions::DrawMenuAction(Actions::MakeNavigateBackAction(true), [&]() { + QueueDeferredAction(m_deferredContextAction, [managerPtr]() { + managerPtr->NavigateBack(); + }); + }); + } + UI::EndContextMenu(); + } ImGui::EndChild(); ImGui::EndChild(); } void ProjectPanel::RenderBrowserHeader(IProjectManager& manager) { + auto* managerPtr = &manager; ImGui::PushStyleColor(ImGuiCol_ChildBg, UI::ProjectBrowserHeaderBackgroundColor()); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(10.0f, 5.0f)); const bool open = ImGui::BeginChild( @@ -445,7 +490,11 @@ void ProjectPanel::RenderBrowserHeader(IProjectManager& manager) { "Assets", manager.GetPathDepth(), [&](size_t index) { return manager.GetPathName(index); }, - [&](size_t index) { manager.NavigateToIndex(index); }); + [&](size_t index) { + QueueDeferredAction(m_deferredContextAction, [managerPtr, index]() { + managerPtr->NavigateToIndex(index); + }); + }); ImDrawList* drawList = ImGui::GetWindowDrawList(); const ImVec2 windowMin = ImGui::GetWindowPos(); @@ -478,41 +527,54 @@ ProjectPanel::AssetItemInteraction ProjectPanel::RenderAssetItem(const AssetItem UI::DrawAssetIcon(drawList, iconMin, iconMax, iconKind); }, tileOptions); + const bool secondaryClicked = !isRenaming && ImGui::IsItemClicked(ImGuiMouseButton_Right); if (isRenaming) { - const ImVec2 restoreCursor = ImGui::GetCursorPos(); - ImGui::SetCursorScreenPos(tile.labelMin); - ImGui::SetNextItemWidth(tile.labelMax.x - tile.labelMin.x); - if (m_renameState.ConsumeFocusRequest()) { - ImGui::SetKeyboardFocusHere(); - } - - const bool submitted = ImGui::InputText( + const float renameWidth = tile.labelMax.x - tile.labelMin.x; + const float renameOffsetY = (std::max)(0.0f, (tile.labelMax.y - tile.labelMin.y - UI::InlineRenameFieldHeight()) * 0.5f); + const UI::InlineRenameFieldResult renameField = UI::DrawInlineRenameFieldAt( "##Rename", + ImVec2(tile.labelMin.x, tile.labelMin.y + renameOffsetY), m_renameState.Buffer(), m_renameState.BufferSize(), - ImGuiInputTextFlags_EnterReturnsTrue | ImGuiInputTextFlags_AutoSelectAll); - const bool cancelRequested = ImGui::IsItemActive() && ImGui::IsKeyPressed(ImGuiKey_Escape); - const bool deactivated = ImGui::IsItemDeactivated(); - ImGui::SetCursorPos(restoreCursor); + renameWidth, + m_renameState.ConsumeFocusRequest()); - if (cancelRequested) { + if (renameField.cancelRequested) { CancelRename(); - } else if (submitted || deactivated) { + } else if (renameField.submitted || renameField.deactivated) { CommitRename(m_context->GetProjectManager()); } } else { if (tile.clicked) { interaction.clicked = true; } - - if (tile.contextRequested) { - interaction.contextRequested = true; + if (secondaryClicked && item) { + m_context->GetProjectManager().SetSelectedItem(item); } RegisterFolderDropTarget(m_context->GetProjectManager(), item); Actions::BeginProjectAssetDrag(item, iconKind); + if (UI::BeginContextMenuForLastItem("##ProjectItemContext")) { + Actions::DrawMenuAction(Actions::MakeOpenAssetAction(Commands::CanOpenAsset(item)), [&]() { + QueueDeferredAction(m_deferredContextAction, [this, item]() { + Actions::OpenProjectAsset(*m_context, item); + }); + }); + Actions::DrawMenuAction(Actions::MakeAction("Rename", nullptr, false, item != nullptr), [&]() { + QueueDeferredAction(m_deferredContextAction, [this, item]() { + BeginRename(item); + }); + }); + Actions::DrawMenuAction(Actions::MakeDeleteAssetAction(item != nullptr), [&]() { + QueueDeferredAction(m_deferredContextAction, [this, item]() { + Commands::DeleteAsset(m_context->GetProjectManager(), item); + }); + }); + UI::EndContextMenu(); + } + if (tile.openRequested) { interaction.openRequested = true; } diff --git a/editor/src/panels/ProjectPanel.h b/editor/src/panels/ProjectPanel.h index 65e3067c..602c4b99 100644 --- a/editor/src/panels/ProjectPanel.h +++ b/editor/src/panels/ProjectPanel.h @@ -5,6 +5,8 @@ #include "UI/PopupState.h" #include "UI/TreeView.h" +#include + namespace XCEngine { namespace Editor { @@ -35,7 +37,6 @@ private: struct AssetItemInteraction { bool clicked = false; - bool contextRequested = false; bool openRequested = false; }; @@ -58,9 +59,8 @@ private: float m_navigationWidth = UI::ProjectNavigationDefaultWidth(); UI::TreeViewState m_folderTreeState; UI::InlineTextEditState m_renameState; - UI::DeferredPopupState m_emptyContextMenu; - UI::TargetedPopupState m_itemContextMenu; AssetDragDropState m_assetDragDropState; + std::function m_deferredContextAction; }; } diff --git a/editor/src/panels/SceneViewPanel.cpp b/editor/src/panels/SceneViewPanel.cpp index dbadbaa9..ef99d296 100644 --- a/editor/src/panels/SceneViewPanel.cpp +++ b/editor/src/panels/SceneViewPanel.cpp @@ -1,233 +1,16 @@ #include "Actions/ActionRouting.h" #include "Core/IEditorContext.h" +#include "Core/ISelectionManager.h" #include "SceneViewPanel.h" +#include "Viewport/SceneViewportOverlayRenderer.h" #include "ViewportPanelContent.h" #include "UI/UI.h" -#include -#include - -#include -#include - #include namespace XCEngine { namespace Editor { -namespace { - -float ComputeGridSpacing(float orbitDistance) { - const float target = (std::max)(orbitDistance * 0.35f, 0.1f); - const float exponent = std::floor(std::log10(target)); - const float base = std::pow(10.0f, exponent); - const float normalized = target / base; - if (normalized < 2.0f) { - return base; - } - if (normalized < 5.0f) { - return 2.0f * base; - } - return 5.0f * base; -} - -Math::Matrix4x4 BuildOverlayViewMatrix(const SceneViewportOverlayData& overlay) { - return Math::Matrix4x4::LookAt( - overlay.cameraPosition, - overlay.cameraPosition + overlay.cameraForward, - overlay.cameraUp); -} - -Math::Matrix4x4 BuildOverlayProjectionMatrix( - const SceneViewportOverlayData& overlay, - const ImVec2& viewportSize) { - const float aspect = viewportSize.y > 0.0f - ? viewportSize.x / viewportSize.y - : 1.0f; - return Math::Matrix4x4::Perspective( - overlay.verticalFovDegrees * Math::DEG_TO_RAD, - aspect, - overlay.nearClipPlane, - overlay.farClipPlane); -} - -bool ProjectWorldPoint( - const Math::Matrix4x4& viewProjection, - const ImVec2& viewportMin, - const ImVec2& viewportSize, - const Math::Vector3& worldPoint, - ImVec2& screenPoint, - float& depth) { - const Math::Vector4 clip = viewProjection * Math::Vector4(worldPoint, 1.0f); - if (clip.w <= 0.001f) { - return false; - } - - const float invW = 1.0f / clip.w; - const float ndcX = clip.x * invW; - const float ndcY = clip.y * invW; - const float ndcZ = clip.z * invW; - if (ndcZ < -0.2f || ndcZ > 1.2f) { - return false; - } - - screenPoint.x = viewportMin.x + (ndcX * 0.5f + 0.5f) * viewportSize.x; - screenPoint.y = viewportMin.y + (-ndcY * 0.5f + 0.5f) * viewportSize.y; - depth = ndcZ; - return true; -} - -void DrawSceneAxisWidget( - ImDrawList* drawList, - const SceneViewportOverlayData& overlay, - const ImVec2& viewportMin, - const ImVec2& viewportMax) { - if (drawList == nullptr || !overlay.valid) { - return; - } - - const Math::Matrix4x4 view = BuildOverlayViewMatrix(overlay); - const ImVec2 center(viewportMin.x + 52.0f, viewportMax.y - 52.0f); - const float radius = 26.0f; - - drawList->AddCircleFilled(center, radius + 10.0f, IM_COL32(18, 20, 24, 170), 24); - drawList->AddCircle(center, radius + 10.0f, IM_COL32(255, 255, 255, 28), 24, 1.0f); - - struct AxisLine { - Math::Vector3 axis; - ImU32 color; - }; - - const AxisLine axes[] = { - { Math::Vector3::Right(), IM_COL32(239, 83, 80, 255) }, - { Math::Vector3::Up(), IM_COL32(102, 187, 106, 255) }, - { Math::Vector3::Forward(), IM_COL32(66, 165, 245, 255) } - }; - - for (const AxisLine& axis : axes) { - const Math::Vector3 viewAxis = view.MultiplyVector(axis.axis); - const ImVec2 end( - center.x + viewAxis.x * radius, - center.y - viewAxis.y * radius); - drawList->AddLine(center, end, axis.color, 2.2f); - drawList->AddCircleFilled(end, 4.0f, axis.color, 12); - } -} - -void DrawSceneGridOverlay( - ImDrawList* drawList, - const SceneViewportOverlayData& overlay, - const ImVec2& viewportMin, - const ImVec2& viewportMax, - const ImVec2& viewportSize) { - if (drawList == nullptr || !overlay.valid || viewportSize.x <= 1.0f || viewportSize.y <= 1.0f) { - return; - } - - const Math::Matrix4x4 viewProjection = - BuildOverlayProjectionMatrix(overlay, viewportSize) * BuildOverlayViewMatrix(overlay); - const float spacing = ComputeGridSpacing(overlay.orbitDistance); - const int halfLineCount = 14; - const float extent = spacing * static_cast(halfLineCount); - - drawList->PushClipRect(viewportMin, viewportMax, true); - - for (int lineIndex = -halfLineCount; lineIndex <= halfLineCount; ++lineIndex) { - const float offset = static_cast(lineIndex) * spacing; - const bool majorLine = (lineIndex % 5) == 0; - const ImU32 lineColor = majorLine - ? IM_COL32(255, 255, 255, 58) - : IM_COL32(255, 255, 255, 24); - - ImVec2 a = {}; - ImVec2 b = {}; - ImVec2 c = {}; - ImVec2 d = {}; - float depthA = 0.0f; - float depthB = 0.0f; - float depthC = 0.0f; - float depthD = 0.0f; - - if (ProjectWorldPoint( - viewProjection, - viewportMin, - viewportSize, - Math::Vector3(offset, 0.0f, -extent), - a, - depthA) && - ProjectWorldPoint( - viewProjection, - viewportMin, - viewportSize, - Math::Vector3(offset, 0.0f, extent), - b, - depthB)) { - drawList->AddLine(a, b, lineColor, majorLine ? 1.35f : 1.0f); - } - - if (ProjectWorldPoint( - viewProjection, - viewportMin, - viewportSize, - Math::Vector3(-extent, 0.0f, offset), - c, - depthC) && - ProjectWorldPoint( - viewProjection, - viewportMin, - viewportSize, - Math::Vector3(extent, 0.0f, offset), - d, - depthD)) { - drawList->AddLine(c, d, lineColor, majorLine ? 1.35f : 1.0f); - } - } - - ImVec2 origin = {}; - ImVec2 xAxisEnd = {}; - ImVec2 yAxisEnd = {}; - ImVec2 zAxisEnd = {}; - float originDepth = 0.0f; - float xDepth = 0.0f; - float yDepth = 0.0f; - float zDepth = 0.0f; - const float axisLength = spacing * 3.0f; - if (ProjectWorldPoint(viewProjection, viewportMin, viewportSize, Math::Vector3::Zero(), origin, originDepth)) { - if (ProjectWorldPoint( - viewProjection, - viewportMin, - viewportSize, - Math::Vector3(axisLength, 0.0f, 0.0f), - xAxisEnd, - xDepth)) { - drawList->AddLine(origin, xAxisEnd, IM_COL32(239, 83, 80, 220), 1.8f); - } - if (ProjectWorldPoint( - viewProjection, - viewportMin, - viewportSize, - Math::Vector3(0.0f, axisLength, 0.0f), - yAxisEnd, - yDepth)) { - drawList->AddLine(origin, yAxisEnd, IM_COL32(102, 187, 106, 220), 1.8f); - } - if (ProjectWorldPoint( - viewProjection, - viewportMin, - viewportSize, - Math::Vector3(0.0f, 0.0f, axisLength), - zAxisEnd, - zDepth)) { - drawList->AddLine(origin, zAxisEnd, IM_COL32(66, 165, 245, 220), 1.8f); - } - } - - DrawSceneAxisWidget(drawList, overlay, viewportMin, viewportMax); - drawList->PopClipRect(); -} - -} // namespace - SceneViewPanel::SceneViewPanel() : Panel("Scene") {} void SceneViewPanel::Render() { @@ -239,35 +22,76 @@ void SceneViewPanel::Render() { const ViewportPanelContentResult content = RenderViewportPanelContent(*m_context, EditorViewportKind::Scene); if (IViewportHostService* viewportHostService = m_context->GetViewportHostService()) { const ImGuiIO& io = ImGui::GetIO(); + const bool selectClick = + content.hovered && + content.frame.hasTexture && + ImGui::IsMouseClicked(ImGuiMouseButton_Left) && + !m_lookDragging && + !m_panDragging; + const bool beginLookDrag = + content.hovered && ImGui::IsMouseClicked(ImGuiMouseButton_Right); + const bool beginPanDrag = + content.hovered && ImGui::IsMouseClicked(ImGuiMouseButton_Middle); - if (!m_lookDragging && content.hovered && ImGui::IsMouseClicked(ImGuiMouseButton_Right)) { - m_lookDragging = true; + if (selectClick || beginLookDrag || beginPanDrag) { + ImGui::SetWindowFocus(); } - if (!m_panDragging && content.hovered && ImGui::IsMouseClicked(ImGuiMouseButton_Middle)) { + + if (selectClick) { + const ImVec2 localMousePosition( + io.MousePos.x - content.itemMin.x, + io.MousePos.y - content.itemMin.y); + const uint64_t selectedEntity = viewportHostService->PickSceneViewEntity( + *m_context, + content.availableSize, + localMousePosition); + if (selectedEntity != 0) { + m_context->GetSelectionManager().SetSelectedEntity(selectedEntity); + } else { + m_context->GetSelectionManager().ClearSelection(); + } + } + + if (beginLookDrag) { + m_lookDragging = true; + m_lastLookDragDelta = ImVec2(0.0f, 0.0f); + } + if (beginPanDrag) { m_panDragging = true; + m_lastPanDragDelta = ImVec2(0.0f, 0.0f); } if (m_lookDragging && !ImGui::IsMouseDown(ImGuiMouseButton_Right)) { m_lookDragging = false; + m_lastLookDragDelta = ImVec2(0.0f, 0.0f); } if (m_panDragging && !ImGui::IsMouseDown(ImGuiMouseButton_Middle)) { m_panDragging = false; + m_lastPanDragDelta = ImVec2(0.0f, 0.0f); + } + + if (m_lookDragging || m_panDragging) { + ImGui::SetNextFrameWantCaptureMouse(true); + } + if (m_lookDragging) { + ImGui::SetNextFrameWantCaptureKeyboard(true); } SceneViewportInput input = {}; input.viewportSize = content.availableSize; input.deltaTime = io.DeltaTime; input.hovered = content.hovered; - input.focused = content.focused; - input.mouseWheel = content.hovered ? -io.MouseWheel : 0.0f; + input.focused = content.focused || m_lookDragging || m_panDragging; + input.mouseWheel = (content.hovered && !m_lookDragging) ? io.MouseWheel : 0.0f; + input.flySpeedDelta = (content.hovered && m_lookDragging) ? io.MouseWheel : 0.0f; input.looking = m_lookDragging; input.orbiting = false; input.panning = m_panDragging; input.fastMove = io.KeyShift; input.focusSelectionRequested = - content.focused && !io.WantTextInput && ImGui::IsKeyPressed(ImGuiKey_F, false); + input.focused && !io.WantTextInput && ImGui::IsKeyPressed(ImGuiKey_F, false); - if (m_lookDragging && content.focused && !io.WantTextInput) { + if (m_lookDragging && !io.WantTextInput) { input.moveForward = (ImGui::IsKeyDown(ImGuiKey_W) ? 1.0f : 0.0f) - (ImGui::IsKeyDown(ImGuiKey_S) ? 1.0f : 0.0f); @@ -280,14 +104,30 @@ void SceneViewPanel::Render() { } if (m_lookDragging || m_panDragging) { - input.mouseDelta = io.MouseDelta; + if (m_lookDragging) { + const ImVec2 lookDragDelta = ImGui::GetMouseDragDelta(ImGuiMouseButton_Right, 0.0f); + input.mouseDelta.x += lookDragDelta.x - m_lastLookDragDelta.x; + input.mouseDelta.y += lookDragDelta.y - m_lastLookDragDelta.y; + m_lastLookDragDelta = lookDragDelta; + } else { + m_lastLookDragDelta = ImVec2(0.0f, 0.0f); + } + + if (m_panDragging) { + const ImVec2 panDragDelta = ImGui::GetMouseDragDelta(ImGuiMouseButton_Middle, 0.0f); + input.mouseDelta.x += panDragDelta.x - m_lastPanDragDelta.x; + input.mouseDelta.y += panDragDelta.y - m_lastPanDragDelta.y; + m_lastPanDragDelta = panDragDelta; + } else { + m_lastPanDragDelta = ImVec2(0.0f, 0.0f); + } } viewportHostService->UpdateSceneViewInput(*m_context, input); if (content.hasViewportArea && content.frame.hasTexture) { const SceneViewportOverlayData overlay = viewportHostService->GetSceneViewOverlayData(); - DrawSceneGridOverlay( + DrawSceneViewportOverlay( ImGui::GetWindowDrawList(), overlay, content.itemMin, diff --git a/editor/src/panels/SceneViewPanel.h b/editor/src/panels/SceneViewPanel.h index 11ab8fd6..8f21dec0 100644 --- a/editor/src/panels/SceneViewPanel.h +++ b/editor/src/panels/SceneViewPanel.h @@ -2,6 +2,8 @@ #include "Panel.h" +#include + namespace XCEngine { namespace Editor { @@ -13,6 +15,8 @@ public: private: bool m_lookDragging = false; bool m_panDragging = false; + ImVec2 m_lastLookDragDelta = ImVec2(0.0f, 0.0f); + ImVec2 m_lastPanDragDelta = ImVec2(0.0f, 0.0f); }; } diff --git a/tests/editor/CMakeLists.txt b/tests/editor/CMakeLists.txt index 21471e83..174b77a2 100644 --- a/tests/editor/CMakeLists.txt +++ b/tests/editor/CMakeLists.txt @@ -5,9 +5,14 @@ project(XCEngine_EditorTests) set(EDITOR_TEST_SOURCES test_action_routing.cpp test_scene_viewport_camera_controller.cpp + test_scene_viewport_picker.cpp + test_scene_viewport_overlay_renderer.cpp + test_scene_viewport_selection_utils.cpp ${CMAKE_SOURCE_DIR}/editor/src/Core/UndoManager.cpp ${CMAKE_SOURCE_DIR}/editor/src/Managers/SceneManager.cpp ${CMAKE_SOURCE_DIR}/editor/src/Managers/ProjectManager.cpp + ${CMAKE_SOURCE_DIR}/editor/src/Viewport/SceneViewportPicker.cpp + ${CMAKE_SOURCE_DIR}/editor/src/Viewport/SceneViewportGrid.cpp ) add_executable(editor_tests ${EDITOR_TEST_SOURCES}) diff --git a/tests/editor/test_scene_viewport_camera_controller.cpp b/tests/editor/test_scene_viewport_camera_controller.cpp index de91098b..f22d3abc 100644 --- a/tests/editor/test_scene_viewport_camera_controller.cpp +++ b/tests/editor/test_scene_viewport_camera_controller.cpp @@ -41,6 +41,22 @@ TEST(SceneViewportCameraController_Test, ApplyToMatchesComputedPositionAndForwar EXPECT_GT(Vector3::Dot(cameraObject.GetTransform()->GetUp().Normalized(), Vector3::Up()), 0.0f); } +TEST(SceneViewportCameraController_Test, ApplyToLooksAtControllerFocalPointInViewSpace) { + SceneViewportCameraController controller; + controller.Reset(); + controller.Focus(Vector3(2.0f, 1.0f, -3.0f)); + + GameObject cameraObject("EditorCamera"); + controller.ApplyTo(*cameraObject.GetTransform()); + + const Vector3 focalPointInViewSpace = + cameraObject.GetTransform()->InverseTransformPoint(controller.GetFocalPoint()); + + EXPECT_NEAR(focalPointInViewSpace.x, 0.0f, 1e-3f); + EXPECT_NEAR(focalPointInViewSpace.y, 0.0f, 1e-3f); + EXPECT_NEAR(focalPointInViewSpace.z, controller.GetDistance(), 1e-3f); +} + TEST(SceneViewportCameraController_Test, LookInputRotatesCameraInPlaceAndKeepsDistance) { SceneViewportCameraController controller; controller.Reset(); @@ -57,7 +73,7 @@ TEST(SceneViewportCameraController_Test, LookInputRotatesCameraInPlaceAndKeepsDi EXPECT_TRUE(NearlyEqual(controller.GetPosition(), initialPosition)); EXPECT_FALSE(NearlyEqual(controller.GetFocalPoint(), initialFocus)); - EXPECT_LT(controller.GetPitchDegrees(), initialPitch); + EXPECT_GT(controller.GetPitchDegrees(), initialPitch); EXPECT_TRUE(NearlyEqual( controller.GetFocalPoint(), controller.GetPosition() + controller.GetForward() * controller.GetDistance(), @@ -81,7 +97,7 @@ TEST(SceneViewportCameraController_Test, OrbitInputRotatesAroundFocalPointAndKee EXPECT_FALSE(NearlyEqual(controller.GetPosition(), initialPosition)); EXPECT_TRUE(NearlyEqual(controller.GetFocalPoint(), initialFocus)); - EXPECT_LT(controller.GetPitchDegrees(), initialPitch); + EXPECT_GT(controller.GetPitchDegrees(), initialPitch); EXPECT_NEAR((controller.GetFocalPoint() - controller.GetPosition()).Magnitude(), initialDistance, 1e-3f); } @@ -107,8 +123,8 @@ TEST(SceneViewportCameraController_Test, PanAndZoomUpdateCameraStateConsistently EXPECT_LT(controller.GetDistance(), initialDistance); const Vector3 panDelta = controller.GetFocalPoint() - initialFocus; EXPECT_NEAR(Vector3::Dot(panDelta, controller.GetForward()), 0.0f, 1e-3f); - EXPECT_GT(std::abs(Vector3::Dot(panDelta, right)), 0.0f); - EXPECT_GT(std::abs(Vector3::Dot(panDelta, up)), 0.0f); + EXPECT_LT(Vector3::Dot(panDelta, right), 0.0f); + EXPECT_LT(Vector3::Dot(panDelta, up), 0.0f); EXPECT_TRUE(NearlyEqual( controller.GetFocalPoint(), controller.GetPosition() + controller.GetForward() * controller.GetDistance(), @@ -135,11 +151,65 @@ TEST(SceneViewportCameraController_Test, FlyInputMovesCameraAndFocalPointTogethe EXPECT_FALSE(NearlyEqual(controller.GetPosition(), initialPosition)); EXPECT_FALSE(NearlyEqual(controller.GetFocalPoint(), initialFocus)); const Vector3 positionDelta = controller.GetPosition() - initialPosition; - EXPECT_GT(std::abs(Vector3::Dot(positionDelta, forward)), 0.0f); - EXPECT_GT(std::abs(Vector3::Dot(positionDelta, right)), 0.0f); + EXPECT_GT(Vector3::Dot(positionDelta, forward), 0.0f); + EXPECT_GT(Vector3::Dot(positionDelta, right), 0.0f); EXPECT_TRUE(NearlyEqual(controller.GetFocalPoint() - controller.GetPosition(), initialOffset, 1e-3f)); } +TEST(SceneViewportCameraController_Test, ZoomDoesNotChangeFlySpeed) { + SceneViewportCameraController zoomedController; + zoomedController.Reset(); + const Vector3 zoomedInitialPosition = zoomedController.GetPosition(); + + SceneViewportCameraController baselineController; + baselineController.Reset(); + const Vector3 baselineInitialPosition = baselineController.GetPosition(); + + SceneViewportCameraInputState zoomInput = {}; + zoomInput.viewportHeight = 720.0f; + zoomInput.zoomDelta = 8.0f; + zoomedController.ApplyInput(zoomInput); + + SceneViewportCameraInputState moveInput = {}; + moveInput.viewportHeight = 720.0f; + moveInput.deltaTime = 0.5f; + moveInput.moveForward = 1.0f; + zoomedController.ApplyInput(moveInput); + baselineController.ApplyInput(moveInput); + + EXPECT_FLOAT_EQ(zoomedController.GetFlySpeed(), baselineController.GetFlySpeed()); + const float zoomedTravel = (zoomedController.GetPosition() - zoomedInitialPosition).Magnitude(); + const float baselineTravel = (baselineController.GetPosition() - baselineInitialPosition).Magnitude(); + EXPECT_NEAR(zoomedTravel, baselineTravel, 1e-3f); +} + +TEST(SceneViewportCameraController_Test, FlySpeedDeltaAdjustsMovementSpeedIndependentlyFromZoom) { + SceneViewportCameraController fasterController; + fasterController.Reset(); + const Vector3 fasterInitialPosition = fasterController.GetPosition(); + + SceneViewportCameraController baselineController; + baselineController.Reset(); + const Vector3 baselineInitialPosition = baselineController.GetPosition(); + + SceneViewportCameraInputState speedInput = {}; + speedInput.viewportHeight = 720.0f; + speedInput.flySpeedDelta = 4.0f; + fasterController.ApplyInput(speedInput); + + SceneViewportCameraInputState moveInput = {}; + moveInput.viewportHeight = 720.0f; + moveInput.deltaTime = 0.5f; + moveInput.moveForward = 1.0f; + fasterController.ApplyInput(moveInput); + baselineController.ApplyInput(moveInput); + + const float fasterTravel = (fasterController.GetPosition() - fasterInitialPosition).Magnitude(); + const float baselineTravel = (baselineController.GetPosition() - baselineInitialPosition).Magnitude(); + EXPECT_GT(fasterController.GetFlySpeed(), baselineController.GetFlySpeed()); + EXPECT_GT(fasterTravel, baselineTravel); +} + TEST(SceneViewportCameraController_Test, FocusMovesPivotWithoutChangingDistance) { SceneViewportCameraController controller; controller.Reset(); diff --git a/tests/editor/test_scene_viewport_overlay_renderer.cpp b/tests/editor/test_scene_viewport_overlay_renderer.cpp new file mode 100644 index 00000000..b4b42b0d --- /dev/null +++ b/tests/editor/test_scene_viewport_overlay_renderer.cpp @@ -0,0 +1,221 @@ +#include + +#include "Viewport/SceneViewportCameraController.h" +#include "Viewport/SceneViewportGrid.h" +#include "Viewport/SceneViewportMath.h" + +#include +#include +#include + +namespace { + +bool NearlyEqual(float lhs, float rhs, float epsilon = 1e-4f) { + return std::abs(lhs - rhs) <= epsilon; +} + +bool NearlyEqual(const XCEngine::Math::Vector3& lhs, const XCEngine::Math::Vector3& rhs, float epsilon = 1e-4f) { + return NearlyEqual(lhs.x, rhs.x, epsilon) && + NearlyEqual(lhs.y, rhs.y, epsilon) && + NearlyEqual(lhs.z, rhs.z, epsilon); +} + +bool NearlyEqual( + const XCEngine::Math::Matrix4x4& lhs, + const XCEngine::Math::Matrix4x4& rhs, + float epsilon = 1e-4f) { + for (int row = 0; row < 4; ++row) { + for (int column = 0; column < 4; ++column) { + if (!NearlyEqual(lhs.m[row][column], rhs.m[row][column], epsilon)) { + return false; + } + } + } + return true; +} + +bool IsPowerOfTenSpacing(float value) { + if (value <= 0.0f) { + return false; + } + + const float exponent = std::floor(std::log10(value)); + const float base = std::pow(10.0f, exponent); + const float normalized = value / base; + return NearlyEqual(normalized, 1.0f, 1e-4f); +} + +} // namespace + +using XCEngine::Editor::BuildSceneGridParameters; +using XCEngine::Editor::SceneViewportCameraController; +using XCEngine::Editor::BuildSceneViewportProjectionMatrix; +using XCEngine::Editor::BuildSceneViewportViewMatrix; +using XCEngine::Editor::SceneGridParameters; +using XCEngine::Editor::SceneViewportOverlayData; +using XCEngine::Components::GameObject; +using XCEngine::Math::Vector3; +using XCEngine::Math::Vector4; + +TEST(SceneViewportOverlayRenderer_Test, BuildSceneGridParametersUsesPowerOfTenSpacingSeries) { + SceneViewportOverlayData overlay = {}; + overlay.valid = true; + overlay.cameraPosition = Vector3(0.0f, 6.0f, -6.0f); + overlay.cameraForward = Vector3(0.0f, -0.5f, 0.8660254f); + + const SceneGridParameters parameters = BuildSceneGridParameters(overlay); + + EXPECT_TRUE(parameters.valid); + EXPECT_TRUE(IsPowerOfTenSpacing(parameters.baseScale)); + EXPECT_GE(parameters.transitionBlend, 0.0f); + EXPECT_LE(parameters.transitionBlend, 1.0f); + EXPECT_GT(parameters.fadeDistance, parameters.baseScale); +} + +TEST(SceneViewportOverlayRenderer_Test, BuildSceneGridParametersIsStableAcrossHorizontalCameraMovement) { + SceneViewportOverlayData left = {}; + left.valid = true; + left.cameraPosition = Vector3(-120.0f, 12.0f, 40.0f); + left.cameraForward = Vector3(0.0f, -0.5f, 0.8660254f); + + SceneViewportOverlayData right = left; + right.cameraPosition = Vector3(380.0f, 12.0f, -260.0f); + + const SceneGridParameters leftParameters = BuildSceneGridParameters(left); + const SceneGridParameters rightParameters = BuildSceneGridParameters(right); + + EXPECT_TRUE(leftParameters.valid); + EXPECT_TRUE(rightParameters.valid); + EXPECT_FLOAT_EQ(leftParameters.baseScale, rightParameters.baseScale); + EXPECT_FLOAT_EQ(leftParameters.transitionBlend, rightParameters.transitionBlend); + EXPECT_FLOAT_EQ(leftParameters.fadeDistance, rightParameters.fadeDistance); +} + +TEST(SceneViewportOverlayRenderer_Test, BuildSceneGridParametersExpandsScaleAsCameraHeightGrows) { + SceneViewportOverlayData nearOverlay = {}; + nearOverlay.valid = true; + nearOverlay.cameraPosition = Vector3(0.0f, 3.0f, -4.0f); + nearOverlay.cameraForward = Vector3(0.0f, -0.5f, 0.8660254f); + + SceneViewportOverlayData farOverlay = nearOverlay; + farOverlay.cameraPosition = Vector3(0.0f, 120.0f, -150.0f); + + const SceneGridParameters nearParameters = BuildSceneGridParameters(nearOverlay); + const SceneGridParameters farParameters = BuildSceneGridParameters(farOverlay); + + EXPECT_TRUE(nearParameters.valid); + EXPECT_TRUE(farParameters.valid); + EXPECT_GE(farParameters.baseScale, nearParameters.baseScale); + EXPECT_GE(farParameters.fadeDistance, nearParameters.fadeDistance); +} + +TEST(SceneViewportOverlayRenderer_Test, BuildSceneGridParametersPromotesMajorLinesByDecimalGrouping) { + SceneViewportOverlayData overlay = {}; + overlay.valid = true; + overlay.cameraPosition = Vector3(0.0f, 14.0f, -18.0f); + overlay.cameraForward = Vector3(0.0f, -0.5f, 0.8660254f); + + const SceneGridParameters parameters = BuildSceneGridParameters(overlay); + + EXPECT_TRUE(parameters.valid); + EXPECT_TRUE(IsPowerOfTenSpacing(parameters.baseScale)); + EXPECT_TRUE(IsPowerOfTenSpacing(parameters.baseScale * 10.0f)); +} + +TEST(SceneViewportOverlayRenderer_Test, BuildSceneGridParametersFadesTowardNextScaleBeforeThreshold) { + SceneViewportOverlayData earlyOverlay = {}; + earlyOverlay.valid = true; + earlyOverlay.cameraPosition = Vector3(0.0f, 4.0f, -6.0f); + + SceneViewportOverlayData lateOverlay = earlyOverlay; + lateOverlay.cameraPosition = Vector3(0.0f, 18.0f, -6.0f); + + const SceneGridParameters earlyParameters = BuildSceneGridParameters(earlyOverlay); + const SceneGridParameters lateParameters = BuildSceneGridParameters(lateOverlay); + + EXPECT_FLOAT_EQ(earlyParameters.baseScale, 1.0f); + EXPECT_FLOAT_EQ(lateParameters.baseScale, 1.0f); + EXPECT_LT(earlyParameters.transitionBlend, 0.05f); + EXPECT_GT(lateParameters.transitionBlend, 0.90f); +} + +TEST(SceneViewportOverlayRenderer_Test, BuildSceneGridParametersSwitchesScaleAtHalfThePreviousDistanceThreshold) { + SceneViewportOverlayData nearOverlay = {}; + nearOverlay.valid = true; + nearOverlay.cameraPosition = Vector3(0.0f, 19.9f, -6.0f); + + SceneViewportOverlayData farOverlay = nearOverlay; + farOverlay.cameraPosition = Vector3(0.0f, 20.0f, -6.0f); + + const SceneGridParameters nearParameters = BuildSceneGridParameters(nearOverlay); + const SceneGridParameters farParameters = BuildSceneGridParameters(farOverlay); + + EXPECT_FLOAT_EQ(nearParameters.baseScale, 1.0f); + EXPECT_FLOAT_EQ(farParameters.baseScale, 10.0f); + EXPECT_GT(nearParameters.transitionBlend, 0.95f); + EXPECT_LT(farParameters.transitionBlend, 0.05f); +} + +TEST(SceneViewportOverlayRenderer_Test, BuildSceneGridParametersIgnoresStaleOrbitDistanceForSameView) { + SceneViewportOverlayData nearOrbit = {}; + nearOrbit.valid = true; + nearOrbit.cameraPosition = Vector3(0.0f, 12.0f, -24.0f); + nearOrbit.cameraForward = Vector3(0.0f, -0.5f, 0.8660254f); + nearOrbit.orbitDistance = 2.0f; + + SceneViewportOverlayData farOrbit = nearOrbit; + farOrbit.orbitDistance = 200.0f; + + const SceneGridParameters nearOrbitParameters = BuildSceneGridParameters(nearOrbit); + const SceneGridParameters farOrbitParameters = BuildSceneGridParameters(farOrbit); + + EXPECT_TRUE(nearOrbitParameters.valid); + EXPECT_TRUE(farOrbitParameters.valid); + EXPECT_FLOAT_EQ(nearOrbitParameters.baseScale, farOrbitParameters.baseScale); + EXPECT_FLOAT_EQ(nearOrbitParameters.transitionBlend, farOrbitParameters.transitionBlend); + EXPECT_FLOAT_EQ(nearOrbitParameters.fadeDistance, farOrbitParameters.fadeDistance); +} + +TEST(SceneViewportOverlayRenderer_Test, ViewMatrixKeepsForwardWorldPointsInFrontOfCamera) { + SceneViewportOverlayData overlay = {}; + overlay.valid = true; + overlay.cameraPosition = Vector3(0.0f, 0.0f, 0.0f); + overlay.cameraForward = Vector3::Forward(); + overlay.cameraRight = Vector3::Right(); + overlay.cameraUp = Vector3::Up(); + overlay.verticalFovDegrees = 60.0f; + overlay.nearClipPlane = 0.03f; + overlay.farClipPlane = 2000.0f; + + const auto view = BuildSceneViewportViewMatrix(overlay); + const Vector3 pointInView = view.MultiplyPoint(Vector3(0.0f, 0.0f, 5.0f)); + EXPECT_TRUE(NearlyEqual(pointInView, Vector3(0.0f, 0.0f, 5.0f), 1e-4f)); + + const auto projection = BuildSceneViewportProjectionMatrix(overlay, 1280.0f, 720.0f); + const Vector4 clipPoint = projection * Vector4(Vector3(0.0f, 0.0f, 5.0f), 1.0f); + EXPECT_GT(clipPoint.w, 0.0f); +} + +TEST(SceneViewportOverlayRenderer_Test, ViewMatrixMatchesSceneCameraTransformConvention) { + SceneViewportCameraController controller; + controller.Reset(); + controller.Focus(Vector3(2.0f, 1.5f, -3.0f)); + + GameObject cameraObject("EditorCamera"); + controller.ApplyTo(*cameraObject.GetTransform()); + + SceneViewportOverlayData overlay = {}; + overlay.valid = true; + overlay.cameraPosition = cameraObject.GetTransform()->GetPosition(); + overlay.cameraForward = cameraObject.GetTransform()->GetForward(); + overlay.cameraRight = cameraObject.GetTransform()->GetRight(); + overlay.cameraUp = cameraObject.GetTransform()->GetUp(); + overlay.verticalFovDegrees = 60.0f; + overlay.nearClipPlane = 0.03f; + overlay.farClipPlane = 2000.0f; + + EXPECT_TRUE(NearlyEqual( + BuildSceneViewportViewMatrix(overlay), + cameraObject.GetTransform()->GetWorldToLocalMatrix(), + 1e-3f)); +} diff --git a/tests/editor/test_scene_viewport_picker.cpp b/tests/editor/test_scene_viewport_picker.cpp new file mode 100644 index 00000000..f68a5450 --- /dev/null +++ b/tests/editor/test_scene_viewport_picker.cpp @@ -0,0 +1,182 @@ +#include + +#include "Viewport/SceneViewportPicker.h" + +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace { + +using XCEngine::Components::GameObject; +using XCEngine::Components::MeshFilterComponent; +using XCEngine::Components::MeshRendererComponent; +using XCEngine::Components::Scene; +using XCEngine::Editor::BuildSceneViewportRay; +using XCEngine::Editor::PickSceneViewportEntity; +using XCEngine::Editor::SceneViewportOverlayData; +using XCEngine::Editor::SceneViewportPickRequest; +using XCEngine::Math::Quaternion; +using XCEngine::Math::Ray; +using XCEngine::Math::Vector2; +using XCEngine::Math::Vector3; +using XCEngine::Resources::Mesh; +using XCEngine::Resources::MeshSection; +using XCEngine::Resources::StaticMeshVertex; + +std::unique_ptr CreateTriangleMesh(uint64_t guid) { + auto mesh = std::make_unique(); + + XCEngine::Resources::IResource::ConstructParams params = {}; + params.name = "TestTriangle"; + params.path = "memory://test_triangle"; + params.guid = XCEngine::Resources::ResourceGUID(guid); + mesh->Initialize(params); + + const StaticMeshVertex vertices[] = { + { Vector3(-1.0f, -1.0f, 0.0f) }, + { Vector3(1.0f, -1.0f, 0.0f) }, + { Vector3(0.0f, 1.0f, 0.0f) } + }; + const uint16_t indices[] = { 0, 1, 2 }; + + mesh->SetVertexData( + vertices, + sizeof(vertices), + static_cast(std::size(vertices)), + sizeof(StaticMeshVertex), + XCEngine::Resources::VertexAttribute::Position); + mesh->SetIndexData(indices, sizeof(indices), static_cast(std::size(indices)), false); + mesh->SetBounds(XCEngine::Math::Bounds(Vector3(0.0f, 0.0f, 0.0f), Vector3(2.0f, 2.0f, 0.0f))); + + MeshSection section = {}; + section.baseVertex = 0; + section.vertexCount = static_cast(std::size(vertices)); + section.startIndex = 0; + section.indexCount = static_cast(std::size(indices)); + section.materialID = 0; + section.bounds = mesh->GetBounds(); + mesh->AddSection(section); + return mesh; +} + +class SceneViewportPickerTest : public ::testing::Test { +protected: + SceneViewportOverlayData CreateDefaultOverlay() const { + SceneViewportOverlayData overlay = {}; + overlay.valid = true; + overlay.cameraPosition = Vector3::Zero(); + overlay.cameraForward = Vector3::Forward(); + overlay.cameraRight = Vector3::Right(); + overlay.cameraUp = Vector3::Up(); + overlay.verticalFovDegrees = 60.0f; + overlay.nearClipPlane = 0.03f; + overlay.farClipPlane = 2000.0f; + return overlay; + } + + GameObject* CreateTriangleObject( + Scene& scene, + const std::string& name, + const Vector3& position, + const Quaternion& rotation = Quaternion::Identity(), + const Vector3& scale = Vector3::One()) { + std::unique_ptr mesh = CreateTriangleMesh(++m_nextMeshGuid); + Mesh* meshPtr = mesh.get(); + m_meshes.push_back(std::move(mesh)); + + GameObject* object = scene.CreateGameObject(name); + object->GetTransform()->SetPosition(position); + object->GetTransform()->SetRotation(rotation); + object->GetTransform()->SetScale(scale); + + auto* meshFilter = object->AddComponent(); + auto* meshRenderer = object->AddComponent(); + meshFilter->SetMesh(meshPtr); + meshRenderer->SetEnabled(true); + return object; + } + +private: + uint64_t m_nextMeshGuid = 1; + std::vector> m_meshes; +}; + +TEST_F(SceneViewportPickerTest, BuildSceneViewportRayPointsThroughViewportCenter) { + Ray ray; + ASSERT_TRUE(BuildSceneViewportRay( + CreateDefaultOverlay(), + Vector2(1280.0f, 720.0f), + Vector2(640.0f, 360.0f), + ray)); + + EXPECT_NEAR(ray.origin.x, 0.0f, 1e-4f); + EXPECT_NEAR(ray.origin.y, 0.0f, 1e-4f); + EXPECT_NEAR(ray.origin.z, 0.0f, 1e-4f); + EXPECT_NEAR(ray.direction.x, 0.0f, 1e-4f); + EXPECT_NEAR(ray.direction.y, 0.0f, 1e-4f); + EXPECT_NEAR(ray.direction.z, 1.0f, 1e-4f); +} + +TEST_F(SceneViewportPickerTest, PickSceneViewportEntityReturnsNearestHit) { + Scene scene("PickerScene"); + GameObject* farObject = CreateTriangleObject(scene, "Far", Vector3(0.0f, 0.0f, 6.0f)); + GameObject* nearObject = CreateTriangleObject(scene, "Near", Vector3(0.0f, 0.0f, 3.0f)); + + SceneViewportPickRequest request = {}; + request.scene = &scene; + request.overlay = CreateDefaultOverlay(); + request.viewportSize = Vector2(1280.0f, 720.0f); + request.viewportPosition = Vector2(640.0f, 360.0f); + + const auto result = PickSceneViewportEntity(request); + + EXPECT_TRUE(result.hit); + EXPECT_EQ(result.entityId, nearObject->GetID()); + EXPECT_LT(result.distanceSq, (farObject->GetTransform()->GetPosition() - request.overlay.cameraPosition).SqrMagnitude()); +} + +TEST_F(SceneViewportPickerTest, PickSceneViewportEntityHandlesRotatedAndScaledObjects) { + Scene scene("PickerScene"); + GameObject* object = CreateTriangleObject( + scene, + "Rotated", + Vector3(0.0f, 0.0f, 4.0f), + Quaternion::FromEulerAngles(0.0f, 45.0f * XCEngine::Math::DEG_TO_RAD, 0.0f), + Vector3(2.0f, 2.0f, 2.0f)); + + SceneViewportPickRequest request = {}; + request.scene = &scene; + request.overlay = CreateDefaultOverlay(); + request.viewportSize = Vector2(1280.0f, 720.0f); + request.viewportPosition = Vector2(640.0f, 360.0f); + + const auto result = PickSceneViewportEntity(request); + + EXPECT_TRUE(result.hit); + EXPECT_EQ(result.entityId, object->GetID()); +} + +TEST_F(SceneViewportPickerTest, PickSceneViewportEntityReturnsNoHitForEmptySpace) { + Scene scene("PickerScene"); + CreateTriangleObject(scene, "Offset", Vector3(3.0f, 0.0f, 4.0f)); + + SceneViewportPickRequest request = {}; + request.scene = &scene; + request.overlay = CreateDefaultOverlay(); + request.viewportSize = Vector2(1280.0f, 720.0f); + request.viewportPosition = Vector2(640.0f, 360.0f); + + const auto result = PickSceneViewportEntity(request); + + EXPECT_FALSE(result.hit); + EXPECT_EQ(result.entityId, 0u); +} + +} // namespace diff --git a/tests/editor/test_scene_viewport_selection_utils.cpp b/tests/editor/test_scene_viewport_selection_utils.cpp new file mode 100644 index 00000000..d3ce626d --- /dev/null +++ b/tests/editor/test_scene_viewport_selection_utils.cpp @@ -0,0 +1,241 @@ +#include + +#include "Viewport/SceneViewportMath.h" +#include "Viewport/SceneViewportSelectionUtils.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace { + +using XCEngine::Components::GameObject; +using XCEngine::Components::CameraComponent; +using XCEngine::Components::CameraProjectionType; +using XCEngine::Components::MeshFilterComponent; +using XCEngine::Components::MeshRendererComponent; +using XCEngine::Components::Scene; +using XCEngine::Editor::BuildSceneViewportCameraData; +using XCEngine::Editor::BuildSceneViewportProjectionMatrix; +using XCEngine::Editor::BuildSceneViewportViewMatrix; +using XCEngine::Editor::CollectSceneViewportSelectionRenderables; +using XCEngine::Editor::SceneViewportOverlayData; +using XCEngine::Math::Matrix4x4; +using XCEngine::Math::Vector3; +using XCEngine::Resources::Mesh; +using XCEngine::Resources::MeshSection; +using XCEngine::Resources::StaticMeshVertex; + +bool NearlyEqual(float lhs, float rhs, float epsilon = 1e-4f) { + return std::abs(lhs - rhs) <= epsilon; +} + +bool NearlyEqual(const Matrix4x4& lhs, const Matrix4x4& rhs, float epsilon = 1e-4f) { + for (int row = 0; row < 4; ++row) { + for (int column = 0; column < 4; ++column) { + if (!NearlyEqual(lhs.m[row][column], rhs.m[row][column], epsilon)) { + return false; + } + } + } + + return true; +} + +std::unique_ptr CreateSelectionMesh(uint64_t guid, bool splitIntoTwoSections) { + auto mesh = std::make_unique(); + + XCEngine::Resources::IResource::ConstructParams params = {}; + params.name = "SelectionMesh"; + params.path = "memory://selection_mesh"; + params.guid = XCEngine::Resources::ResourceGUID(guid); + mesh->Initialize(params); + + const StaticMeshVertex vertices[] = { + { Vector3(-1.0f, -1.0f, 0.0f) }, + { Vector3(1.0f, -1.0f, 0.0f) }, + { Vector3(0.0f, 1.0f, 0.0f) }, + { Vector3(-1.0f, -1.0f, 0.0f) }, + { Vector3(0.0f, 1.0f, 0.0f) }, + { Vector3(-2.0f, 1.0f, 0.0f) } + }; + const uint16_t indices[] = { 0, 1, 2, 3, 4, 5 }; + + mesh->SetVertexData( + vertices, + sizeof(vertices), + static_cast(std::size(vertices)), + sizeof(StaticMeshVertex), + XCEngine::Resources::VertexAttribute::Position); + mesh->SetIndexData(indices, sizeof(indices), static_cast(std::size(indices)), false); + mesh->SetBounds(XCEngine::Math::Bounds(Vector3(-0.5f, 0.0f, 0.0f), Vector3(3.0f, 2.0f, 0.0f))); + + MeshSection firstSection = {}; + firstSection.baseVertex = 0; + firstSection.vertexCount = 3; + firstSection.startIndex = 0; + firstSection.indexCount = 3; + firstSection.materialID = 0; + firstSection.bounds = mesh->GetBounds(); + mesh->AddSection(firstSection); + + if (splitIntoTwoSections) { + MeshSection secondSection = {}; + secondSection.baseVertex = 3; + secondSection.vertexCount = 3; + secondSection.startIndex = 3; + secondSection.indexCount = 3; + secondSection.materialID = 1; + secondSection.bounds = mesh->GetBounds(); + mesh->AddSection(secondSection); + } + + return mesh; +} + +class SceneViewportSelectionUtilsTest : public ::testing::Test { +protected: + SceneViewportOverlayData CreateOverlay() const { + SceneViewportOverlayData overlay = {}; + overlay.valid = true; + overlay.cameraPosition = Vector3(2.0f, 3.0f, -8.0f); + overlay.cameraForward = Vector3::Forward(); + overlay.cameraRight = Vector3::Right(); + overlay.cameraUp = Vector3::Up(); + overlay.verticalFovDegrees = 60.0f; + overlay.nearClipPlane = 0.03f; + overlay.farClipPlane = 2000.0f; + return overlay; + } + + GameObject* CreateMeshObject( + Scene& scene, + const std::string& name, + const Vector3& position, + bool enabled = true, + bool splitIntoTwoSections = false) { + std::unique_ptr mesh = CreateSelectionMesh(++m_nextMeshGuid, splitIntoTwoSections); + Mesh* meshPtr = mesh.get(); + m_meshes.push_back(std::move(mesh)); + + GameObject* object = scene.CreateGameObject(name); + object->GetTransform()->SetPosition(position); + + auto* meshFilter = object->AddComponent(); + auto* meshRenderer = object->AddComponent(); + meshFilter->SetMesh(meshPtr); + meshFilter->SetEnabled(enabled); + meshRenderer->SetEnabled(enabled); + return object; + } + +private: + uint64_t m_nextMeshGuid = 1; + std::vector> m_meshes; +}; + +TEST_F(SceneViewportSelectionUtilsTest, BuildSceneViewportCameraDataMatchesViewportMathConvention) { + const SceneViewportOverlayData overlay = CreateOverlay(); + const auto cameraData = BuildSceneViewportCameraData(overlay, 1280, 720); + + EXPECT_TRUE(NearlyEqual( + cameraData.view.Transpose(), + BuildSceneViewportViewMatrix(overlay))); + EXPECT_TRUE(NearlyEqual( + cameraData.projection.Transpose(), + BuildSceneViewportProjectionMatrix(overlay, 1280.0f, 720.0f))); + EXPECT_EQ(cameraData.viewportWidth, 1280u); + EXPECT_EQ(cameraData.viewportHeight, 720u); + EXPECT_EQ(cameraData.worldPosition, overlay.cameraPosition); +} + +TEST_F(SceneViewportSelectionUtilsTest, BuildSceneViewportCameraDataFromCameraMatchesRendererConvention) { + Scene scene("SelectionUtilsScene"); + GameObject* cameraObject = scene.CreateGameObject("Camera"); + auto* camera = cameraObject->AddComponent(); + ASSERT_NE(camera, nullptr); + + cameraObject->GetTransform()->SetPosition(Vector3(2.0f, 3.0f, -8.0f)); + cameraObject->GetTransform()->SetRotation(XCEngine::Math::Quaternion::FromEulerAngles( + 12.0f * XCEngine::Math::DEG_TO_RAD, + -37.0f * XCEngine::Math::DEG_TO_RAD, + 5.0f * XCEngine::Math::DEG_TO_RAD)); + camera->SetProjectionType(CameraProjectionType::Perspective); + camera->SetFieldOfView(53.0f); + camera->SetNearClipPlane(0.03f); + camera->SetFarClipPlane(1500.0f); + + const auto cameraData = BuildSceneViewportCameraData(*camera, 1280, 720); + + EXPECT_TRUE(NearlyEqual( + cameraData.view.Transpose(), + cameraObject->GetTransform()->GetWorldToLocalMatrix())); + EXPECT_TRUE(NearlyEqual( + cameraData.projection.Transpose(), + Matrix4x4::Perspective( + 53.0f * XCEngine::Math::DEG_TO_RAD, + 1280.0f / 720.0f, + 0.03f, + 1500.0f))); + EXPECT_EQ(cameraData.worldPosition, cameraObject->GetTransform()->GetPosition()); +} + +TEST_F(SceneViewportSelectionUtilsTest, CollectSceneViewportSelectionRenderablesFiltersInvalidSelectionTargets) { + Scene scene("SelectionUtilsScene"); + + GameObject* validObject = CreateMeshObject(scene, "Valid", Vector3(0.0f, 0.0f, 4.0f)); + GameObject* disabledObject = CreateMeshObject(scene, "Disabled", Vector3(2.0f, 0.0f, 4.0f), false); + GameObject* inactiveObject = CreateMeshObject(scene, "Inactive", Vector3(-2.0f, 0.0f, 4.0f)); + inactiveObject->SetActive(false); + + const std::vector selection = { + 0u, + validObject->GetID(), + disabledObject->GetID(), + inactiveObject->GetID(), + validObject->GetID(), + 987654321u + }; + + const auto renderables = CollectSceneViewportSelectionRenderables( + scene, + selection, + CreateOverlay().cameraPosition); + + ASSERT_EQ(renderables.size(), 1u); + EXPECT_EQ(renderables[0].gameObject, validObject); + EXPECT_EQ(renderables[0].meshRenderer, validObject->GetComponent()); + EXPECT_TRUE(renderables[0].hasSection); +} + +TEST_F(SceneViewportSelectionUtilsTest, CollectSceneViewportSelectionRenderablesExpandsMeshSections) { + Scene scene("SelectionUtilsScene"); + GameObject* object = CreateMeshObject( + scene, + "Sectioned", + Vector3(1.0f, 0.0f, 6.0f), + true, + true); + + const auto renderables = CollectSceneViewportSelectionRenderables( + scene, + { object->GetID() }, + CreateOverlay().cameraPosition); + + ASSERT_EQ(renderables.size(), 2u); + EXPECT_EQ(renderables[0].gameObject, object); + EXPECT_EQ(renderables[0].sectionIndex, 0u); + EXPECT_EQ(renderables[0].materialIndex, 0u); + EXPECT_EQ(renderables[1].sectionIndex, 1u); + EXPECT_EQ(renderables[1].materialIndex, 1u); +} + +} // namespace