Refine editor viewport and interaction workflow
This commit is contained in:
@@ -1,9 +1,14 @@
|
|||||||
cmake_minimum_required(VERSION 3.15)
|
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 17)
|
||||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
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(-DUNICODE -D_UNICODE)
|
||||||
add_definitions(-DIMGUI_ENABLE_DOCKING)
|
add_definitions(-DIMGUI_ENABLE_DOCKING)
|
||||||
|
|
||||||
@@ -18,6 +23,37 @@ FetchContent_Declare(
|
|||||||
|
|
||||||
FetchContent_MakeAvailable(imgui)
|
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
|
set(IMGUI_SOURCES
|
||||||
${imgui_SOURCE_DIR}/imgui.cpp
|
${imgui_SOURCE_DIR}/imgui.cpp
|
||||||
${imgui_SOURCE_DIR}/imgui_demo.cpp
|
${imgui_SOURCE_DIR}/imgui_demo.cpp
|
||||||
@@ -42,6 +78,12 @@ add_executable(${PROJECT_NAME} WIN32
|
|||||||
src/panels/MenuBar.cpp
|
src/panels/MenuBar.cpp
|
||||||
src/panels/HierarchyPanel.cpp
|
src/panels/HierarchyPanel.cpp
|
||||||
src/panels/SceneViewPanel.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/GameViewPanel.cpp
|
||||||
src/panels/InspectorPanel.cpp
|
src/panels/InspectorPanel.cpp
|
||||||
src/panels/ConsolePanel.cpp
|
src/panels/ConsolePanel.cpp
|
||||||
@@ -69,6 +111,7 @@ endif()
|
|||||||
target_link_libraries(${PROJECT_NAME} PRIVATE
|
target_link_libraries(${PROJECT_NAME} PRIVATE
|
||||||
XCEngine
|
XCEngine
|
||||||
d3d12.lib
|
d3d12.lib
|
||||||
|
Dbghelp.lib
|
||||||
dxgi.lib
|
dxgi.lib
|
||||||
d3dcompiler.lib
|
d3dcompiler.lib
|
||||||
Ole32.lib
|
Ole32.lib
|
||||||
@@ -83,6 +126,6 @@ set_target_properties(${PROJECT_NAME} PROPERTIES
|
|||||||
|
|
||||||
add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD
|
add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD
|
||||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||||
${CMAKE_SOURCE_DIR}/engine/third_party/assimp/bin/assimp-vc143-mt.dll
|
${XCENGINE_ASSIMP_DLL}
|
||||||
$<TARGET_FILE_DIR:${PROJECT_NAME}>/assimp-vc143-mt.dll
|
$<TARGET_FILE_DIR:${PROJECT_NAME}>/assimp-vc143-mt.dll
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -7,10 +7,21 @@
|
|||||||
#include "Core/IEditorContext.h"
|
#include "Core/IEditorContext.h"
|
||||||
#include "UI/PopupState.h"
|
#include "UI/PopupState.h"
|
||||||
|
|
||||||
|
#include <XCEngine/Debug/Logger.h>
|
||||||
|
|
||||||
|
#include <imgui.h>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
namespace XCEngine {
|
namespace XCEngine {
|
||||||
namespace Editor {
|
namespace Editor {
|
||||||
namespace Actions {
|
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() {
|
inline constexpr const char* HierarchyEntityPayloadType() {
|
||||||
return "ENTITY_PTR";
|
return "ENTITY_PTR";
|
||||||
}
|
}
|
||||||
@@ -60,11 +71,64 @@ inline void RequestHierarchyOptionsPopup(UI::DeferredPopupState& optionsPopup) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
inline void RequestHierarchyBackgroundContextPopup(UI::DeferredPopupState& backgroundContextMenu) {
|
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();
|
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 <size_t BufferCapacity>
|
||||||
|
inline void DrawHierarchyBackgroundInteraction(
|
||||||
|
IEditorContext& context,
|
||||||
|
const UI::InlineTextEditState<uint64_t, BufferCapacity>& 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 <typename SortMode, typename SetSortModeFn>
|
template <typename SortMode, typename SetSortModeFn>
|
||||||
inline void DrawHierarchySortOptionsPopup(
|
inline void DrawHierarchySortOptionsPopup(
|
||||||
UI::DeferredPopupState& optionsPopup,
|
UI::DeferredPopupState& optionsPopup,
|
||||||
@@ -103,6 +167,144 @@ inline void DrawHierarchySortOptionsPopup(
|
|||||||
UI::EndPopup();
|
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) {
|
inline bool BeginHierarchyEntityDrag(::XCEngine::Components::GameObject* gameObject) {
|
||||||
if (!gameObject || !ImGui::BeginDragDropSource(ImGuiDragDropFlags_None)) {
|
if (!gameObject || !ImGui::BeginDragDropSource(ImGuiDragDropFlags_None)) {
|
||||||
return false;
|
return false;
|
||||||
@@ -131,124 +333,6 @@ inline bool AcceptHierarchyEntityDrop(IEditorContext& context, ::XCEngine::Compo
|
|||||||
return accepted;
|
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 Actions
|
||||||
} // namespace Editor
|
} // namespace Editor
|
||||||
} // namespace XCEngine
|
} // namespace XCEngine
|
||||||
|
|||||||
@@ -90,17 +90,6 @@ inline void DrawInspectorAddComponentPopup(
|
|||||||
UI::EndTitledPopup();
|
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) {
|
inline void FinalizeInspectorInteractiveChangeIfIdle(IEditorContext& context) {
|
||||||
if (context.GetUndoManager().HasPendingInteractiveChange() && !ImGui::IsAnyItemActive()) {
|
if (context.GetUndoManager().HasPendingInteractiveChange() && !ImGui::IsAnyItemActive()) {
|
||||||
context.GetUndoManager().FinalizeInteractiveChange();
|
context.GetUndoManager().FinalizeInteractiveChange();
|
||||||
|
|||||||
@@ -15,8 +15,6 @@ inline constexpr const char* ProjectAssetPayloadType() {
|
|||||||
return "ASSET_ITEM";
|
return "ASSET_ITEM";
|
||||||
}
|
}
|
||||||
|
|
||||||
inline void DrawProjectAssetContextActions(IEditorContext& context, const AssetItemPtr& item);
|
|
||||||
|
|
||||||
inline const char* GetDraggedProjectAssetPath() {
|
inline const char* GetDraggedProjectAssetPath() {
|
||||||
const ImGuiPayload* payload = ImGui::GetDragDropPayload();
|
const ImGuiPayload* payload = ImGui::GetDragDropPayload();
|
||||||
if (!payload || !payload->IsDataType(ProjectAssetPayloadType())) {
|
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) {
|
inline void RequestProjectEmptyContextPopup(UI::DeferredPopupState& emptyContextMenu) {
|
||||||
if (ImGui::IsWindowHovered() && ImGui::IsMouseClicked(1) && !ImGui::IsAnyItemHovered()) {
|
if (ImGui::IsWindowHovered() && ImGui::IsMouseClicked(1) && !ImGui::IsAnyItemHovered()) {
|
||||||
emptyContextMenu.RequestOpen();
|
emptyContextMenu.RequestOpen();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inline void HandleProjectItemSelection(IProjectManager& projectManager, const AssetItemPtr& item) {
|
|
||||||
projectManager.SetSelectedItem(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void HandleProjectItemContextRequest(
|
inline void HandleProjectItemContextRequest(
|
||||||
IProjectManager& projectManager,
|
IProjectManager& projectManager,
|
||||||
const AssetItemPtr& item,
|
const AssetItemPtr& item,
|
||||||
@@ -89,50 +87,6 @@ inline void HandleProjectItemContextRequest(
|
|||||||
itemContextMenu.RequestOpen(item);
|
itemContextMenu.RequestOpen(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
inline void DrawProjectItemContextPopup(IEditorContext& context, UI::TargetedPopupState<AssetItemPtr>& 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 <typename CreateFolderFn>
|
|
||||||
inline void DrawProjectEmptyContextPopup(
|
|
||||||
UI::DeferredPopupState& emptyContextMenu,
|
|
||||||
CreateFolderFn&& createFolder) {
|
|
||||||
emptyContextMenu.ConsumeOpenRequest("EmptyContextMenu");
|
|
||||||
|
|
||||||
if (!UI::BeginPopup("EmptyContextMenu")) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
DrawMenuAction(MakeCreateFolderAction(), [&]() {
|
|
||||||
createFolder();
|
|
||||||
});
|
|
||||||
|
|
||||||
UI::EndPopup();
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace Actions
|
} // namespace Actions
|
||||||
} // namespace Editor
|
} // namespace Editor
|
||||||
} // namespace XCEngine
|
} // namespace XCEngine
|
||||||
|
|||||||
@@ -135,14 +135,19 @@ bool Application::Initialize(HWND hwnd) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
m_hwnd = hwnd;
|
m_hwnd = hwnd;
|
||||||
|
logger.Info(Debug::LogCategory::General, "Initializing editor window renderer...");
|
||||||
|
|
||||||
if (!InitializeWindowRenderer(hwnd)) {
|
if (!InitializeWindowRenderer(hwnd)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.Info(Debug::LogCategory::General, "Initializing editor context...");
|
||||||
InitializeEditorContext(projectRoot);
|
InitializeEditorContext(projectRoot);
|
||||||
|
logger.Info(Debug::LogCategory::General, "Initializing ImGui backend...");
|
||||||
InitializeImGui(hwnd);
|
InitializeImGui(hwnd);
|
||||||
|
logger.Info(Debug::LogCategory::General, "Attaching editor layer...");
|
||||||
AttachEditorLayer();
|
AttachEditorLayer();
|
||||||
|
logger.Info(Debug::LogCategory::General, "Editor initialization completed.");
|
||||||
m_renderReady = true;
|
m_renderReady = true;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#include "Platform/Win32Utf8.h"
|
#include "Platform/Win32Utf8.h"
|
||||||
|
|
||||||
|
#include <dbghelp.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
@@ -14,6 +15,49 @@ inline std::string GetExecutableLogPath(const char* fileName) {
|
|||||||
return GetExecutableDirectoryUtf8() + "\\" + 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<SYMBOL_INFO*>(symbolStorage);
|
||||||
|
symbol->SizeOfStruct = sizeof(SYMBOL_INFO);
|
||||||
|
symbol->MaxNameLen = MAX_SYM_NAME;
|
||||||
|
|
||||||
|
for (USHORT frameIndex = 0; frameIndex < frameCount; ++frameIndex) {
|
||||||
|
const DWORD64 address = reinterpret_cast<DWORD64>(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<unsigned long long>(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) {
|
inline LONG WINAPI CrashExceptionFilter(EXCEPTION_POINTERS* exceptionPointers) {
|
||||||
const std::string logPath = GetExecutableLogPath("crash.log");
|
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",
|
"[CRASH] ExceptionCode=0x%08X, Address=0x%p\n",
|
||||||
exceptionPointers->ExceptionRecord->ExceptionCode,
|
exceptionPointers->ExceptionRecord->ExceptionCode,
|
||||||
exceptionPointers->ExceptionRecord->ExceptionAddress);
|
exceptionPointers->ExceptionRecord->ExceptionAddress);
|
||||||
|
WriteCrashStackTrace(file);
|
||||||
fclose(file);
|
fclose(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
123
editor/src/UI/ContextMenu.h
Normal file
123
editor/src/UI/ContextMenu.h
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "Core.h"
|
||||||
|
#include "StyleTokens.h"
|
||||||
|
#include "Widgets.h"
|
||||||
|
|
||||||
|
#include <XCEngine/Debug/Logger.h>
|
||||||
|
|
||||||
|
#include <imgui.h>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
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 <typename DrawContentFn>
|
||||||
|
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
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
#include "DividerChrome.h"
|
#include "DividerChrome.h"
|
||||||
#include "StyleTokens.h"
|
#include "StyleTokens.h"
|
||||||
|
|
||||||
|
#include <cmath>
|
||||||
#include <imgui.h>
|
#include <imgui.h>
|
||||||
|
|
||||||
namespace XCEngine {
|
namespace XCEngine {
|
||||||
@@ -38,27 +39,41 @@ inline void DrawDisclosureArrow(ImDrawList* drawList, const ImVec2& min, const I
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ImVec2 center((min.x + max.x) * 0.5f, (min.y + max.y) * 0.5f);
|
const ImVec2 center(
|
||||||
|
static_cast<float>(std::floor((min.x + max.x) * 0.5f)) + 0.5f,
|
||||||
|
static_cast<float>(std::floor((min.y + max.y) * 0.5f)) + 0.5f);
|
||||||
const float width = max.x - min.x;
|
const float width = max.x - min.x;
|
||||||
const float height = max.y - min.y;
|
const float height = max.y - min.y;
|
||||||
const float size = (width < height ? width : height) * DisclosureArrowScale();
|
const float radius = (std::max)(
|
||||||
if (size <= 0.0f) {
|
3.0f,
|
||||||
|
static_cast<float>(std::floor((width < height ? width : height) * DisclosureArrowScale())));
|
||||||
|
const float baseHalfExtent = radius;
|
||||||
|
const float tipExtent = (std::max)(2.0f, static_cast<float>(std::floor(radius * 0.70f)));
|
||||||
|
if (baseHalfExtent < 1.0f || tipExtent < 1.0f) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (open) {
|
ImVec2 points[3] = {
|
||||||
drawList->AddTriangleFilled(
|
ImVec2(-baseHalfExtent, -tipExtent),
|
||||||
ImVec2(center.x - size, center.y - size * 0.45f),
|
ImVec2(baseHalfExtent, -tipExtent),
|
||||||
ImVec2(center.x + size, center.y - size * 0.45f),
|
ImVec2(0.0f, tipExtent)
|
||||||
ImVec2(center.x, center.y + size),
|
};
|
||||||
color);
|
|
||||||
return;
|
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(
|
drawList->AddTriangleFilled(
|
||||||
ImVec2(center.x - size * 0.45f, center.y - size),
|
points[0],
|
||||||
ImVec2(center.x - size * 0.45f, center.y + size),
|
points[1],
|
||||||
ImVec2(center.x + size, center.y),
|
points[2],
|
||||||
color);
|
color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -120,7 +120,6 @@ private:
|
|||||||
}
|
}
|
||||||
|
|
||||||
io.FontDefault = uiFont;
|
io.FontDefault = uiFont;
|
||||||
atlas->Build();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string m_iniPath;
|
std::string m_iniPath;
|
||||||
|
|||||||
34
editor/src/UI/MenuCommand.h
Normal file
34
editor/src/UI/MenuCommand.h
Normal file
@@ -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
|
||||||
@@ -6,6 +6,8 @@ namespace XCEngine {
|
|||||||
namespace Editor {
|
namespace Editor {
|
||||||
namespace UI {
|
namespace UI {
|
||||||
|
|
||||||
|
inline float PopupWindowBorderSize();
|
||||||
|
|
||||||
inline ImVec2 DockHostFramePadding() {
|
inline ImVec2 DockHostFramePadding() {
|
||||||
return ImVec2(4.0f, 2.0f);
|
return ImVec2(4.0f, 2.0f);
|
||||||
}
|
}
|
||||||
@@ -246,6 +248,10 @@ inline float CompactNavigationTreeIndentSpacing() {
|
|||||||
return 14.0f;
|
return 14.0f;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inline float HierarchyTreeIndentSpacing() {
|
||||||
|
return 18.0f;
|
||||||
|
}
|
||||||
|
|
||||||
inline float NavigationTreeIconSize() {
|
inline float NavigationTreeIconSize() {
|
||||||
return 18.0f;
|
return 18.0f;
|
||||||
}
|
}
|
||||||
@@ -263,7 +269,7 @@ inline float NavigationTreePrefixLabelGap() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
inline float DisclosureArrowScale() {
|
inline float DisclosureArrowScale() {
|
||||||
return 0.14f;
|
return 0.28f;
|
||||||
}
|
}
|
||||||
|
|
||||||
inline ImVec4 NavigationTreePrefixColor(bool selected = false, bool hovered = false) {
|
inline ImVec4 NavigationTreePrefixColor(bool selected = false, bool hovered = false) {
|
||||||
@@ -295,7 +301,9 @@ inline TreeViewStyle NavigationTreeStyle() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
inline TreeViewStyle HierarchyTreeStyle() {
|
inline TreeViewStyle HierarchyTreeStyle() {
|
||||||
return NavigationTreeStyle();
|
TreeViewStyle style = NavigationTreeStyle();
|
||||||
|
style.indentSpacing = HierarchyTreeIndentSpacing();
|
||||||
|
return style;
|
||||||
}
|
}
|
||||||
|
|
||||||
inline TreeViewStyle ProjectFolderTreeStyle() {
|
inline TreeViewStyle ProjectFolderTreeStyle() {
|
||||||
@@ -338,6 +346,18 @@ inline float SearchFieldFrameRounding() {
|
|||||||
return 2.0f;
|
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() {
|
inline ImU32 PanelDividerColor() {
|
||||||
return ImGui::GetColorU32(PanelSplitterIdleColor());
|
return ImGui::GetColorU32(PanelSplitterIdleColor());
|
||||||
}
|
}
|
||||||
@@ -476,6 +496,18 @@ inline float PopupFrameRounding() {
|
|||||||
return 3.0f;
|
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() {
|
inline float PopupSubmenuArrowExtent() {
|
||||||
return 8.0f;
|
return 8.0f;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,10 +5,12 @@
|
|||||||
#include "BuiltInIcons.h"
|
#include "BuiltInIcons.h"
|
||||||
#include "ConsoleFilterState.h"
|
#include "ConsoleFilterState.h"
|
||||||
#include "ConsoleLogFormatter.h"
|
#include "ConsoleLogFormatter.h"
|
||||||
|
#include "ContextMenu.h"
|
||||||
#include "Core.h"
|
#include "Core.h"
|
||||||
#include "DockHostStyle.h"
|
#include "DockHostStyle.h"
|
||||||
#include "DockTabBarChrome.h"
|
#include "DockTabBarChrome.h"
|
||||||
#include "DividerChrome.h"
|
#include "DividerChrome.h"
|
||||||
|
#include "MenuCommand.h"
|
||||||
#include "PanelChrome.h"
|
#include "PanelChrome.h"
|
||||||
#include "PopupState.h"
|
#include "PopupState.h"
|
||||||
#include "PropertyLayout.h"
|
#include "PropertyLayout.h"
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "Core.h"
|
#include "Core.h"
|
||||||
|
#include "MenuCommand.h"
|
||||||
#include "StyleTokens.h"
|
#include "StyleTokens.h"
|
||||||
|
|
||||||
|
#include <XCEngine/Debug/Logger.h>
|
||||||
|
|
||||||
#include <imgui.h>
|
#include <imgui.h>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
@@ -10,6 +13,16 @@ namespace XCEngine {
|
|||||||
namespace Editor {
|
namespace Editor {
|
||||||
namespace UI {
|
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 {
|
struct ComponentSectionResult {
|
||||||
bool open = false;
|
bool open = false;
|
||||||
float contentIndent = 0.0f;
|
float contentIndent = 0.0f;
|
||||||
@@ -17,7 +30,6 @@ struct ComponentSectionResult {
|
|||||||
|
|
||||||
struct AssetTileResult {
|
struct AssetTileResult {
|
||||||
bool clicked = false;
|
bool clicked = false;
|
||||||
bool contextRequested = false;
|
|
||||||
bool openRequested = false;
|
bool openRequested = false;
|
||||||
bool hovered = false;
|
bool hovered = false;
|
||||||
ImVec2 min = ImVec2(0.0f, 0.0f);
|
ImVec2 min = ImVec2(0.0f, 0.0f);
|
||||||
@@ -41,29 +53,11 @@ enum class DialogActionResult {
|
|||||||
Secondary
|
Secondary
|
||||||
};
|
};
|
||||||
|
|
||||||
enum class MenuCommandKind {
|
struct InlineRenameFieldResult {
|
||||||
Action,
|
bool submitted = false;
|
||||||
Separator
|
bool cancelRequested = false;
|
||||||
};
|
bool deactivated = false;
|
||||||
|
bool active = false;
|
||||||
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 };
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
template <typename DrawContentFn>
|
template <typename DrawContentFn>
|
||||||
@@ -101,11 +95,13 @@ inline bool DrawPopupSubmenuScope(const char* label, DrawContentFn&& drawContent
|
|||||||
popupOpen,
|
popupOpen,
|
||||||
ImGuiSelectableFlags_NoAutoClosePopups,
|
ImGuiSelectableFlags_NoAutoClosePopups,
|
||||||
ImVec2(rowWidth, rowHeight))) {
|
ImVec2(rowWidth, rowHeight))) {
|
||||||
|
TracePopupSubmenuIfNeeded(label, "Hierarchy create submenu selectable clicked -> OpenPopup");
|
||||||
ImGui::OpenPopup(popupId);
|
ImGui::OpenPopup(popupId);
|
||||||
}
|
}
|
||||||
|
|
||||||
const bool hovered = ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup);
|
const bool hovered = ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup);
|
||||||
if (hovered && !popupOpen) {
|
if (hovered && !popupOpen) {
|
||||||
|
TracePopupSubmenuIfNeeded(label, "Hierarchy create submenu hovered -> OpenPopup");
|
||||||
ImGui::OpenPopup(popupId);
|
ImGui::OpenPopup(popupId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,6 +134,15 @@ inline bool DrawPopupSubmenuScope(const char* label, DrawContentFn&& drawContent
|
|||||||
ImGuiWindowFlags_NoMove |
|
ImGuiWindowFlags_NoMove |
|
||||||
ImGuiWindowFlags_NoResize |
|
ImGuiWindowFlags_NoResize |
|
||||||
ImGuiWindowFlags_NoSavedSettings);
|
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) {
|
if (!open) {
|
||||||
ImGui::PopID();
|
ImGui::PopID();
|
||||||
return false;
|
return false;
|
||||||
@@ -146,6 +151,12 @@ inline bool DrawPopupSubmenuScope(const char* label, DrawContentFn&& drawContent
|
|||||||
drawContent();
|
drawContent();
|
||||||
const bool popupHovered = ImGui::IsWindowHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup);
|
const bool popupHovered = ImGui::IsWindowHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup);
|
||||||
if (!hovered && !popupHovered && !ImGui::IsWindowAppearing()) {
|
if (!hovered && !popupHovered && !ImGui::IsWindowAppearing()) {
|
||||||
|
TracePopupSubmenuIfNeeded(
|
||||||
|
label,
|
||||||
|
std::string("Hierarchy create submenu auto-close: rowHovered=") +
|
||||||
|
(hovered ? "1" : "0") +
|
||||||
|
", popupHovered=" +
|
||||||
|
(popupHovered ? "1" : "0"));
|
||||||
ImGui::CloseCurrentPopup();
|
ImGui::CloseCurrentPopup();
|
||||||
}
|
}
|
||||||
EndPopup();
|
EndPopup();
|
||||||
@@ -190,6 +201,50 @@ inline bool ToolbarSearchField(
|
|||||||
return changed;
|
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) {
|
inline void DrawToolbarLabel(const char* text) {
|
||||||
ImGui::AlignTextToFramePadding();
|
ImGui::AlignTextToFramePadding();
|
||||||
ImGui::TextColored(HintTextColor(), "%s", text);
|
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 padding = BreadcrumbSegmentPadding();
|
||||||
const ImVec2 textSize = ImGui::CalcTextSize(label);
|
const ImVec2 textSize = ImGui::CalcTextSize(label);
|
||||||
const ImVec2 size(textSize.x + padding.x * 2.0f, BreadcrumbItemHeight());
|
const ImVec2 size(textSize.x + padding.x * 2.0f, BreadcrumbItemHeight());
|
||||||
bool pressed = false;
|
ImGui::InvisibleButton("##BreadcrumbItem", size);
|
||||||
bool hovered = false;
|
|
||||||
DrawBreadcrumbTextItem(label, size, BreadcrumbSegmentTextColor(current, hovered), clickable, &pressed, &hovered);
|
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) {
|
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(
|
ImGui::GetWindowDrawList()->AddText(
|
||||||
ImVec2(textX, textY),
|
ImVec2(textX, textY),
|
||||||
ImGui::GetColorU32(BreadcrumbSegmentTextColor(current, true)),
|
ImGui::GetColorU32(BreadcrumbSegmentTextColor(current, true)),
|
||||||
@@ -301,7 +362,10 @@ inline void DrawToolbarBreadcrumbs(
|
|||||||
size_t segmentCount,
|
size_t segmentCount,
|
||||||
GetNameFn&& getName,
|
GetNameFn&& getName,
|
||||||
NavigateFn&& navigateToSegment) {
|
NavigateFn&& navigateToSegment) {
|
||||||
|
const float lineY = ImGui::GetCursorPosY();
|
||||||
|
|
||||||
if (segmentCount == 0) {
|
if (segmentCount == 0) {
|
||||||
|
ImGui::SetCursorPosY(lineY);
|
||||||
DrawBreadcrumbSegment(rootLabel, false, true);
|
DrawBreadcrumbSegment(rootLabel, false, true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -309,8 +373,10 @@ inline void DrawToolbarBreadcrumbs(
|
|||||||
for (size_t i = 0; i < segmentCount; ++i) {
|
for (size_t i = 0; i < segmentCount; ++i) {
|
||||||
if (i > 0) {
|
if (i > 0) {
|
||||||
ImGui::SameLine(0.0f, BreadcrumbSegmentSpacing());
|
ImGui::SameLine(0.0f, BreadcrumbSegmentSpacing());
|
||||||
|
ImGui::SetCursorPosY(lineY);
|
||||||
DrawBreadcrumbSeparator();
|
DrawBreadcrumbSeparator();
|
||||||
ImGui::SameLine(0.0f, BreadcrumbSegmentSpacing());
|
ImGui::SameLine(0.0f, BreadcrumbSegmentSpacing());
|
||||||
|
ImGui::SetCursorPosY(lineY);
|
||||||
}
|
}
|
||||||
|
|
||||||
const std::string label = (i == 0 && rootLabel && rootLabel[0] != '\0')
|
const std::string label = (i == 0 && rootLabel && rootLabel[0] != '\0')
|
||||||
@@ -318,6 +384,7 @@ inline void DrawToolbarBreadcrumbs(
|
|||||||
: getName(i);
|
: getName(i);
|
||||||
const bool current = (i + 1 == segmentCount);
|
const bool current = (i + 1 == segmentCount);
|
||||||
|
|
||||||
|
ImGui::SetCursorPosY(lineY);
|
||||||
ImGui::PushID(static_cast<int>(i));
|
ImGui::PushID(static_cast<int>(i));
|
||||||
if (DrawBreadcrumbSegment(label.c_str(), !current, current)) {
|
if (DrawBreadcrumbSegment(label.c_str(), !current, current)) {
|
||||||
navigateToSegment(i);
|
navigateToSegment(i);
|
||||||
@@ -346,7 +413,6 @@ inline AssetTileResult DrawAssetTile(
|
|||||||
ImGui::InvisibleButton("##AssetBtn", tileSize);
|
ImGui::InvisibleButton("##AssetBtn", tileSize);
|
||||||
|
|
||||||
const bool clicked = ImGui::IsItemClicked(ImGuiMouseButton_Left);
|
const bool clicked = ImGui::IsItemClicked(ImGuiMouseButton_Left);
|
||||||
const bool contextRequested = ImGui::IsItemClicked(ImGuiMouseButton_Right);
|
|
||||||
const bool openRequested = ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(0);
|
const bool openRequested = ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(0);
|
||||||
const bool hovered = ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem);
|
const bool hovered = ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem);
|
||||||
|
|
||||||
@@ -388,7 +454,7 @@ inline AssetTileResult DrawAssetTile(
|
|||||||
ImGui::PopClipRect();
|
ImGui::PopClipRect();
|
||||||
}
|
}
|
||||||
|
|
||||||
return AssetTileResult{ clicked, contextRequested, openRequested, hovered, min, max, labelMin, labelMax };
|
return AssetTileResult{ clicked, openRequested, hovered, min, max, labelMin, labelMax };
|
||||||
}
|
}
|
||||||
|
|
||||||
template <typename DrawMenuFn>
|
template <typename DrawMenuFn>
|
||||||
@@ -397,6 +463,7 @@ inline ComponentSectionResult BeginComponentSection(
|
|||||||
const char* label,
|
const char* label,
|
||||||
DrawMenuFn&& drawMenu,
|
DrawMenuFn&& drawMenu,
|
||||||
bool defaultOpen = true) {
|
bool defaultOpen = true) {
|
||||||
|
(void)drawMenu;
|
||||||
const ImGuiStyle& style = ImGui::GetStyle();
|
const ImGuiStyle& style = ImGui::GetStyle();
|
||||||
const ImVec2 framePadding = InspectorSectionFramePadding();
|
const ImVec2 framePadding = InspectorSectionFramePadding();
|
||||||
const float availableWidth = ImMax(ImGui::GetContentRegionAvail().x, 1.0f);
|
const float availableWidth = ImMax(ImGui::GetContentRegionAvail().x, 1.0f);
|
||||||
@@ -453,11 +520,6 @@ inline ComponentSectionResult BeginComponentSection(
|
|||||||
drawList->PopClipRect();
|
drawList->PopClipRect();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (BeginPopupContextItem("##ComponentSettings")) {
|
|
||||||
drawMenu();
|
|
||||||
EndPopup();
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui::PopID();
|
ImGui::PopID();
|
||||||
return ComponentSectionResult{ open, InspectorSectionContentIndent() };
|
return ComponentSectionResult{ open, InspectorSectionContentIndent() };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ struct SceneViewportInput {
|
|||||||
ImVec2 viewportSize = ImVec2(0.0f, 0.0f);
|
ImVec2 viewportSize = ImVec2(0.0f, 0.0f);
|
||||||
ImVec2 mouseDelta = ImVec2(0.0f, 0.0f);
|
ImVec2 mouseDelta = ImVec2(0.0f, 0.0f);
|
||||||
float mouseWheel = 0.0f;
|
float mouseWheel = 0.0f;
|
||||||
|
float flySpeedDelta = 0.0f;
|
||||||
float deltaTime = 0.0f;
|
float deltaTime = 0.0f;
|
||||||
float moveForward = 0.0f;
|
float moveForward = 0.0f;
|
||||||
float moveRight = 0.0f;
|
float moveRight = 0.0f;
|
||||||
@@ -65,6 +66,10 @@ public:
|
|||||||
virtual void BeginFrame() = 0;
|
virtual void BeginFrame() = 0;
|
||||||
virtual EditorViewportFrame RequestViewport(EditorViewportKind kind, const ImVec2& requestedSize) = 0;
|
virtual EditorViewportFrame RequestViewport(EditorViewportKind kind, const ImVec2& requestedSize) = 0;
|
||||||
virtual void UpdateSceneViewInput(IEditorContext& context, const SceneViewportInput& input) = 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 SceneViewportOverlayData GetSceneViewOverlayData() const = 0;
|
||||||
virtual void RenderRequestedViewports(
|
virtual void RenderRequestedViewports(
|
||||||
IEditorContext& context,
|
IEditorContext& context,
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ struct SceneViewportCameraInputState {
|
|||||||
float panDeltaX = 0.0f;
|
float panDeltaX = 0.0f;
|
||||||
float panDeltaY = 0.0f;
|
float panDeltaY = 0.0f;
|
||||||
float zoomDelta = 0.0f;
|
float zoomDelta = 0.0f;
|
||||||
|
float flySpeedDelta = 0.0f;
|
||||||
float deltaTime = 0.0f;
|
float deltaTime = 0.0f;
|
||||||
float moveForward = 0.0f;
|
float moveForward = 0.0f;
|
||||||
float moveRight = 0.0f;
|
float moveRight = 0.0f;
|
||||||
@@ -32,6 +33,7 @@ public:
|
|||||||
void Reset() {
|
void Reset() {
|
||||||
m_focalPoint = Math::Vector3::Zero();
|
m_focalPoint = Math::Vector3::Zero();
|
||||||
m_distance = 6.0f;
|
m_distance = 6.0f;
|
||||||
|
m_flySpeed = 5.0f;
|
||||||
m_yawDegrees = -35.0f;
|
m_yawDegrees = -35.0f;
|
||||||
m_pitchDegrees = -20.0f;
|
m_pitchDegrees = -20.0f;
|
||||||
UpdatePositionFromFocalPoint();
|
UpdatePositionFromFocalPoint();
|
||||||
@@ -45,6 +47,10 @@ public:
|
|||||||
return m_distance;
|
return m_distance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
float GetFlySpeed() const {
|
||||||
|
return m_flySpeed;
|
||||||
|
}
|
||||||
|
|
||||||
float GetYawDegrees() const {
|
float GetYawDegrees() const {
|
||||||
return m_yawDegrees;
|
return m_yawDegrees;
|
||||||
}
|
}
|
||||||
@@ -95,22 +101,27 @@ public:
|
|||||||
const Math::Vector3 up = GetUp();
|
const Math::Vector3 up = GetUp();
|
||||||
const float worldUnitsPerPixel = ComputeWorldUnitsPerPixel(input.viewportHeight);
|
const float worldUnitsPerPixel = ComputeWorldUnitsPerPixel(input.viewportHeight);
|
||||||
const Math::Vector3 delta =
|
const Math::Vector3 delta =
|
||||||
((right * input.panDeltaX) + (up * -input.panDeltaY)) * worldUnitsPerPixel;
|
((right * -input.panDeltaX) + (up * input.panDeltaY)) * worldUnitsPerPixel;
|
||||||
m_focalPoint += delta;
|
m_focalPoint += delta;
|
||||||
m_position += 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 &&
|
if (input.deltaTime > 0.0f &&
|
||||||
(std::abs(input.moveForward) > Math::EPSILON ||
|
(std::abs(input.moveForward) > Math::EPSILON ||
|
||||||
std::abs(input.moveRight) > Math::EPSILON ||
|
std::abs(input.moveRight) > Math::EPSILON ||
|
||||||
std::abs(input.moveUp) > Math::EPSILON)) {
|
std::abs(input.moveUp) > Math::EPSILON)) {
|
||||||
const Math::Vector3 movement =
|
const Math::Vector3 movement =
|
||||||
GetForward() * -input.moveForward +
|
GetForward() * input.moveForward +
|
||||||
GetRight() * -input.moveRight +
|
GetRight() * input.moveRight +
|
||||||
Math::Vector3::Up() * input.moveUp;
|
Math::Vector3::Up() * input.moveUp;
|
||||||
if (movement.SqrMagnitude() > Math::EPSILON) {
|
if (movement.SqrMagnitude() > Math::EPSILON) {
|
||||||
const float speedMultiplier = input.fastMove ? 4.0f : 1.0f;
|
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;
|
const Math::Vector3 delta = Math::Vector3::Normalize(movement) * flySpeed * input.deltaTime;
|
||||||
m_position += delta;
|
m_position += delta;
|
||||||
m_focalPoint += delta;
|
m_focalPoint += delta;
|
||||||
@@ -135,7 +146,7 @@ public:
|
|||||||
private:
|
private:
|
||||||
void ApplyRotationDelta(float deltaX, float deltaY) {
|
void ApplyRotationDelta(float deltaX, float deltaY) {
|
||||||
m_yawDegrees += deltaX * 0.30f;
|
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 {
|
Math::Vector3 GetRight() const {
|
||||||
@@ -166,6 +177,7 @@ private:
|
|||||||
Math::Vector3 m_position = Math::Vector3::Zero();
|
Math::Vector3 m_position = Math::Vector3::Zero();
|
||||||
Math::Vector3 m_focalPoint = Math::Vector3::Zero();
|
Math::Vector3 m_focalPoint = Math::Vector3::Zero();
|
||||||
float m_distance = 6.0f;
|
float m_distance = 6.0f;
|
||||||
|
float m_flySpeed = 5.0f;
|
||||||
float m_yawDegrees = -35.0f;
|
float m_yawDegrees = -35.0f;
|
||||||
float m_pitchDegrees = -20.0f;
|
float m_pitchDegrees = -20.0f;
|
||||||
};
|
};
|
||||||
|
|||||||
71
editor/src/Viewport/SceneViewportGrid.cpp
Normal file
71
editor/src/Viewport/SceneViewportGrid.cpp
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
#include "SceneViewportGrid.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
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
|
||||||
18
editor/src/Viewport/SceneViewportGrid.h
Normal file
18
editor/src/Viewport/SceneViewportGrid.h
Normal file
@@ -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
|
||||||
389
editor/src/Viewport/SceneViewportInfiniteGridPass.cpp
Normal file
389
editor/src/Viewport/SceneViewportInfiniteGridPass.cpp
Normal file
@@ -0,0 +1,389 @@
|
|||||||
|
#include "SceneViewportInfiniteGridPass.h"
|
||||||
|
|
||||||
|
#include "SceneViewportGrid.h"
|
||||||
|
#include "SceneViewportMath.h"
|
||||||
|
|
||||||
|
#include <XCEngine/Core/Math/Vector4.h>
|
||||||
|
#include <XCEngine/RHI/RHICommandList.h>
|
||||||
|
#include <XCEngine/RHI/RHIDevice.h>
|
||||||
|
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
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<RHI::RHIResourceView*>& 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<float>(surface.GetWidth()),
|
||||||
|
static_cast<float>(surface.GetHeight())) *
|
||||||
|
BuildSceneViewportViewMatrix(overlay);
|
||||||
|
|
||||||
|
const float aspect = surface.GetHeight() > 0
|
||||||
|
? static_cast<float>(surface.GetWidth()) / static_cast<float>(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<float>(surface.GetWidth()),
|
||||||
|
static_cast<float>(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<float>(surface.GetWidth()),
|
||||||
|
static_cast<float>(surface.GetHeight()),
|
||||||
|
0.0f,
|
||||||
|
1.0f
|
||||||
|
};
|
||||||
|
const RHI::Rect scissorRect = {
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
static_cast<int32_t>(surface.GetWidth()),
|
||||||
|
static_cast<int32_t>(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<uint32_t>(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<uint32_t>(RHI::PrimitiveTopologyType::Triangle);
|
||||||
|
pipelineDesc.renderTargetCount = 1;
|
||||||
|
pipelineDesc.renderTargetFormats[0] = static_cast<uint32_t>(RHI::Format::R8G8B8A8_UNorm);
|
||||||
|
pipelineDesc.depthStencilFormat = static_cast<uint32_t>(RHI::Format::D24_UNorm_S8_UInt);
|
||||||
|
pipelineDesc.sampleCount = 1;
|
||||||
|
|
||||||
|
pipelineDesc.rasterizerState.fillMode = static_cast<uint32_t>(RHI::FillMode::Solid);
|
||||||
|
pipelineDesc.rasterizerState.cullMode = static_cast<uint32_t>(RHI::CullMode::None);
|
||||||
|
pipelineDesc.rasterizerState.frontFace = static_cast<uint32_t>(RHI::FrontFace::CounterClockwise);
|
||||||
|
pipelineDesc.rasterizerState.depthClipEnable = true;
|
||||||
|
|
||||||
|
pipelineDesc.blendState.blendEnable = true;
|
||||||
|
pipelineDesc.blendState.srcBlend = static_cast<uint32_t>(RHI::BlendFactor::SrcAlpha);
|
||||||
|
pipelineDesc.blendState.dstBlend = static_cast<uint32_t>(RHI::BlendFactor::InvSrcAlpha);
|
||||||
|
pipelineDesc.blendState.srcBlendAlpha = static_cast<uint32_t>(RHI::BlendFactor::One);
|
||||||
|
pipelineDesc.blendState.dstBlendAlpha = static_cast<uint32_t>(RHI::BlendFactor::InvSrcAlpha);
|
||||||
|
pipelineDesc.blendState.blendOp = static_cast<uint32_t>(RHI::BlendOp::Add);
|
||||||
|
pipelineDesc.blendState.blendOpAlpha = static_cast<uint32_t>(RHI::BlendOp::Add);
|
||||||
|
pipelineDesc.blendState.colorWriteMask = 0xF;
|
||||||
|
|
||||||
|
pipelineDesc.depthStencilState.depthTestEnable = true;
|
||||||
|
pipelineDesc.depthStencilState.depthWriteEnable = false;
|
||||||
|
pipelineDesc.depthStencilState.depthFunc = static_cast<uint32_t>(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
|
||||||
42
editor/src/Viewport/SceneViewportInfiniteGridPass.h
Normal file
42
editor/src/Viewport/SceneViewportInfiniteGridPass.h
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "IViewportHostService.h"
|
||||||
|
|
||||||
|
#include <XCEngine/RHI/RHIDescriptorPool.h>
|
||||||
|
#include <XCEngine/RHI/RHIDescriptorSet.h>
|
||||||
|
#include <XCEngine/RHI/RHIEnums.h>
|
||||||
|
#include <XCEngine/RHI/RHIPipelineState.h>
|
||||||
|
#include <XCEngine/RHI/RHIPipelineLayout.h>
|
||||||
|
#include <XCEngine/Rendering/RenderContext.h>
|
||||||
|
#include <XCEngine/Rendering/RenderSurface.h>
|
||||||
|
|
||||||
|
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
|
||||||
48
editor/src/Viewport/SceneViewportMath.h
Normal file
48
editor/src/Viewport/SceneViewportMath.h
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "IViewportHostService.h"
|
||||||
|
|
||||||
|
#include <XCEngine/Core/Math/Matrix4.h>
|
||||||
|
|
||||||
|
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
|
||||||
82
editor/src/Viewport/SceneViewportOverlayRenderer.cpp
Normal file
82
editor/src/Viewport/SceneViewportOverlayRenderer.cpp
Normal file
@@ -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
|
||||||
18
editor/src/Viewport/SceneViewportOverlayRenderer.h
Normal file
18
editor/src/Viewport/SceneViewportOverlayRenderer.h
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "IViewportHostService.h"
|
||||||
|
|
||||||
|
#include <imgui.h>
|
||||||
|
|
||||||
|
namespace XCEngine {
|
||||||
|
namespace Editor {
|
||||||
|
|
||||||
|
void DrawSceneViewportOverlay(
|
||||||
|
ImDrawList* drawList,
|
||||||
|
const SceneViewportOverlayData& overlay,
|
||||||
|
const ImVec2& viewportMin,
|
||||||
|
const ImVec2& viewportMax,
|
||||||
|
const ImVec2& viewportSize);
|
||||||
|
|
||||||
|
} // namespace Editor
|
||||||
|
} // namespace XCEngine
|
||||||
316
editor/src/Viewport/SceneViewportPicker.cpp
Normal file
316
editor/src/Viewport/SceneViewportPicker.cpp
Normal file
@@ -0,0 +1,316 @@
|
|||||||
|
#include "SceneViewportPicker.h"
|
||||||
|
|
||||||
|
#include <XCEngine/Components/GameObject.h>
|
||||||
|
#include <XCEngine/Components/MeshFilterComponent.h>
|
||||||
|
#include <XCEngine/Components/MeshRendererComponent.h>
|
||||||
|
#include <XCEngine/Core/Math/Box.h>
|
||||||
|
#include <XCEngine/Resources/Mesh/Mesh.h>
|
||||||
|
#include <XCEngine/Scene/Scene.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
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<const uint8_t*>(mesh.GetVertexData());
|
||||||
|
if (vertexData == nullptr || vertexStride < sizeof(Vector3) || vertexIndex >= mesh.GetVertexCount()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const size_t byteOffset = static_cast<size_t>(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<const uint32_t*>(mesh.GetIndexData());
|
||||||
|
outIndex = indices[indexOffset];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto* indices = static_cast<const uint16_t*>(mesh.GetIndexData());
|
||||||
|
outIndex = static_cast<uint32_t>(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<Components::MeshFilterComponent>();
|
||||||
|
auto* meshRenderer = gameObject->GetComponent<Components::MeshRendererComponent>();
|
||||||
|
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<GameObject*> roots = request.scene->GetRootGameObjects();
|
||||||
|
for (GameObject* root : roots) {
|
||||||
|
PickRecursive(root, worldRay, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Editor
|
||||||
|
} // namespace XCEngine
|
||||||
40
editor/src/Viewport/SceneViewportPicker.h
Normal file
40
editor/src/Viewport/SceneViewportPicker.h
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "IViewportHostService.h"
|
||||||
|
|
||||||
|
#include <XCEngine/Core/Math/Ray.h>
|
||||||
|
#include <XCEngine/Core/Math/Vector2.h>
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
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
|
||||||
339
editor/src/Viewport/SceneViewportSelectionMaskPass.cpp
Normal file
339
editor/src/Viewport/SceneViewportSelectionMaskPass.cpp
Normal file
@@ -0,0 +1,339 @@
|
|||||||
|
#include "SceneViewportSelectionMaskPass.h"
|
||||||
|
|
||||||
|
#include <XCEngine/Components/GameObject.h>
|
||||||
|
#include <XCEngine/RHI/RHICommandList.h>
|
||||||
|
#include <XCEngine/RHI/RHIDevice.h>
|
||||||
|
#include <XCEngine/Resources/Mesh/Mesh.h>
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
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<uint32_t>(RHI::Format::R32G32B32_Float);
|
||||||
|
position.inputSlot = 0;
|
||||||
|
position.alignedByteOffset = static_cast<uint32_t>(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<uint32_t>(RHI::PrimitiveTopologyType::Triangle);
|
||||||
|
pipelineDesc.renderTargetCount = 1;
|
||||||
|
pipelineDesc.renderTargetFormats[0] = static_cast<uint32_t>(RHI::Format::R8G8B8A8_UNorm);
|
||||||
|
pipelineDesc.depthStencilFormat = static_cast<uint32_t>(RHI::Format::D24_UNorm_S8_UInt);
|
||||||
|
pipelineDesc.sampleCount = 1;
|
||||||
|
pipelineDesc.inputLayout = BuildInputLayout();
|
||||||
|
|
||||||
|
pipelineDesc.rasterizerState.fillMode = static_cast<uint32_t>(RHI::FillMode::Solid);
|
||||||
|
pipelineDesc.rasterizerState.cullMode = static_cast<uint32_t>(RHI::CullMode::None);
|
||||||
|
pipelineDesc.rasterizerState.frontFace = static_cast<uint32_t>(RHI::FrontFace::CounterClockwise);
|
||||||
|
pipelineDesc.rasterizerState.depthClipEnable = true;
|
||||||
|
|
||||||
|
pipelineDesc.blendState.blendEnable = false;
|
||||||
|
pipelineDesc.blendState.colorWriteMask = static_cast<uint8_t>(RHI::ColorWriteMask::All);
|
||||||
|
|
||||||
|
pipelineDesc.depthStencilState.depthTestEnable = true;
|
||||||
|
pipelineDesc.depthStencilState.depthWriteEnable = false;
|
||||||
|
pipelineDesc.depthStencilState.depthFunc = static_cast<uint32_t>(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<Rendering::VisibleRenderItem>& renderables) {
|
||||||
|
if (!renderContext.IsValid() ||
|
||||||
|
renderContext.backendType != RHI::RHIType::D3D12 ||
|
||||||
|
renderables.empty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!EnsureInitialized(renderContext)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::vector<RHI::RHIResourceView*>& 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<float>(surface.GetWidth()),
|
||||||
|
static_cast<float>(surface.GetHeight()),
|
||||||
|
0.0f,
|
||||||
|
1.0f
|
||||||
|
};
|
||||||
|
const RHI::Rect scissorRect = {
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
static_cast<int32_t>(surface.GetWidth()),
|
||||||
|
static_cast<int32_t>(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<uint32_t>(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<uint32_t>(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
|
||||||
65
editor/src/Viewport/SceneViewportSelectionMaskPass.h
Normal file
65
editor/src/Viewport/SceneViewportSelectionMaskPass.h
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <XCEngine/Rendering/RenderCameraData.h>
|
||||||
|
#include <XCEngine/Rendering/RenderContext.h>
|
||||||
|
#include <XCEngine/Rendering/RenderResourceCache.h>
|
||||||
|
#include <XCEngine/Rendering/RenderSurface.h>
|
||||||
|
#include <XCEngine/Rendering/VisibleRenderObject.h>
|
||||||
|
|
||||||
|
#include <XCEngine/RHI/RHIDescriptorPool.h>
|
||||||
|
#include <XCEngine/RHI/RHIDescriptorSet.h>
|
||||||
|
#include <XCEngine/RHI/RHIEnums.h>
|
||||||
|
#include <XCEngine/RHI/RHIPipelineLayout.h>
|
||||||
|
#include <XCEngine/RHI/RHIPipelineState.h>
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
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<Rendering::VisibleRenderItem>& 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<uint64_t, OwnedDescriptorSet> m_perObjectSets;
|
||||||
|
Rendering::RenderResourceCache m_resourceCache;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Editor
|
||||||
|
} // namespace XCEngine
|
||||||
386
editor/src/Viewport/SceneViewportSelectionOutlinePass.cpp
Normal file
386
editor/src/Viewport/SceneViewportSelectionOutlinePass.cpp
Normal file
@@ -0,0 +1,386 @@
|
|||||||
|
#include "SceneViewportSelectionOutlinePass.h"
|
||||||
|
|
||||||
|
#include <XCEngine/RHI/RHICommandList.h>
|
||||||
|
#include <XCEngine/RHI/RHIDevice.h>
|
||||||
|
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
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<RHI::RHIResourceView*>& colorAttachments = surface.GetColorAttachments();
|
||||||
|
if (colorAttachments.empty() || colorAttachments[0] == nullptr) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
OutlineConstants constants = {};
|
||||||
|
constants.viewportSizeAndTexelSize = Math::Vector4(
|
||||||
|
static_cast<float>(surface.GetWidth()),
|
||||||
|
static_cast<float>(surface.GetHeight()),
|
||||||
|
surface.GetWidth() > 0 ? 1.0f / static_cast<float>(surface.GetWidth()) : 0.0f,
|
||||||
|
surface.GetHeight() > 0 ? 1.0f / static_cast<float>(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<float>(surface.GetWidth()),
|
||||||
|
static_cast<float>(surface.GetHeight()),
|
||||||
|
0.0f,
|
||||||
|
1.0f
|
||||||
|
};
|
||||||
|
const RHI::Rect scissorRect = {
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
static_cast<int32_t>(surface.GetWidth()),
|
||||||
|
static_cast<int32_t>(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<uint32_t>(RHI::DescriptorType::CBV);
|
||||||
|
setBindings[0].count = 1;
|
||||||
|
setBindings[1].binding = 0;
|
||||||
|
setBindings[1].type = static_cast<uint32_t>(RHI::DescriptorType::SRV);
|
||||||
|
setBindings[1].count = 1;
|
||||||
|
setBindings[2].binding = 0;
|
||||||
|
setBindings[2].type = static_cast<uint32_t>(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<uint32_t>(RHI::FilterMode::Linear);
|
||||||
|
samplerDesc.addressU = static_cast<uint32_t>(RHI::TextureAddressMode::Clamp);
|
||||||
|
samplerDesc.addressV = static_cast<uint32_t>(RHI::TextureAddressMode::Clamp);
|
||||||
|
samplerDesc.addressW = static_cast<uint32_t>(RHI::TextureAddressMode::Clamp);
|
||||||
|
samplerDesc.mipLodBias = 0.0f;
|
||||||
|
samplerDesc.maxAnisotropy = 1;
|
||||||
|
samplerDesc.comparisonFunc = static_cast<uint32_t>(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<uint32_t>(RHI::PrimitiveTopologyType::Triangle);
|
||||||
|
pipelineDesc.renderTargetCount = 1;
|
||||||
|
pipelineDesc.renderTargetFormats[0] = static_cast<uint32_t>(RHI::Format::R8G8B8A8_UNorm);
|
||||||
|
pipelineDesc.depthStencilFormat = static_cast<uint32_t>(RHI::Format::Unknown);
|
||||||
|
pipelineDesc.sampleCount = 1;
|
||||||
|
|
||||||
|
pipelineDesc.rasterizerState.fillMode = static_cast<uint32_t>(RHI::FillMode::Solid);
|
||||||
|
pipelineDesc.rasterizerState.cullMode = static_cast<uint32_t>(RHI::CullMode::None);
|
||||||
|
pipelineDesc.rasterizerState.frontFace = static_cast<uint32_t>(RHI::FrontFace::CounterClockwise);
|
||||||
|
pipelineDesc.rasterizerState.depthClipEnable = true;
|
||||||
|
|
||||||
|
pipelineDesc.blendState.blendEnable = true;
|
||||||
|
pipelineDesc.blendState.srcBlend = static_cast<uint32_t>(RHI::BlendFactor::SrcAlpha);
|
||||||
|
pipelineDesc.blendState.dstBlend = static_cast<uint32_t>(RHI::BlendFactor::InvSrcAlpha);
|
||||||
|
pipelineDesc.blendState.srcBlendAlpha = static_cast<uint32_t>(RHI::BlendFactor::One);
|
||||||
|
pipelineDesc.blendState.dstBlendAlpha = static_cast<uint32_t>(RHI::BlendFactor::InvSrcAlpha);
|
||||||
|
pipelineDesc.blendState.blendOp = static_cast<uint32_t>(RHI::BlendOp::Add);
|
||||||
|
pipelineDesc.blendState.blendOpAlpha = static_cast<uint32_t>(RHI::BlendOp::Add);
|
||||||
|
pipelineDesc.blendState.colorWriteMask = static_cast<uint8_t>(RHI::ColorWriteMask::All);
|
||||||
|
|
||||||
|
pipelineDesc.depthStencilState.depthTestEnable = false;
|
||||||
|
pipelineDesc.depthStencilState.depthWriteEnable = false;
|
||||||
|
pipelineDesc.depthStencilState.depthFunc = static_cast<uint32_t>(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
|
||||||
57
editor/src/Viewport/SceneViewportSelectionOutlinePass.h
Normal file
57
editor/src/Viewport/SceneViewportSelectionOutlinePass.h
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <XCEngine/Core/Math/Vector3.h>
|
||||||
|
#include <XCEngine/Core/Math/Vector4.h>
|
||||||
|
#include <XCEngine/Rendering/RenderCameraData.h>
|
||||||
|
#include <XCEngine/Rendering/RenderContext.h>
|
||||||
|
#include <XCEngine/Rendering/RenderSurface.h>
|
||||||
|
|
||||||
|
#include <XCEngine/RHI/RHIDescriptorPool.h>
|
||||||
|
#include <XCEngine/RHI/RHIDescriptorSet.h>
|
||||||
|
#include <XCEngine/RHI/RHIEnums.h>
|
||||||
|
#include <XCEngine/RHI/RHIPipelineLayout.h>
|
||||||
|
#include <XCEngine/RHI/RHIPipelineState.h>
|
||||||
|
#include <XCEngine/RHI/RHIResourceView.h>
|
||||||
|
#include <XCEngine/RHI/RHISampler.h>
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
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
|
||||||
161
editor/src/Viewport/SceneViewportSelectionUtils.h
Normal file
161
editor/src/Viewport/SceneViewportSelectionUtils.h
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "IViewportHostService.h"
|
||||||
|
#include "SceneViewportMath.h"
|
||||||
|
|
||||||
|
#include <XCEngine/Components/CameraComponent.h>
|
||||||
|
#include <XCEngine/Components/GameObject.h>
|
||||||
|
#include <XCEngine/Components/MeshFilterComponent.h>
|
||||||
|
#include <XCEngine/Components/MeshRendererComponent.h>
|
||||||
|
#include <XCEngine/Rendering/RenderCameraData.h>
|
||||||
|
#include <XCEngine/Rendering/RenderMaterialUtility.h>
|
||||||
|
#include <XCEngine/Rendering/VisibleRenderObject.h>
|
||||||
|
#include <XCEngine/Scene/Scene.h>
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <unordered_set>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
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<float>(viewportWidth),
|
||||||
|
static_cast<float>(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<float>(viewportWidth) / static_cast<float>(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<Rendering::VisibleRenderItem> CollectSceneViewportSelectionRenderables(
|
||||||
|
const Components::Scene& scene,
|
||||||
|
const std::vector<uint64_t>& selectedEntityIds,
|
||||||
|
const Math::Vector3& cameraPosition) {
|
||||||
|
std::vector<Rendering::VisibleRenderItem> renderables;
|
||||||
|
std::unordered_set<uint64_t> 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<Components::MeshFilterComponent>();
|
||||||
|
auto* meshRenderer = gameObject->GetComponent<Components::MeshRendererComponent>();
|
||||||
|
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<Core::uint32>(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
|
||||||
@@ -4,7 +4,12 @@
|
|||||||
#include "Core/ISceneManager.h"
|
#include "Core/ISceneManager.h"
|
||||||
#include "Core/ISelectionManager.h"
|
#include "Core/ISelectionManager.h"
|
||||||
#include "IViewportHostService.h"
|
#include "IViewportHostService.h"
|
||||||
|
#include "SceneViewportPicker.h"
|
||||||
#include "SceneViewportCameraController.h"
|
#include "SceneViewportCameraController.h"
|
||||||
|
#include "SceneViewportInfiniteGridPass.h"
|
||||||
|
#include "SceneViewportSelectionMaskPass.h"
|
||||||
|
#include "SceneViewportSelectionOutlinePass.h"
|
||||||
|
#include "SceneViewportSelectionUtils.h"
|
||||||
#include "UI/ImGuiBackendBridge.h"
|
#include "UI/ImGuiBackendBridge.h"
|
||||||
|
|
||||||
#include <XCEngine/Components/CameraComponent.h>
|
#include <XCEngine/Components/CameraComponent.h>
|
||||||
@@ -27,6 +32,12 @@
|
|||||||
namespace XCEngine {
|
namespace XCEngine {
|
||||||
namespace Editor {
|
namespace Editor {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr bool kDebugSceneSelectionMask = false;
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
class ViewportHostService : public IViewportHostService {
|
class ViewportHostService : public IViewportHostService {
|
||||||
public:
|
public:
|
||||||
void Initialize(UI::ImGuiBackendBridge& backend, RHI::RHIDevice* device) {
|
void Initialize(UI::ImGuiBackendBridge& backend, RHI::RHIDevice* device) {
|
||||||
@@ -44,6 +55,9 @@ public:
|
|||||||
m_sceneViewCamera = {};
|
m_sceneViewCamera = {};
|
||||||
m_device = nullptr;
|
m_device = nullptr;
|
||||||
m_backend = nullptr;
|
m_backend = nullptr;
|
||||||
|
m_sceneGridPass.Shutdown();
|
||||||
|
m_sceneSelectionMaskPass.Shutdown();
|
||||||
|
m_sceneSelectionOutlinePass.Shutdown();
|
||||||
m_sceneRenderer.reset();
|
m_sceneRenderer.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,6 +105,7 @@ public:
|
|||||||
SceneViewportCameraInputState controllerInput = {};
|
SceneViewportCameraInputState controllerInput = {};
|
||||||
controllerInput.viewportHeight = input.viewportSize.y;
|
controllerInput.viewportHeight = input.viewportSize.y;
|
||||||
controllerInput.zoomDelta = input.hovered ? input.mouseWheel : 0.0f;
|
controllerInput.zoomDelta = input.hovered ? input.mouseWheel : 0.0f;
|
||||||
|
controllerInput.flySpeedDelta = input.hovered ? input.flySpeedDelta : 0.0f;
|
||||||
controllerInput.deltaTime = input.deltaTime;
|
controllerInput.deltaTime = input.deltaTime;
|
||||||
controllerInput.moveForward = input.moveForward;
|
controllerInput.moveForward = input.moveForward;
|
||||||
controllerInput.moveRight = input.moveRight;
|
controllerInput.moveRight = input.moveRight;
|
||||||
@@ -116,6 +131,27 @@ public:
|
|||||||
ApplySceneViewCameraController();
|
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 GetSceneViewOverlayData() const override {
|
||||||
SceneViewportOverlayData data = {};
|
SceneViewportOverlayData data = {};
|
||||||
if (m_sceneViewCamera.gameObject == nullptr || m_sceneViewCamera.camera == nullptr) {
|
if (m_sceneViewCamera.gameObject == nullptr || m_sceneViewCamera.camera == nullptr) {
|
||||||
@@ -159,7 +195,7 @@ public:
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
RenderViewportEntry(entry, scene, renderContext);
|
RenderViewportEntry(entry, context, scene, renderContext);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,10 +211,14 @@ private:
|
|||||||
RHI::RHIResourceView* colorView = nullptr;
|
RHI::RHIResourceView* colorView = nullptr;
|
||||||
RHI::RHITexture* depthTexture = nullptr;
|
RHI::RHITexture* depthTexture = nullptr;
|
||||||
RHI::RHIResourceView* depthView = nullptr;
|
RHI::RHIResourceView* depthView = nullptr;
|
||||||
|
RHI::RHITexture* selectionMaskTexture = nullptr;
|
||||||
|
RHI::RHIResourceView* selectionMaskView = nullptr;
|
||||||
|
RHI::RHIResourceView* selectionMaskShaderView = nullptr;
|
||||||
D3D12_CPU_DESCRIPTOR_HANDLE imguiCpuHandle = {};
|
D3D12_CPU_DESCRIPTOR_HANDLE imguiCpuHandle = {};
|
||||||
D3D12_GPU_DESCRIPTOR_HANDLE imguiGpuHandle = {};
|
D3D12_GPU_DESCRIPTOR_HANDLE imguiGpuHandle = {};
|
||||||
ImTextureID textureId = {};
|
ImTextureID textureId = {};
|
||||||
RHI::ResourceStates colorState = RHI::ResourceStates::Common;
|
RHI::ResourceStates colorState = RHI::ResourceStates::Common;
|
||||||
|
RHI::ResourceStates selectionMaskState = RHI::ResourceStates::Common;
|
||||||
std::string statusText;
|
std::string statusText;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -277,6 +317,10 @@ private:
|
|||||||
entry.colorView != nullptr &&
|
entry.colorView != nullptr &&
|
||||||
entry.depthTexture != nullptr &&
|
entry.depthTexture != nullptr &&
|
||||||
entry.depthView != nullptr &&
|
entry.depthView != nullptr &&
|
||||||
|
(entry.kind != EditorViewportKind::Scene ||
|
||||||
|
(entry.selectionMaskTexture != nullptr &&
|
||||||
|
entry.selectionMaskView != nullptr &&
|
||||||
|
entry.selectionMaskShaderView != nullptr)) &&
|
||||||
entry.textureId != ImTextureID{}) {
|
entry.textureId != ImTextureID{}) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -338,6 +382,40 @@ private:
|
|||||||
return false;
|
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<uint32_t>(RHI::Format::R8G8B8A8_UNorm);
|
||||||
|
selectionMaskDesc.textureType = static_cast<uint32_t>(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<uint32_t>(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);
|
m_backend->AllocateTextureDescriptor(&entry.imguiCpuHandle, &entry.imguiGpuHandle);
|
||||||
if (entry.imguiCpuHandle.ptr == 0 || entry.imguiGpuHandle.ptr == 0) {
|
if (entry.imguiCpuHandle.ptr == 0 || entry.imguiGpuHandle.ptr == 0) {
|
||||||
DestroyViewportResources(entry);
|
DestroyViewportResources(entry);
|
||||||
@@ -357,6 +435,7 @@ private:
|
|||||||
|
|
||||||
entry.textureId = static_cast<ImTextureID>(entry.imguiGpuHandle.ptr);
|
entry.textureId = static_cast<ImTextureID>(entry.imguiGpuHandle.ptr);
|
||||||
entry.colorState = RHI::ResourceStates::Common;
|
entry.colorState = RHI::ResourceStates::Common;
|
||||||
|
entry.selectionMaskState = RHI::ResourceStates::Common;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -369,8 +448,18 @@ private:
|
|||||||
return surface;
|
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(
|
void RenderViewportEntry(
|
||||||
ViewportEntry& entry,
|
ViewportEntry& entry,
|
||||||
|
IEditorContext& context,
|
||||||
const Components::Scene* scene,
|
const Components::Scene* scene,
|
||||||
const Rendering::RenderContext& renderContext) {
|
const Rendering::RenderContext& renderContext) {
|
||||||
if (entry.colorView == nullptr || entry.depthView == nullptr) {
|
if (entry.colorView == nullptr || entry.depthView == nullptr) {
|
||||||
@@ -378,12 +467,6 @@ private:
|
|||||||
return;
|
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);
|
Rendering::RenderSurface surface = BuildSurface(entry);
|
||||||
|
|
||||||
if (entry.kind == EditorViewportKind::Scene) {
|
if (entry.kind == EditorViewportKind::Scene) {
|
||||||
@@ -394,14 +477,130 @@ private:
|
|||||||
}
|
}
|
||||||
|
|
||||||
ApplySceneViewCameraController();
|
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";
|
entry.statusText = "Scene renderer failed";
|
||||||
ClearViewport(entry, renderContext, 0.18f, 0.07f, 0.07f, 1.0f);
|
ClearViewport(entry, renderContext, 0.18f, 0.07f, 0.07f, 1.0f);
|
||||||
return;
|
return;
|
||||||
|
} else {
|
||||||
|
entry.colorState = RHI::ResourceStates::PixelShaderResource;
|
||||||
|
entry.statusText.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
entry.colorState = RHI::ResourceStates::PixelShaderResource;
|
const SceneViewportOverlayData overlay = GetSceneViewOverlayData();
|
||||||
entry.statusText.clear();
|
if (overlay.valid) {
|
||||||
|
RHI::RHICommandList* commandList = renderContext.commandList;
|
||||||
|
bool hasSelection = false;
|
||||||
|
Rendering::RenderCameraData cameraData = {};
|
||||||
|
std::vector<Rendering::VisibleRenderItem> 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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -456,6 +655,24 @@ private:
|
|||||||
m_backend->FreeTextureDescriptor(entry.imguiCpuHandle, entry.imguiGpuHandle);
|
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) {
|
if (entry.depthView != nullptr) {
|
||||||
entry.depthView->Shutdown();
|
entry.depthView->Shutdown();
|
||||||
delete entry.depthView;
|
delete entry.depthView;
|
||||||
@@ -486,6 +703,7 @@ private:
|
|||||||
entry.imguiGpuHandle = {};
|
entry.imguiGpuHandle = {};
|
||||||
entry.textureId = {};
|
entry.textureId = {};
|
||||||
entry.colorState = RHI::ResourceStates::Common;
|
entry.colorState = RHI::ResourceStates::Common;
|
||||||
|
entry.selectionMaskState = RHI::ResourceStates::Common;
|
||||||
}
|
}
|
||||||
|
|
||||||
UI::ImGuiBackendBridge* m_backend = nullptr;
|
UI::ImGuiBackendBridge* m_backend = nullptr;
|
||||||
@@ -493,6 +711,9 @@ private:
|
|||||||
std::unique_ptr<Rendering::SceneRenderer> m_sceneRenderer;
|
std::unique_ptr<Rendering::SceneRenderer> m_sceneRenderer;
|
||||||
std::array<ViewportEntry, 2> m_entries = {};
|
std::array<ViewportEntry, 2> m_entries = {};
|
||||||
SceneViewCameraState m_sceneViewCamera;
|
SceneViewCameraState m_sceneViewCamera;
|
||||||
|
SceneViewportInfiniteGridPass m_sceneGridPass;
|
||||||
|
SceneViewportSelectionMaskPass m_sceneSelectionMaskPass;
|
||||||
|
SceneViewportSelectionOutlinePass m_sceneSelectionOutlinePass;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace Editor
|
} // namespace Editor
|
||||||
|
|||||||
@@ -111,11 +111,20 @@ void HierarchyPanel::Render() {
|
|||||||
RenderEntity(gameObject);
|
RenderEntity(gameObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
Actions::HandleHierarchyBackgroundPrimaryClick(*m_context, m_renameState);
|
Actions::DrawHierarchyBackgroundInteraction(*m_context, m_renameState);
|
||||||
Actions::RequestHierarchyBackgroundContextPopup(m_backgroundContextMenu);
|
|
||||||
Actions::DrawHierarchyEntityContextPopup(*m_context, m_itemContextMenu);
|
Actions::DrawHierarchyEntityContextPopup(*m_context, m_itemContextMenu);
|
||||||
Actions::DrawHierarchyBackgroundContextPopup(*m_context, m_backgroundContextMenu);
|
static bool s_backgroundContextOpen = false;
|
||||||
Actions::DrawHierarchyRootDropTarget(*m_context);
|
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);
|
ImGui::PopStyleColor(2);
|
||||||
}
|
}
|
||||||
@@ -126,22 +135,16 @@ void HierarchyPanel::RenderEntity(::XCEngine::Components::GameObject* gameObject
|
|||||||
ImGui::PushID(static_cast<int>(gameObject->GetID()));
|
ImGui::PushID(static_cast<int>(gameObject->GetID()));
|
||||||
|
|
||||||
if (m_renameState.IsEditing(gameObject->GetID())) {
|
if (m_renameState.IsEditing(gameObject->GetID())) {
|
||||||
if (m_renameState.ConsumeFocusRequest()) {
|
const UI::InlineRenameFieldResult renameField = UI::DrawInlineRenameField(
|
||||||
ImGui::SetKeyboardFocusHere();
|
"##Rename",
|
||||||
}
|
m_renameState.Buffer(),
|
||||||
|
m_renameState.BufferSize(),
|
||||||
ImGui::SetNextItemWidth(-1);
|
-1.0f,
|
||||||
if (ImGui::InputText(
|
m_renameState.ConsumeFocusRequest());
|
||||||
"##Rename",
|
|
||||||
m_renameState.Buffer(),
|
|
||||||
m_renameState.BufferSize(),
|
|
||||||
ImGuiInputTextFlags_EnterReturnsTrue | ImGuiInputTextFlags_AutoSelectAll)) {
|
|
||||||
CommitRename();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ImGui::IsItemActive() && ImGui::IsKeyPressed(ImGuiKey_Escape)) {
|
if (renameField.cancelRequested) {
|
||||||
CancelRename();
|
CancelRename();
|
||||||
} else if (!ImGui::IsItemActive() && ImGui::IsMouseClicked(0)) {
|
} else if (renameField.submitted || renameField.deactivated) {
|
||||||
CommitRename();
|
CommitRename();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -161,10 +164,6 @@ void HierarchyPanel::RenderEntity(::XCEngine::Components::GameObject* gameObject
|
|||||||
if (node.secondaryClicked) {
|
if (node.secondaryClicked) {
|
||||||
Actions::HandleHierarchyItemContextRequest(*m_context, gameObject, m_itemContextMenu);
|
Actions::HandleHierarchyItemContextRequest(*m_context, gameObject, m_itemContextMenu);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (node.doubleClicked) {
|
|
||||||
BeginRename(gameObject);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
nodeDefinition.callbacks.onRenderExtras = [this, gameObject]() {
|
nodeDefinition.callbacks.onRenderExtras = [this, gameObject]() {
|
||||||
Actions::BeginHierarchyEntityDrag(gameObject);
|
Actions::BeginHierarchyEntityDrag(gameObject);
|
||||||
|
|||||||
@@ -16,6 +16,32 @@
|
|||||||
namespace XCEngine {
|
namespace XCEngine {
|
||||||
namespace Editor {
|
namespace Editor {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
template <typename Fn>
|
||||||
|
void QueueDeferredAction(std::function<void()>& pendingAction, Fn&& fn) {
|
||||||
|
if (!pendingAction) {
|
||||||
|
pendingAction = std::forward<Fn>(fn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DrawInspectorComponentContextMenu(
|
||||||
|
IEditorContext& context,
|
||||||
|
::XCEngine::Components::Component* component,
|
||||||
|
::XCEngine::Components::GameObject* gameObject,
|
||||||
|
std::function<void()>& 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() : Panel("Inspector") {}
|
||||||
|
|
||||||
InspectorPanel::~InspectorPanel() {
|
InspectorPanel::~InspectorPanel() {
|
||||||
@@ -91,14 +117,24 @@ void InspectorPanel::RenderGameObject(::XCEngine::Components::GameObject* gameOb
|
|||||||
}
|
}
|
||||||
|
|
||||||
auto components = gameObject->GetComponents<::XCEngine::Components::Component>();
|
auto components = gameObject->GetComponents<::XCEngine::Components::Component>();
|
||||||
|
m_deferredContextAction = {};
|
||||||
for (auto* component : components) {
|
for (auto* component : components) {
|
||||||
RenderComponent(component, gameObject);
|
RenderComponent(component, gameObject);
|
||||||
|
if (m_deferredContextAction) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Actions::DrawInspectorAddComponentButton(m_addComponentPopup, gameObject != nullptr);
|
Actions::DrawInspectorAddComponentButton(m_addComponentPopup, gameObject != nullptr);
|
||||||
Actions::DrawInspectorAddComponentPopup(*m_context, m_addComponentPopup, gameObject);
|
Actions::DrawInspectorAddComponentPopup(*m_context, m_addComponentPopup, gameObject);
|
||||||
|
|
||||||
Actions::FinalizeInspectorInteractiveChangeIfIdle(*m_context);
|
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) {
|
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);
|
IComponentEditor* editor = ComponentEditorRegistry::Get().FindEditor(component);
|
||||||
const std::string name = component->GetName();
|
const std::string name = component->GetName();
|
||||||
|
|
||||||
bool removed = false;
|
|
||||||
const UI::ComponentSectionResult section = UI::BeginComponentSection(
|
const UI::ComponentSectionResult section = UI::BeginComponentSection(
|
||||||
(void*)typeid(*component).hash_code(),
|
(void*)typeid(*component).hash_code(),
|
||||||
name.c_str(),
|
name.c_str());
|
||||||
[&]() {
|
|
||||||
removed = Actions::DrawInspectorComponentMenu(*m_context, component, gameObject, editor);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (removed) {
|
if (UI::BeginContextMenuForLastItem("##InspectorComponentContext")) {
|
||||||
return;
|
DrawInspectorComponentContextMenu(*m_context, component, gameObject, m_deferredContextAction);
|
||||||
|
UI::EndContextMenu();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (section.open) {
|
if (section.open) {
|
||||||
ImGui::Indent(section.contentIndent);
|
ImGui::Indent(section.contentIndent);
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, UI::InspectorComponentControlSpacing());
|
if (!m_deferredContextAction) {
|
||||||
if (editor) {
|
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, UI::InspectorComponentControlSpacing());
|
||||||
if (editor->Render(component, &m_context->GetUndoManager())) {
|
if (editor) {
|
||||||
m_context->GetSceneManager().MarkSceneDirty();
|
if (editor->Render(component, &m_context->GetUndoManager())) {
|
||||||
|
m_context->GetSceneManager().MarkSceneDirty();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
UI::DrawHintText("No registered editor for this component");
|
||||||
}
|
}
|
||||||
} else {
|
ImGui::PopStyleVar();
|
||||||
UI::DrawHintText("No registered editor for this component");
|
|
||||||
}
|
}
|
||||||
ImGui::PopStyleVar();
|
|
||||||
|
|
||||||
UI::EndComponentSection(section);
|
UI::EndComponentSection(section);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
#include "UI/PopupState.h"
|
#include "UI/PopupState.h"
|
||||||
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
#include <functional>
|
||||||
|
|
||||||
namespace XCEngine {
|
namespace XCEngine {
|
||||||
namespace Components {
|
namespace Components {
|
||||||
@@ -31,6 +32,7 @@ private:
|
|||||||
uint64_t m_selectionHandlerId = 0;
|
uint64_t m_selectionHandlerId = 0;
|
||||||
uint64_t m_selectedEntityId = 0;
|
uint64_t m_selectedEntityId = 0;
|
||||||
UI::DeferredPopupState m_addComponentPopup;
|
UI::DeferredPopupState m_addComponentPopup;
|
||||||
|
std::function<void()> m_deferredContextAction;
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,13 @@ namespace Editor {
|
|||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
|
template <typename Fn>
|
||||||
|
void QueueDeferredAction(std::function<void()>& pendingAction, Fn&& fn) {
|
||||||
|
if (!pendingAction) {
|
||||||
|
pendingAction = std::forward<Fn>(fn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void DrawProjectFolderTreePrefix(const UI::TreeNodePrefixContext& context) {
|
void DrawProjectFolderTreePrefix(const UI::TreeNodePrefixContext& context) {
|
||||||
if (!context.drawList) {
|
if (!context.drawList) {
|
||||||
return;
|
return;
|
||||||
@@ -186,6 +193,7 @@ void ProjectPanel::Render() {
|
|||||||
|
|
||||||
auto& manager = m_context->GetProjectManager();
|
auto& manager = m_context->GetProjectManager();
|
||||||
BeginAssetDragDropFrame();
|
BeginAssetDragDropFrame();
|
||||||
|
m_deferredContextAction = {};
|
||||||
RenderToolbar();
|
RenderToolbar();
|
||||||
|
|
||||||
ImGui::PushStyleColor(ImGuiCol_ChildBg, UI::ProjectBrowserSurfaceColor());
|
ImGui::PushStyleColor(ImGuiCol_ChildBg, UI::ProjectBrowserSurfaceColor());
|
||||||
@@ -215,6 +223,12 @@ void ProjectPanel::Render() {
|
|||||||
|
|
||||||
FinalizeAssetDragDrop(manager);
|
FinalizeAssetDragDrop(manager);
|
||||||
ImGui::PopStyleColor();
|
ImGui::PopStyleColor();
|
||||||
|
|
||||||
|
if (m_deferredContextAction) {
|
||||||
|
auto deferredAction = std::move(m_deferredContextAction);
|
||||||
|
m_deferredContextAction = {};
|
||||||
|
deferredAction();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void ProjectPanel::RenderToolbar() {
|
void ProjectPanel::RenderToolbar() {
|
||||||
@@ -246,6 +260,7 @@ void ProjectPanel::RenderToolbar() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void ProjectPanel::RenderFolderTreePane(IProjectManager& manager) {
|
void ProjectPanel::RenderFolderTreePane(IProjectManager& manager) {
|
||||||
|
auto* managerPtr = &manager;
|
||||||
ImGui::PushStyleColor(ImGuiCol_ChildBg, UI::ProjectNavigationPaneBackgroundColor());
|
ImGui::PushStyleColor(ImGuiCol_ChildBg, UI::ProjectNavigationPaneBackgroundColor());
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, UI::ProjectNavigationPanePadding());
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, UI::ProjectNavigationPanePadding());
|
||||||
const bool open = ImGui::BeginChild("ProjectFolderTree", ImVec2(m_navigationWidth, 0.0f), false);
|
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");
|
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();
|
ImGui::EndChild();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -298,9 +324,6 @@ void ProjectPanel::RenderFolderTreeNode(
|
|||||||
manager.NavigateToFolder(folder);
|
manager.NavigateToFolder(folder);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (node.secondaryClicked) {
|
|
||||||
Actions::HandleProjectItemContextRequest(manager, folder, m_itemContextMenu);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const UI::TreeNodeResult node = UI::DrawTreeNode(
|
const UI::TreeNodeResult node = UI::DrawTreeNode(
|
||||||
@@ -323,6 +346,7 @@ void ProjectPanel::RenderFolderTreeNode(
|
|||||||
}
|
}
|
||||||
|
|
||||||
void ProjectPanel::RenderBrowserPane(IProjectManager& manager) {
|
void ProjectPanel::RenderBrowserPane(IProjectManager& manager) {
|
||||||
|
auto* managerPtr = &manager;
|
||||||
ImGui::PushStyleColor(ImGuiCol_ChildBg, UI::ProjectBrowserPaneBackgroundColor());
|
ImGui::PushStyleColor(ImGuiCol_ChildBg, UI::ProjectBrowserPaneBackgroundColor());
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f));
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f));
|
||||||
const bool open = ImGui::BeginChild("ProjectBrowser", ImVec2(0.0f, 0.0f), false);
|
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 tileWidth = UI::AssetTileSize().x;
|
||||||
|
const float tileHeight = UI::AssetTileSize().y;
|
||||||
const float spacing = UI::AssetGridSpacing().x;
|
const float spacing = UI::AssetGridSpacing().x;
|
||||||
|
const float rowSpacing = UI::AssetGridSpacing().y;
|
||||||
const float panelWidth = ImGui::GetContentRegionAvail().x;
|
const float panelWidth = ImGui::GetContentRegionAvail().x;
|
||||||
int columns = static_cast<int>((panelWidth + spacing) / (tileWidth + spacing));
|
int columns = static_cast<int>((panelWidth + spacing) / (tileWidth + spacing));
|
||||||
if (columns < 1) {
|
if (columns < 1) {
|
||||||
@@ -369,26 +395,33 @@ void ProjectPanel::RenderBrowserPane(IProjectManager& manager) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
AssetItemPtr pendingSelection;
|
AssetItemPtr pendingSelection;
|
||||||
AssetItemPtr pendingContextTarget;
|
|
||||||
AssetItemPtr pendingOpenTarget;
|
AssetItemPtr pendingOpenTarget;
|
||||||
const std::string selectedItemPath = manager.GetSelectedItemPath();
|
const std::string selectedItemPath = manager.GetSelectedItemPath();
|
||||||
|
const ImVec2 gridOrigin = ImGui::GetCursorPos();
|
||||||
for (int visibleIndex = 0; visibleIndex < static_cast<int>(visibleItems.size()); ++visibleIndex) {
|
for (int visibleIndex = 0; visibleIndex < static_cast<int>(visibleItems.size()); ++visibleIndex) {
|
||||||
if (visibleIndex > 0 && visibleIndex % columns != 0) {
|
const int column = visibleIndex % columns;
|
||||||
ImGui::SameLine();
|
const int row = visibleIndex / columns;
|
||||||
}
|
ImGui::SetCursorPos(ImVec2(
|
||||||
|
gridOrigin.x + column * (tileWidth + spacing),
|
||||||
|
gridOrigin.y + row * (tileHeight + rowSpacing)));
|
||||||
|
|
||||||
const AssetItemPtr& item = visibleItems[visibleIndex];
|
const AssetItemPtr& item = visibleItems[visibleIndex];
|
||||||
const AssetItemInteraction interaction = RenderAssetItem(item, selectedItemPath == item->fullPath);
|
const AssetItemInteraction interaction = RenderAssetItem(item, selectedItemPath == item->fullPath);
|
||||||
if (interaction.clicked) {
|
if (interaction.clicked) {
|
||||||
pendingSelection = item;
|
pendingSelection = item;
|
||||||
}
|
}
|
||||||
if (interaction.contextRequested) {
|
|
||||||
pendingContextTarget = item;
|
|
||||||
}
|
|
||||||
if (interaction.openRequested) {
|
if (interaction.openRequested) {
|
||||||
pendingOpenTarget = item;
|
pendingOpenTarget = item;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
if (m_deferredContextAction) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!visibleItems.empty()) {
|
||||||
|
const int rowCount = (static_cast<int>(visibleItems.size()) + columns - 1) / columns;
|
||||||
|
ImGui::SetCursorPosY(gridOrigin.y + rowCount * tileHeight + (rowCount - 1) * rowSpacing);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (visibleItems.empty() && !search.empty()) {
|
if (visibleItems.empty() && !search.empty()) {
|
||||||
@@ -397,29 +430,41 @@ void ProjectPanel::RenderBrowserPane(IProjectManager& manager) {
|
|||||||
"No assets match the current search");
|
"No assets match the current search");
|
||||||
}
|
}
|
||||||
|
|
||||||
Actions::HandleProjectBackgroundPrimaryClick(manager, m_renameState);
|
if (!m_deferredContextAction) {
|
||||||
if (pendingSelection) {
|
Actions::HandleProjectBackgroundPrimaryClick(manager, m_renameState);
|
||||||
manager.SetSelectedItem(pendingSelection);
|
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 (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();
|
||||||
ImGui::EndChild();
|
ImGui::EndChild();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ProjectPanel::RenderBrowserHeader(IProjectManager& manager) {
|
void ProjectPanel::RenderBrowserHeader(IProjectManager& manager) {
|
||||||
|
auto* managerPtr = &manager;
|
||||||
ImGui::PushStyleColor(ImGuiCol_ChildBg, UI::ProjectBrowserHeaderBackgroundColor());
|
ImGui::PushStyleColor(ImGuiCol_ChildBg, UI::ProjectBrowserHeaderBackgroundColor());
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(10.0f, 5.0f));
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(10.0f, 5.0f));
|
||||||
const bool open = ImGui::BeginChild(
|
const bool open = ImGui::BeginChild(
|
||||||
@@ -445,7 +490,11 @@ void ProjectPanel::RenderBrowserHeader(IProjectManager& manager) {
|
|||||||
"Assets",
|
"Assets",
|
||||||
manager.GetPathDepth(),
|
manager.GetPathDepth(),
|
||||||
[&](size_t index) { return manager.GetPathName(index); },
|
[&](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();
|
ImDrawList* drawList = ImGui::GetWindowDrawList();
|
||||||
const ImVec2 windowMin = ImGui::GetWindowPos();
|
const ImVec2 windowMin = ImGui::GetWindowPos();
|
||||||
@@ -478,41 +527,54 @@ ProjectPanel::AssetItemInteraction ProjectPanel::RenderAssetItem(const AssetItem
|
|||||||
UI::DrawAssetIcon(drawList, iconMin, iconMax, iconKind);
|
UI::DrawAssetIcon(drawList, iconMin, iconMax, iconKind);
|
||||||
},
|
},
|
||||||
tileOptions);
|
tileOptions);
|
||||||
|
const bool secondaryClicked = !isRenaming && ImGui::IsItemClicked(ImGuiMouseButton_Right);
|
||||||
|
|
||||||
if (isRenaming) {
|
if (isRenaming) {
|
||||||
const ImVec2 restoreCursor = ImGui::GetCursorPos();
|
const float renameWidth = tile.labelMax.x - tile.labelMin.x;
|
||||||
ImGui::SetCursorScreenPos(tile.labelMin);
|
const float renameOffsetY = (std::max)(0.0f, (tile.labelMax.y - tile.labelMin.y - UI::InlineRenameFieldHeight()) * 0.5f);
|
||||||
ImGui::SetNextItemWidth(tile.labelMax.x - tile.labelMin.x);
|
const UI::InlineRenameFieldResult renameField = UI::DrawInlineRenameFieldAt(
|
||||||
if (m_renameState.ConsumeFocusRequest()) {
|
|
||||||
ImGui::SetKeyboardFocusHere();
|
|
||||||
}
|
|
||||||
|
|
||||||
const bool submitted = ImGui::InputText(
|
|
||||||
"##Rename",
|
"##Rename",
|
||||||
|
ImVec2(tile.labelMin.x, tile.labelMin.y + renameOffsetY),
|
||||||
m_renameState.Buffer(),
|
m_renameState.Buffer(),
|
||||||
m_renameState.BufferSize(),
|
m_renameState.BufferSize(),
|
||||||
ImGuiInputTextFlags_EnterReturnsTrue | ImGuiInputTextFlags_AutoSelectAll);
|
renameWidth,
|
||||||
const bool cancelRequested = ImGui::IsItemActive() && ImGui::IsKeyPressed(ImGuiKey_Escape);
|
m_renameState.ConsumeFocusRequest());
|
||||||
const bool deactivated = ImGui::IsItemDeactivated();
|
|
||||||
ImGui::SetCursorPos(restoreCursor);
|
|
||||||
|
|
||||||
if (cancelRequested) {
|
if (renameField.cancelRequested) {
|
||||||
CancelRename();
|
CancelRename();
|
||||||
} else if (submitted || deactivated) {
|
} else if (renameField.submitted || renameField.deactivated) {
|
||||||
CommitRename(m_context->GetProjectManager());
|
CommitRename(m_context->GetProjectManager());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (tile.clicked) {
|
if (tile.clicked) {
|
||||||
interaction.clicked = true;
|
interaction.clicked = true;
|
||||||
}
|
}
|
||||||
|
if (secondaryClicked && item) {
|
||||||
if (tile.contextRequested) {
|
m_context->GetProjectManager().SetSelectedItem(item);
|
||||||
interaction.contextRequested = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
RegisterFolderDropTarget(m_context->GetProjectManager(), item);
|
RegisterFolderDropTarget(m_context->GetProjectManager(), item);
|
||||||
Actions::BeginProjectAssetDrag(item, iconKind);
|
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) {
|
if (tile.openRequested) {
|
||||||
interaction.openRequested = true;
|
interaction.openRequested = true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
#include "UI/PopupState.h"
|
#include "UI/PopupState.h"
|
||||||
#include "UI/TreeView.h"
|
#include "UI/TreeView.h"
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
|
|
||||||
namespace XCEngine {
|
namespace XCEngine {
|
||||||
namespace Editor {
|
namespace Editor {
|
||||||
|
|
||||||
@@ -35,7 +37,6 @@ private:
|
|||||||
|
|
||||||
struct AssetItemInteraction {
|
struct AssetItemInteraction {
|
||||||
bool clicked = false;
|
bool clicked = false;
|
||||||
bool contextRequested = false;
|
|
||||||
bool openRequested = false;
|
bool openRequested = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -58,9 +59,8 @@ private:
|
|||||||
float m_navigationWidth = UI::ProjectNavigationDefaultWidth();
|
float m_navigationWidth = UI::ProjectNavigationDefaultWidth();
|
||||||
UI::TreeViewState m_folderTreeState;
|
UI::TreeViewState m_folderTreeState;
|
||||||
UI::InlineTextEditState<std::string, 256> m_renameState;
|
UI::InlineTextEditState<std::string, 256> m_renameState;
|
||||||
UI::DeferredPopupState m_emptyContextMenu;
|
|
||||||
UI::TargetedPopupState<AssetItemPtr> m_itemContextMenu;
|
|
||||||
AssetDragDropState m_assetDragDropState;
|
AssetDragDropState m_assetDragDropState;
|
||||||
|
std::function<void()> m_deferredContextAction;
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,233 +1,16 @@
|
|||||||
#include "Actions/ActionRouting.h"
|
#include "Actions/ActionRouting.h"
|
||||||
#include "Core/IEditorContext.h"
|
#include "Core/IEditorContext.h"
|
||||||
|
#include "Core/ISelectionManager.h"
|
||||||
#include "SceneViewPanel.h"
|
#include "SceneViewPanel.h"
|
||||||
|
#include "Viewport/SceneViewportOverlayRenderer.h"
|
||||||
#include "ViewportPanelContent.h"
|
#include "ViewportPanelContent.h"
|
||||||
#include "UI/UI.h"
|
#include "UI/UI.h"
|
||||||
|
|
||||||
#include <XCEngine/Core/Math/Matrix4.h>
|
|
||||||
#include <XCEngine/Core/Math/Vector4.h>
|
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
#include <cmath>
|
|
||||||
|
|
||||||
#include <imgui.h>
|
#include <imgui.h>
|
||||||
|
|
||||||
namespace XCEngine {
|
namespace XCEngine {
|
||||||
namespace Editor {
|
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<float>(halfLineCount);
|
|
||||||
|
|
||||||
drawList->PushClipRect(viewportMin, viewportMax, true);
|
|
||||||
|
|
||||||
for (int lineIndex = -halfLineCount; lineIndex <= halfLineCount; ++lineIndex) {
|
|
||||||
const float offset = static_cast<float>(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") {}
|
SceneViewPanel::SceneViewPanel() : Panel("Scene") {}
|
||||||
|
|
||||||
void SceneViewPanel::Render() {
|
void SceneViewPanel::Render() {
|
||||||
@@ -239,35 +22,76 @@ void SceneViewPanel::Render() {
|
|||||||
const ViewportPanelContentResult content = RenderViewportPanelContent(*m_context, EditorViewportKind::Scene);
|
const ViewportPanelContentResult content = RenderViewportPanelContent(*m_context, EditorViewportKind::Scene);
|
||||||
if (IViewportHostService* viewportHostService = m_context->GetViewportHostService()) {
|
if (IViewportHostService* viewportHostService = m_context->GetViewportHostService()) {
|
||||||
const ImGuiIO& io = ImGui::GetIO();
|
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)) {
|
if (selectClick || beginLookDrag || beginPanDrag) {
|
||||||
m_lookDragging = true;
|
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_panDragging = true;
|
||||||
|
m_lastPanDragDelta = ImVec2(0.0f, 0.0f);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (m_lookDragging && !ImGui::IsMouseDown(ImGuiMouseButton_Right)) {
|
if (m_lookDragging && !ImGui::IsMouseDown(ImGuiMouseButton_Right)) {
|
||||||
m_lookDragging = false;
|
m_lookDragging = false;
|
||||||
|
m_lastLookDragDelta = ImVec2(0.0f, 0.0f);
|
||||||
}
|
}
|
||||||
if (m_panDragging && !ImGui::IsMouseDown(ImGuiMouseButton_Middle)) {
|
if (m_panDragging && !ImGui::IsMouseDown(ImGuiMouseButton_Middle)) {
|
||||||
m_panDragging = false;
|
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 = {};
|
SceneViewportInput input = {};
|
||||||
input.viewportSize = content.availableSize;
|
input.viewportSize = content.availableSize;
|
||||||
input.deltaTime = io.DeltaTime;
|
input.deltaTime = io.DeltaTime;
|
||||||
input.hovered = content.hovered;
|
input.hovered = content.hovered;
|
||||||
input.focused = content.focused;
|
input.focused = content.focused || m_lookDragging || m_panDragging;
|
||||||
input.mouseWheel = content.hovered ? -io.MouseWheel : 0.0f;
|
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.looking = m_lookDragging;
|
||||||
input.orbiting = false;
|
input.orbiting = false;
|
||||||
input.panning = m_panDragging;
|
input.panning = m_panDragging;
|
||||||
input.fastMove = io.KeyShift;
|
input.fastMove = io.KeyShift;
|
||||||
input.focusSelectionRequested =
|
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 =
|
input.moveForward =
|
||||||
(ImGui::IsKeyDown(ImGuiKey_W) ? 1.0f : 0.0f) -
|
(ImGui::IsKeyDown(ImGuiKey_W) ? 1.0f : 0.0f) -
|
||||||
(ImGui::IsKeyDown(ImGuiKey_S) ? 1.0f : 0.0f);
|
(ImGui::IsKeyDown(ImGuiKey_S) ? 1.0f : 0.0f);
|
||||||
@@ -280,14 +104,30 @@ void SceneViewPanel::Render() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (m_lookDragging || m_panDragging) {
|
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);
|
viewportHostService->UpdateSceneViewInput(*m_context, input);
|
||||||
|
|
||||||
if (content.hasViewportArea && content.frame.hasTexture) {
|
if (content.hasViewportArea && content.frame.hasTexture) {
|
||||||
const SceneViewportOverlayData overlay = viewportHostService->GetSceneViewOverlayData();
|
const SceneViewportOverlayData overlay = viewportHostService->GetSceneViewOverlayData();
|
||||||
DrawSceneGridOverlay(
|
DrawSceneViewportOverlay(
|
||||||
ImGui::GetWindowDrawList(),
|
ImGui::GetWindowDrawList(),
|
||||||
overlay,
|
overlay,
|
||||||
content.itemMin,
|
content.itemMin,
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
#include "Panel.h"
|
#include "Panel.h"
|
||||||
|
|
||||||
|
#include <imgui.h>
|
||||||
|
|
||||||
namespace XCEngine {
|
namespace XCEngine {
|
||||||
namespace Editor {
|
namespace Editor {
|
||||||
|
|
||||||
@@ -13,6 +15,8 @@ public:
|
|||||||
private:
|
private:
|
||||||
bool m_lookDragging = false;
|
bool m_lookDragging = false;
|
||||||
bool m_panDragging = false;
|
bool m_panDragging = false;
|
||||||
|
ImVec2 m_lastLookDragDelta = ImVec2(0.0f, 0.0f);
|
||||||
|
ImVec2 m_lastPanDragDelta = ImVec2(0.0f, 0.0f);
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,9 +5,14 @@ project(XCEngine_EditorTests)
|
|||||||
set(EDITOR_TEST_SOURCES
|
set(EDITOR_TEST_SOURCES
|
||||||
test_action_routing.cpp
|
test_action_routing.cpp
|
||||||
test_scene_viewport_camera_controller.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/Core/UndoManager.cpp
|
||||||
${CMAKE_SOURCE_DIR}/editor/src/Managers/SceneManager.cpp
|
${CMAKE_SOURCE_DIR}/editor/src/Managers/SceneManager.cpp
|
||||||
${CMAKE_SOURCE_DIR}/editor/src/Managers/ProjectManager.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})
|
add_executable(editor_tests ${EDITOR_TEST_SOURCES})
|
||||||
|
|||||||
@@ -41,6 +41,22 @@ TEST(SceneViewportCameraController_Test, ApplyToMatchesComputedPositionAndForwar
|
|||||||
EXPECT_GT(Vector3::Dot(cameraObject.GetTransform()->GetUp().Normalized(), Vector3::Up()), 0.0f);
|
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) {
|
TEST(SceneViewportCameraController_Test, LookInputRotatesCameraInPlaceAndKeepsDistance) {
|
||||||
SceneViewportCameraController controller;
|
SceneViewportCameraController controller;
|
||||||
controller.Reset();
|
controller.Reset();
|
||||||
@@ -57,7 +73,7 @@ TEST(SceneViewportCameraController_Test, LookInputRotatesCameraInPlaceAndKeepsDi
|
|||||||
|
|
||||||
EXPECT_TRUE(NearlyEqual(controller.GetPosition(), initialPosition));
|
EXPECT_TRUE(NearlyEqual(controller.GetPosition(), initialPosition));
|
||||||
EXPECT_FALSE(NearlyEqual(controller.GetFocalPoint(), initialFocus));
|
EXPECT_FALSE(NearlyEqual(controller.GetFocalPoint(), initialFocus));
|
||||||
EXPECT_LT(controller.GetPitchDegrees(), initialPitch);
|
EXPECT_GT(controller.GetPitchDegrees(), initialPitch);
|
||||||
EXPECT_TRUE(NearlyEqual(
|
EXPECT_TRUE(NearlyEqual(
|
||||||
controller.GetFocalPoint(),
|
controller.GetFocalPoint(),
|
||||||
controller.GetPosition() + controller.GetForward() * controller.GetDistance(),
|
controller.GetPosition() + controller.GetForward() * controller.GetDistance(),
|
||||||
@@ -81,7 +97,7 @@ TEST(SceneViewportCameraController_Test, OrbitInputRotatesAroundFocalPointAndKee
|
|||||||
|
|
||||||
EXPECT_FALSE(NearlyEqual(controller.GetPosition(), initialPosition));
|
EXPECT_FALSE(NearlyEqual(controller.GetPosition(), initialPosition));
|
||||||
EXPECT_TRUE(NearlyEqual(controller.GetFocalPoint(), initialFocus));
|
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);
|
EXPECT_NEAR((controller.GetFocalPoint() - controller.GetPosition()).Magnitude(), initialDistance, 1e-3f);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,8 +123,8 @@ TEST(SceneViewportCameraController_Test, PanAndZoomUpdateCameraStateConsistently
|
|||||||
EXPECT_LT(controller.GetDistance(), initialDistance);
|
EXPECT_LT(controller.GetDistance(), initialDistance);
|
||||||
const Vector3 panDelta = controller.GetFocalPoint() - initialFocus;
|
const Vector3 panDelta = controller.GetFocalPoint() - initialFocus;
|
||||||
EXPECT_NEAR(Vector3::Dot(panDelta, controller.GetForward()), 0.0f, 1e-3f);
|
EXPECT_NEAR(Vector3::Dot(panDelta, controller.GetForward()), 0.0f, 1e-3f);
|
||||||
EXPECT_GT(std::abs(Vector3::Dot(panDelta, right)), 0.0f);
|
EXPECT_LT(Vector3::Dot(panDelta, right), 0.0f);
|
||||||
EXPECT_GT(std::abs(Vector3::Dot(panDelta, up)), 0.0f);
|
EXPECT_LT(Vector3::Dot(panDelta, up), 0.0f);
|
||||||
EXPECT_TRUE(NearlyEqual(
|
EXPECT_TRUE(NearlyEqual(
|
||||||
controller.GetFocalPoint(),
|
controller.GetFocalPoint(),
|
||||||
controller.GetPosition() + controller.GetForward() * controller.GetDistance(),
|
controller.GetPosition() + controller.GetForward() * controller.GetDistance(),
|
||||||
@@ -135,11 +151,65 @@ TEST(SceneViewportCameraController_Test, FlyInputMovesCameraAndFocalPointTogethe
|
|||||||
EXPECT_FALSE(NearlyEqual(controller.GetPosition(), initialPosition));
|
EXPECT_FALSE(NearlyEqual(controller.GetPosition(), initialPosition));
|
||||||
EXPECT_FALSE(NearlyEqual(controller.GetFocalPoint(), initialFocus));
|
EXPECT_FALSE(NearlyEqual(controller.GetFocalPoint(), initialFocus));
|
||||||
const Vector3 positionDelta = controller.GetPosition() - initialPosition;
|
const Vector3 positionDelta = controller.GetPosition() - initialPosition;
|
||||||
EXPECT_GT(std::abs(Vector3::Dot(positionDelta, forward)), 0.0f);
|
EXPECT_GT(Vector3::Dot(positionDelta, forward), 0.0f);
|
||||||
EXPECT_GT(std::abs(Vector3::Dot(positionDelta, right)), 0.0f);
|
EXPECT_GT(Vector3::Dot(positionDelta, right), 0.0f);
|
||||||
EXPECT_TRUE(NearlyEqual(controller.GetFocalPoint() - controller.GetPosition(), initialOffset, 1e-3f));
|
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) {
|
TEST(SceneViewportCameraController_Test, FocusMovesPivotWithoutChangingDistance) {
|
||||||
SceneViewportCameraController controller;
|
SceneViewportCameraController controller;
|
||||||
controller.Reset();
|
controller.Reset();
|
||||||
|
|||||||
221
tests/editor/test_scene_viewport_overlay_renderer.cpp
Normal file
221
tests/editor/test_scene_viewport_overlay_renderer.cpp
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include "Viewport/SceneViewportCameraController.h"
|
||||||
|
#include "Viewport/SceneViewportGrid.h"
|
||||||
|
#include "Viewport/SceneViewportMath.h"
|
||||||
|
|
||||||
|
#include <XCEngine/Components/GameObject.h>
|
||||||
|
#include <XCEngine/Core/Math/Vector4.h>
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
182
tests/editor/test_scene_viewport_picker.cpp
Normal file
182
tests/editor/test_scene_viewport_picker.cpp
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include "Viewport/SceneViewportPicker.h"
|
||||||
|
|
||||||
|
#include <XCEngine/Components/MeshFilterComponent.h>
|
||||||
|
#include <XCEngine/Components/MeshRendererComponent.h>
|
||||||
|
#include <XCEngine/Core/Asset/IResource.h>
|
||||||
|
#include <XCEngine/Core/Math/Quaternion.h>
|
||||||
|
#include <XCEngine/Resources/Mesh/Mesh.h>
|
||||||
|
#include <XCEngine/Scene/Scene.h>
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
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<Mesh> CreateTriangleMesh(uint64_t guid) {
|
||||||
|
auto mesh = std::make_unique<Mesh>();
|
||||||
|
|
||||||
|
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<uint32_t>(std::size(vertices)),
|
||||||
|
sizeof(StaticMeshVertex),
|
||||||
|
XCEngine::Resources::VertexAttribute::Position);
|
||||||
|
mesh->SetIndexData(indices, sizeof(indices), static_cast<uint32_t>(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<uint32_t>(std::size(vertices));
|
||||||
|
section.startIndex = 0;
|
||||||
|
section.indexCount = static_cast<uint32_t>(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> 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<MeshFilterComponent>();
|
||||||
|
auto* meshRenderer = object->AddComponent<MeshRendererComponent>();
|
||||||
|
meshFilter->SetMesh(meshPtr);
|
||||||
|
meshRenderer->SetEnabled(true);
|
||||||
|
return object;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
uint64_t m_nextMeshGuid = 1;
|
||||||
|
std::vector<std::unique_ptr<Mesh>> 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
|
||||||
241
tests/editor/test_scene_viewport_selection_utils.cpp
Normal file
241
tests/editor/test_scene_viewport_selection_utils.cpp
Normal file
@@ -0,0 +1,241 @@
|
|||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include "Viewport/SceneViewportMath.h"
|
||||||
|
#include "Viewport/SceneViewportSelectionUtils.h"
|
||||||
|
|
||||||
|
#include <XCEngine/Components/CameraComponent.h>
|
||||||
|
#include <XCEngine/Components/MeshFilterComponent.h>
|
||||||
|
#include <XCEngine/Components/MeshRendererComponent.h>
|
||||||
|
#include <XCEngine/Core/Asset/IResource.h>
|
||||||
|
#include <XCEngine/Core/Math/Quaternion.h>
|
||||||
|
#include <XCEngine/Resources/Mesh/Mesh.h>
|
||||||
|
#include <XCEngine/Scene/Scene.h>
|
||||||
|
|
||||||
|
#include <cmath>
|
||||||
|
#include <memory>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
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<Mesh> CreateSelectionMesh(uint64_t guid, bool splitIntoTwoSections) {
|
||||||
|
auto mesh = std::make_unique<Mesh>();
|
||||||
|
|
||||||
|
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<uint32_t>(std::size(vertices)),
|
||||||
|
sizeof(StaticMeshVertex),
|
||||||
|
XCEngine::Resources::VertexAttribute::Position);
|
||||||
|
mesh->SetIndexData(indices, sizeof(indices), static_cast<uint32_t>(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> 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<MeshFilterComponent>();
|
||||||
|
auto* meshRenderer = object->AddComponent<MeshRendererComponent>();
|
||||||
|
meshFilter->SetMesh(meshPtr);
|
||||||
|
meshFilter->SetEnabled(enabled);
|
||||||
|
meshRenderer->SetEnabled(enabled);
|
||||||
|
return object;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
uint64_t m_nextMeshGuid = 1;
|
||||||
|
std::vector<std::unique_ptr<Mesh>> 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<CameraComponent>();
|
||||||
|
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<uint64_t> 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<MeshRendererComponent>());
|
||||||
|
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
|
||||||
Reference in New Issue
Block a user