Refine editor viewport and interaction workflow
This commit is contained in:
@@ -1,9 +1,14 @@
|
||||
cmake_minimum_required(VERSION 3.15)
|
||||
project(XCVolumeRendererUI2 VERSION 1.0 LANGUAGES CXX)
|
||||
project(XCEditor VERSION 1.0 LANGUAGES CXX)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
get_filename_component(XCENGINE_ROOT_DIR "${CMAKE_CURRENT_SOURCE_DIR}/.." ABSOLUTE)
|
||||
set(XCENGINE_ENGINE_DIR "${XCENGINE_ROOT_DIR}/engine")
|
||||
set(XCENGINE_ASSIMP_DLL "${XCENGINE_ENGINE_DIR}/third_party/assimp/bin/assimp-vc143-mt.dll")
|
||||
set(XCENGINE_ASSIMP_LIB "${XCENGINE_ENGINE_DIR}/third_party/assimp/lib/assimp-vc143-mt.lib")
|
||||
|
||||
add_definitions(-DUNICODE -D_UNICODE)
|
||||
add_definitions(-DIMGUI_ENABLE_DOCKING)
|
||||
|
||||
@@ -18,6 +23,37 @@ FetchContent_Declare(
|
||||
|
||||
FetchContent_MakeAvailable(imgui)
|
||||
|
||||
if(NOT TARGET XCEngine)
|
||||
set(XCENGINE_VULKAN_SDK_HINT "$ENV{VULKAN_SDK}")
|
||||
if(NOT EXISTS "${XCENGINE_VULKAN_SDK_HINT}/Lib/vulkan-1.lib")
|
||||
file(GLOB XCENGINE_VULKAN_SDK_DIRS "D:/VulkanSDK/*")
|
||||
if(XCENGINE_VULKAN_SDK_DIRS)
|
||||
list(SORT XCENGINE_VULKAN_SDK_DIRS COMPARE NATURAL ORDER DESCENDING)
|
||||
list(GET XCENGINE_VULKAN_SDK_DIRS 0 XCENGINE_VULKAN_SDK_HINT)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(EXISTS "${XCENGINE_VULKAN_SDK_HINT}/Lib/vulkan-1.lib")
|
||||
set(Vulkan_ROOT "${XCENGINE_VULKAN_SDK_HINT}")
|
||||
list(APPEND CMAKE_PREFIX_PATH "${XCENGINE_VULKAN_SDK_HINT}")
|
||||
endif()
|
||||
|
||||
find_package(Vulkan REQUIRED)
|
||||
|
||||
add_library(XCEngine STATIC IMPORTED GLOBAL)
|
||||
|
||||
set_target_properties(XCEngine PROPERTIES
|
||||
IMPORTED_CONFIGURATIONS "Debug;Release;RelWithDebInfo;MinSizeRel"
|
||||
IMPORTED_LOCATION_DEBUG "${XCENGINE_ENGINE_DIR}/build/Debug/XCEngine.lib"
|
||||
IMPORTED_LOCATION_RELEASE "${XCENGINE_ENGINE_DIR}/build/Release/XCEngine.lib"
|
||||
IMPORTED_LOCATION_RELWITHDEBINFO "${XCENGINE_ENGINE_DIR}/build/Release/XCEngine.lib"
|
||||
IMPORTED_LOCATION_MINSIZEREL "${XCENGINE_ENGINE_DIR}/build/Release/XCEngine.lib"
|
||||
INTERFACE_INCLUDE_DIRECTORIES
|
||||
"${XCENGINE_ENGINE_DIR}/include;${XCENGINE_ENGINE_DIR}/include/XCEngine;${XCENGINE_ENGINE_DIR}/src;${XCENGINE_ENGINE_DIR}/third_party;${XCENGINE_ENGINE_DIR}/third_party/GLAD/include;${XCENGINE_ENGINE_DIR}/third_party/stb;${XCENGINE_ENGINE_DIR}/third_party/assimp/include"
|
||||
INTERFACE_LINK_LIBRARIES
|
||||
"${XCENGINE_ASSIMP_LIB};opengl32;Vulkan::Vulkan")
|
||||
endif()
|
||||
|
||||
set(IMGUI_SOURCES
|
||||
${imgui_SOURCE_DIR}/imgui.cpp
|
||||
${imgui_SOURCE_DIR}/imgui_demo.cpp
|
||||
@@ -42,6 +78,12 @@ add_executable(${PROJECT_NAME} WIN32
|
||||
src/panels/MenuBar.cpp
|
||||
src/panels/HierarchyPanel.cpp
|
||||
src/panels/SceneViewPanel.cpp
|
||||
src/Viewport/SceneViewportPicker.cpp
|
||||
src/Viewport/SceneViewportGrid.cpp
|
||||
src/Viewport/SceneViewportInfiniteGridPass.cpp
|
||||
src/Viewport/SceneViewportSelectionMaskPass.cpp
|
||||
src/Viewport/SceneViewportSelectionOutlinePass.cpp
|
||||
src/Viewport/SceneViewportOverlayRenderer.cpp
|
||||
src/panels/GameViewPanel.cpp
|
||||
src/panels/InspectorPanel.cpp
|
||||
src/panels/ConsolePanel.cpp
|
||||
@@ -69,6 +111,7 @@ endif()
|
||||
target_link_libraries(${PROJECT_NAME} PRIVATE
|
||||
XCEngine
|
||||
d3d12.lib
|
||||
Dbghelp.lib
|
||||
dxgi.lib
|
||||
d3dcompiler.lib
|
||||
Ole32.lib
|
||||
@@ -83,6 +126,6 @@ set_target_properties(${PROJECT_NAME} PROPERTIES
|
||||
|
||||
add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
${CMAKE_SOURCE_DIR}/engine/third_party/assimp/bin/assimp-vc143-mt.dll
|
||||
${XCENGINE_ASSIMP_DLL}
|
||||
$<TARGET_FILE_DIR:${PROJECT_NAME}>/assimp-vc143-mt.dll
|
||||
)
|
||||
|
||||
@@ -7,10 +7,21 @@
|
||||
#include "Core/IEditorContext.h"
|
||||
#include "UI/PopupState.h"
|
||||
|
||||
#include <XCEngine/Debug/Logger.h>
|
||||
|
||||
#include <imgui.h>
|
||||
#include <string>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Editor {
|
||||
namespace Actions {
|
||||
|
||||
inline void TraceHierarchyPopup(const std::string& message) {
|
||||
XCEngine::Debug::Logger::Get().Info(
|
||||
XCEngine::Debug::LogCategory::General,
|
||||
XCEngine::Containers::String(message.c_str()));
|
||||
}
|
||||
|
||||
inline constexpr const char* HierarchyEntityPayloadType() {
|
||||
return "ENTITY_PTR";
|
||||
}
|
||||
@@ -60,11 +71,64 @@ inline void RequestHierarchyOptionsPopup(UI::DeferredPopupState& optionsPopup) {
|
||||
}
|
||||
|
||||
inline void RequestHierarchyBackgroundContextPopup(UI::DeferredPopupState& backgroundContextMenu) {
|
||||
if (ImGui::IsWindowHovered() && ImGui::IsMouseClicked(1) && !ImGui::IsAnyItemHovered()) {
|
||||
const bool windowHovered = ImGui::IsWindowHovered();
|
||||
const bool rightClicked = ImGui::IsMouseClicked(1);
|
||||
const bool anyItemHovered = ImGui::IsAnyItemHovered();
|
||||
if (rightClicked) {
|
||||
TraceHierarchyPopup(
|
||||
std::string("Hierarchy background RMB: windowHovered=") +
|
||||
(windowHovered ? "1" : "0") +
|
||||
", anyItemHovered=" +
|
||||
(anyItemHovered ? "1" : "0"));
|
||||
}
|
||||
|
||||
if (windowHovered && rightClicked && !anyItemHovered) {
|
||||
TraceHierarchyPopup("Hierarchy background popup requested");
|
||||
backgroundContextMenu.RequestOpen();
|
||||
}
|
||||
}
|
||||
|
||||
inline bool AcceptHierarchyEntityDropToRoot(IEditorContext& context) {
|
||||
if (!ImGui::BeginDragDropTarget()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool accepted = false;
|
||||
if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload(HierarchyEntityPayloadType())) {
|
||||
::XCEngine::Components::GameObject* sourceGameObject = *(::XCEngine::Components::GameObject**)payload->Data;
|
||||
if (sourceGameObject && sourceGameObject->GetParent() != nullptr) {
|
||||
Commands::ReparentEntityPreserveWorldTransform(context, sourceGameObject, 0);
|
||||
accepted = true;
|
||||
}
|
||||
}
|
||||
ImGui::EndDragDropTarget();
|
||||
return accepted;
|
||||
}
|
||||
|
||||
inline void DrawHierarchyRootDropTarget(IEditorContext& context) {
|
||||
ImGui::InvisibleButton("##DragTarget", ImVec2(-1, -1));
|
||||
AcceptHierarchyEntityDropToRoot(context);
|
||||
}
|
||||
|
||||
template <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>
|
||||
inline void DrawHierarchySortOptionsPopup(
|
||||
UI::DeferredPopupState& optionsPopup,
|
||||
@@ -103,6 +167,144 @@ inline void DrawHierarchySortOptionsPopup(
|
||||
UI::EndPopup();
|
||||
}
|
||||
|
||||
inline void DrawHierarchyCreateActions(IEditorContext& context, ::XCEngine::Components::GameObject* parent) {
|
||||
DrawMenuAction(MakeCreateEmptyEntityAction(), [&]() {
|
||||
TraceHierarchyPopup("Hierarchy create clicked: Empty Object");
|
||||
auto* created = Commands::CreateEmptyEntity(context, parent, "Create Entity", "GameObject");
|
||||
TraceHierarchyPopup(
|
||||
std::string("Hierarchy create result: Empty Object, createdId=") +
|
||||
std::to_string(created ? created->GetID() : 0));
|
||||
});
|
||||
DrawMenuSeparator();
|
||||
DrawMenuAction(MakeCreateCameraEntityAction(), [&]() {
|
||||
TraceHierarchyPopup("Hierarchy create clicked: Camera");
|
||||
auto* created = Commands::CreateCameraEntity(context, parent);
|
||||
TraceHierarchyPopup(
|
||||
std::string("Hierarchy create result: Camera, createdId=") +
|
||||
std::to_string(created ? created->GetID() : 0));
|
||||
});
|
||||
DrawMenuAction(MakeCreateLightEntityAction(), [&]() {
|
||||
TraceHierarchyPopup("Hierarchy create clicked: Light");
|
||||
auto* created = Commands::CreateLightEntity(context, parent);
|
||||
TraceHierarchyPopup(
|
||||
std::string("Hierarchy create result: Light, createdId=") +
|
||||
std::to_string(created ? created->GetID() : 0));
|
||||
});
|
||||
DrawMenuSeparator();
|
||||
DrawMenuAction(MakeCreateCubeEntityAction(), [&]() {
|
||||
TraceHierarchyPopup("Hierarchy create clicked: Cube");
|
||||
auto* created = Commands::CreateEmptyEntity(context, parent, "Create Cube", "Cube");
|
||||
TraceHierarchyPopup(
|
||||
std::string("Hierarchy create result: Cube, createdId=") +
|
||||
std::to_string(created ? created->GetID() : 0));
|
||||
});
|
||||
DrawMenuAction(MakeCreateSphereEntityAction(), [&]() {
|
||||
TraceHierarchyPopup("Hierarchy create clicked: Sphere");
|
||||
auto* created = Commands::CreateEmptyEntity(context, parent, "Create Sphere", "Sphere");
|
||||
TraceHierarchyPopup(
|
||||
std::string("Hierarchy create result: Sphere, createdId=") +
|
||||
std::to_string(created ? created->GetID() : 0));
|
||||
});
|
||||
DrawMenuAction(MakeCreatePlaneEntityAction(), [&]() {
|
||||
TraceHierarchyPopup("Hierarchy create clicked: Plane");
|
||||
auto* created = Commands::CreateEmptyEntity(context, parent, "Create Plane", "Plane");
|
||||
TraceHierarchyPopup(
|
||||
std::string("Hierarchy create result: Plane, createdId=") +
|
||||
std::to_string(created ? created->GetID() : 0));
|
||||
});
|
||||
}
|
||||
|
||||
inline void HandleHierarchyItemContextRequest(
|
||||
IEditorContext& context,
|
||||
::XCEngine::Components::GameObject* gameObject,
|
||||
UI::TargetedPopupState<::XCEngine::Components::GameObject*>& itemContextMenu) {
|
||||
if (!gameObject) {
|
||||
return;
|
||||
}
|
||||
|
||||
TraceHierarchyPopup(
|
||||
std::string("Hierarchy item popup requested for entityId=") +
|
||||
std::to_string(gameObject->GetID()) +
|
||||
", name=" +
|
||||
gameObject->GetName());
|
||||
context.GetSelectionManager().SetSelectedEntity(gameObject->GetID());
|
||||
itemContextMenu.RequestOpen(gameObject);
|
||||
}
|
||||
|
||||
inline void DrawHierarchyBackgroundContextPopup(IEditorContext& context, UI::DeferredPopupState& backgroundContextMenu) {
|
||||
backgroundContextMenu.ConsumeOpenRequest("HierarchyContextMenu");
|
||||
static bool s_lastBackgroundPopupOpen = false;
|
||||
|
||||
if (!UI::BeginPopup("HierarchyContextMenu")) {
|
||||
if (s_lastBackgroundPopupOpen) {
|
||||
TraceHierarchyPopup("Hierarchy background popup closed");
|
||||
s_lastBackgroundPopupOpen = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!s_lastBackgroundPopupOpen) {
|
||||
TraceHierarchyPopup("Hierarchy background popup opened");
|
||||
s_lastBackgroundPopupOpen = true;
|
||||
}
|
||||
|
||||
DrawHierarchyCreateActions(context, nullptr);
|
||||
UI::EndPopup();
|
||||
}
|
||||
|
||||
inline void DrawHierarchyContextActions(IEditorContext& context, ::XCEngine::Components::GameObject* gameObject) {
|
||||
DrawMenuAction(MakeDetachEntityAction(gameObject), [&]() {
|
||||
Commands::DetachEntity(context, gameObject);
|
||||
});
|
||||
DrawMenuAction(MakeRenameEntityAction(gameObject), [&]() {
|
||||
RequestEntityRename(context, gameObject);
|
||||
});
|
||||
DrawMenuAction(MakeDeleteEntityAction(gameObject), [&]() {
|
||||
Commands::DeleteEntity(context, gameObject->GetID());
|
||||
});
|
||||
DrawMenuSeparator();
|
||||
DrawMenuAction(MakeCopyEntityAction(gameObject), [&]() {
|
||||
Commands::CopyEntity(context, gameObject->GetID());
|
||||
});
|
||||
DrawMenuAction(MakePasteEntityAction(context), [&]() {
|
||||
Commands::PasteEntity(context, gameObject->GetID());
|
||||
});
|
||||
DrawMenuAction(MakeDuplicateEntityAction(gameObject), [&]() {
|
||||
Commands::DuplicateEntity(context, gameObject->GetID());
|
||||
});
|
||||
DrawMenuSeparator();
|
||||
DrawHierarchyCreateActions(context, gameObject);
|
||||
}
|
||||
|
||||
inline void DrawHierarchyEntityContextPopup(
|
||||
IEditorContext& context,
|
||||
UI::TargetedPopupState<::XCEngine::Components::GameObject*>& itemContextMenu) {
|
||||
itemContextMenu.ConsumeOpenRequest("HierarchyEntityContextMenu");
|
||||
static bool s_lastEntityPopupOpen = false;
|
||||
|
||||
if (!UI::BeginPopup("HierarchyEntityContextMenu")) {
|
||||
if (s_lastEntityPopupOpen) {
|
||||
TraceHierarchyPopup("Hierarchy entity popup closed");
|
||||
s_lastEntityPopupOpen = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!s_lastEntityPopupOpen) {
|
||||
TraceHierarchyPopup("Hierarchy entity popup opened");
|
||||
s_lastEntityPopupOpen = true;
|
||||
}
|
||||
|
||||
if (itemContextMenu.HasTarget()) {
|
||||
DrawHierarchyContextActions(context, itemContextMenu.TargetValue());
|
||||
}
|
||||
UI::EndPopup();
|
||||
|
||||
if (!ImGui::IsPopupOpen("HierarchyEntityContextMenu") && !itemContextMenu.HasPendingOpenRequest()) {
|
||||
itemContextMenu.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
inline bool BeginHierarchyEntityDrag(::XCEngine::Components::GameObject* gameObject) {
|
||||
if (!gameObject || !ImGui::BeginDragDropSource(ImGuiDragDropFlags_None)) {
|
||||
return false;
|
||||
@@ -131,124 +333,6 @@ inline bool AcceptHierarchyEntityDrop(IEditorContext& context, ::XCEngine::Compo
|
||||
return accepted;
|
||||
}
|
||||
|
||||
inline bool AcceptHierarchyEntityDropToRoot(IEditorContext& context) {
|
||||
if (!ImGui::BeginDragDropTarget()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool accepted = false;
|
||||
if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload(HierarchyEntityPayloadType())) {
|
||||
::XCEngine::Components::GameObject* sourceGameObject = *(::XCEngine::Components::GameObject**)payload->Data;
|
||||
if (sourceGameObject && sourceGameObject->GetParent() != nullptr) {
|
||||
Commands::ReparentEntityPreserveWorldTransform(context, sourceGameObject, 0);
|
||||
accepted = true;
|
||||
}
|
||||
}
|
||||
ImGui::EndDragDropTarget();
|
||||
return accepted;
|
||||
}
|
||||
|
||||
inline void DrawHierarchyRootDropTarget(IEditorContext& context) {
|
||||
ImGui::InvisibleButton("##DragTarget", ImVec2(-1, -1));
|
||||
AcceptHierarchyEntityDropToRoot(context);
|
||||
}
|
||||
|
||||
inline void DrawHierarchyCreateActions(IEditorContext& context, ::XCEngine::Components::GameObject* parent) {
|
||||
DrawMenuAction(MakeCreateEmptyEntityAction(), [&]() {
|
||||
Commands::CreateEmptyEntity(context, parent, "Create Entity", "GameObject");
|
||||
});
|
||||
DrawMenuSeparator();
|
||||
DrawMenuAction(MakeCreateCameraEntityAction(), [&]() {
|
||||
Commands::CreateCameraEntity(context, parent);
|
||||
});
|
||||
DrawMenuAction(MakeCreateLightEntityAction(), [&]() {
|
||||
Commands::CreateLightEntity(context, parent);
|
||||
});
|
||||
DrawMenuSeparator();
|
||||
DrawMenuAction(MakeCreateCubeEntityAction(), [&]() {
|
||||
Commands::CreateEmptyEntity(context, parent, "Create Cube", "Cube");
|
||||
});
|
||||
DrawMenuAction(MakeCreateSphereEntityAction(), [&]() {
|
||||
Commands::CreateEmptyEntity(context, parent, "Create Sphere", "Sphere");
|
||||
});
|
||||
DrawMenuAction(MakeCreatePlaneEntityAction(), [&]() {
|
||||
Commands::CreateEmptyEntity(context, parent, "Create Plane", "Plane");
|
||||
});
|
||||
}
|
||||
|
||||
inline void HandleHierarchyItemContextRequest(
|
||||
IEditorContext& context,
|
||||
::XCEngine::Components::GameObject* gameObject,
|
||||
UI::TargetedPopupState<::XCEngine::Components::GameObject*>& itemContextMenu) {
|
||||
if (!gameObject) {
|
||||
return;
|
||||
}
|
||||
|
||||
context.GetSelectionManager().SetSelectedEntity(gameObject->GetID());
|
||||
itemContextMenu.RequestOpen(gameObject);
|
||||
}
|
||||
|
||||
inline void DrawHierarchyBackgroundContextPopup(IEditorContext& context, UI::DeferredPopupState& backgroundContextMenu) {
|
||||
backgroundContextMenu.ConsumeOpenRequest("HierarchyContextMenu");
|
||||
|
||||
if (!UI::BeginPopup("HierarchyContextMenu")) {
|
||||
return;
|
||||
}
|
||||
|
||||
DrawHierarchyCreateActions(context, nullptr);
|
||||
UI::EndPopup();
|
||||
}
|
||||
|
||||
inline void DrawHierarchyContextActions(IEditorContext& context, ::XCEngine::Components::GameObject* gameObject) {
|
||||
if (UI::DrawPopupSubmenuScope("Create", [&]() {
|
||||
DrawHierarchyCreateActions(context, gameObject);
|
||||
})) {
|
||||
}
|
||||
|
||||
DrawMenuAction(MakeCreateChildEntityAction(gameObject), [&]() {
|
||||
Commands::CreateEmptyEntity(context, gameObject, "Create Child", "GameObject");
|
||||
});
|
||||
DrawMenuSeparator();
|
||||
DrawMenuAction(MakeDetachEntityAction(gameObject), [&]() {
|
||||
Commands::DetachEntity(context, gameObject);
|
||||
});
|
||||
DrawMenuAction(MakeRenameEntityAction(gameObject), [&]() {
|
||||
RequestEntityRename(context, gameObject);
|
||||
});
|
||||
DrawMenuAction(MakeDeleteEntityAction(gameObject), [&]() {
|
||||
Commands::DeleteEntity(context, gameObject->GetID());
|
||||
});
|
||||
DrawMenuSeparator();
|
||||
DrawMenuAction(MakeCopyEntityAction(gameObject), [&]() {
|
||||
Commands::CopyEntity(context, gameObject->GetID());
|
||||
});
|
||||
DrawMenuAction(MakePasteEntityAction(context), [&]() {
|
||||
Commands::PasteEntity(context, gameObject->GetID());
|
||||
});
|
||||
DrawMenuAction(MakeDuplicateEntityAction(gameObject), [&]() {
|
||||
Commands::DuplicateEntity(context, gameObject->GetID());
|
||||
});
|
||||
}
|
||||
|
||||
inline void DrawHierarchyEntityContextPopup(
|
||||
IEditorContext& context,
|
||||
UI::TargetedPopupState<::XCEngine::Components::GameObject*>& itemContextMenu) {
|
||||
itemContextMenu.ConsumeOpenRequest("HierarchyEntityContextMenu");
|
||||
|
||||
if (!UI::BeginPopup("HierarchyEntityContextMenu")) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (itemContextMenu.HasTarget()) {
|
||||
DrawHierarchyContextActions(context, itemContextMenu.TargetValue());
|
||||
}
|
||||
UI::EndPopup();
|
||||
|
||||
if (!ImGui::IsPopupOpen("HierarchyEntityContextMenu") && !itemContextMenu.HasPendingOpenRequest()) {
|
||||
itemContextMenu.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Actions
|
||||
} // namespace Editor
|
||||
} // namespace XCEngine
|
||||
|
||||
@@ -90,17 +90,6 @@ inline void DrawInspectorAddComponentPopup(
|
||||
UI::EndTitledPopup();
|
||||
}
|
||||
|
||||
inline bool DrawInspectorComponentMenu(
|
||||
IEditorContext& context,
|
||||
::XCEngine::Components::Component* component,
|
||||
::XCEngine::Components::GameObject* gameObject,
|
||||
const IComponentEditor* editor) {
|
||||
const bool canRemove = Commands::CanRemoveComponent(component, editor);
|
||||
return DrawMenuAction(MakeRemoveComponentAction(canRemove), [&]() {
|
||||
Commands::RemoveComponent(context, component, gameObject, editor);
|
||||
});
|
||||
}
|
||||
|
||||
inline void FinalizeInspectorInteractiveChangeIfIdle(IEditorContext& context) {
|
||||
if (context.GetUndoManager().HasPendingInteractiveChange() && !ImGui::IsAnyItemActive()) {
|
||||
context.GetUndoManager().FinalizeInteractiveChange();
|
||||
|
||||
@@ -15,8 +15,6 @@ inline constexpr const char* ProjectAssetPayloadType() {
|
||||
return "ASSET_ITEM";
|
||||
}
|
||||
|
||||
inline void DrawProjectAssetContextActions(IEditorContext& context, const AssetItemPtr& item);
|
||||
|
||||
inline const char* GetDraggedProjectAssetPath() {
|
||||
const ImGuiPayload* payload = ImGui::GetDragDropPayload();
|
||||
if (!payload || !payload->IsDataType(ProjectAssetPayloadType())) {
|
||||
@@ -71,16 +69,16 @@ inline void HandleProjectBackgroundPrimaryClick(
|
||||
}
|
||||
}
|
||||
|
||||
inline void HandleProjectItemSelection(IProjectManager& projectManager, const AssetItemPtr& item) {
|
||||
projectManager.SetSelectedItem(item);
|
||||
}
|
||||
|
||||
inline void RequestProjectEmptyContextPopup(UI::DeferredPopupState& emptyContextMenu) {
|
||||
if (ImGui::IsWindowHovered() && ImGui::IsMouseClicked(1) && !ImGui::IsAnyItemHovered()) {
|
||||
emptyContextMenu.RequestOpen();
|
||||
}
|
||||
}
|
||||
|
||||
inline void HandleProjectItemSelection(IProjectManager& projectManager, const AssetItemPtr& item) {
|
||||
projectManager.SetSelectedItem(item);
|
||||
}
|
||||
|
||||
inline void HandleProjectItemContextRequest(
|
||||
IProjectManager& projectManager,
|
||||
const AssetItemPtr& item,
|
||||
@@ -89,50 +87,6 @@ inline void HandleProjectItemContextRequest(
|
||||
itemContextMenu.RequestOpen(item);
|
||||
}
|
||||
|
||||
inline void DrawProjectItemContextPopup(IEditorContext& context, UI::TargetedPopupState<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 Editor
|
||||
} // namespace XCEngine
|
||||
|
||||
@@ -135,14 +135,19 @@ bool Application::Initialize(HWND hwnd) {
|
||||
}
|
||||
|
||||
m_hwnd = hwnd;
|
||||
logger.Info(Debug::LogCategory::General, "Initializing editor window renderer...");
|
||||
|
||||
if (!InitializeWindowRenderer(hwnd)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
logger.Info(Debug::LogCategory::General, "Initializing editor context...");
|
||||
InitializeEditorContext(projectRoot);
|
||||
logger.Info(Debug::LogCategory::General, "Initializing ImGui backend...");
|
||||
InitializeImGui(hwnd);
|
||||
logger.Info(Debug::LogCategory::General, "Attaching editor layer...");
|
||||
AttachEditorLayer();
|
||||
logger.Info(Debug::LogCategory::General, "Editor initialization completed.");
|
||||
m_renderReady = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "Platform/Win32Utf8.h"
|
||||
|
||||
#include <dbghelp.h>
|
||||
#include <stdio.h>
|
||||
#include <string>
|
||||
#include <windows.h>
|
||||
@@ -14,6 +15,49 @@ inline std::string GetExecutableLogPath(const char* fileName) {
|
||||
return GetExecutableDirectoryUtf8() + "\\" + fileName;
|
||||
}
|
||||
|
||||
inline void WriteCrashStackTrace(FILE* file) {
|
||||
if (file == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
HANDLE process = GetCurrentProcess();
|
||||
SymSetOptions(SYMOPT_LOAD_LINES | SYMOPT_UNDNAME | SYMOPT_DEFERRED_LOADS);
|
||||
if (!SymInitialize(process, nullptr, TRUE)) {
|
||||
fprintf(file, "[CRASH] SymInitialize failed: %lu\n", GetLastError());
|
||||
return;
|
||||
}
|
||||
|
||||
void* frames[64] = {};
|
||||
const USHORT frameCount = CaptureStackBackTrace(0, 64, frames, nullptr);
|
||||
|
||||
char symbolStorage[sizeof(SYMBOL_INFO) + MAX_SYM_NAME] = {};
|
||||
SYMBOL_INFO* symbol = reinterpret_cast<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) {
|
||||
const std::string logPath = GetExecutableLogPath("crash.log");
|
||||
|
||||
@@ -25,6 +69,7 @@ inline LONG WINAPI CrashExceptionFilter(EXCEPTION_POINTERS* exceptionPointers) {
|
||||
"[CRASH] ExceptionCode=0x%08X, Address=0x%p\n",
|
||||
exceptionPointers->ExceptionRecord->ExceptionCode,
|
||||
exceptionPointers->ExceptionRecord->ExceptionAddress);
|
||||
WriteCrashStackTrace(file);
|
||||
fclose(file);
|
||||
}
|
||||
|
||||
|
||||
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 "StyleTokens.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <imgui.h>
|
||||
|
||||
namespace XCEngine {
|
||||
@@ -38,27 +39,41 @@ inline void DrawDisclosureArrow(ImDrawList* drawList, const ImVec2& min, const I
|
||||
return;
|
||||
}
|
||||
|
||||
const ImVec2 center((min.x + max.x) * 0.5f, (min.y + max.y) * 0.5f);
|
||||
const ImVec2 center(
|
||||
static_cast<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 height = max.y - min.y;
|
||||
const float size = (width < height ? width : height) * DisclosureArrowScale();
|
||||
if (size <= 0.0f) {
|
||||
const float radius = (std::max)(
|
||||
3.0f,
|
||||
static_cast<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;
|
||||
}
|
||||
|
||||
if (open) {
|
||||
drawList->AddTriangleFilled(
|
||||
ImVec2(center.x - size, center.y - size * 0.45f),
|
||||
ImVec2(center.x + size, center.y - size * 0.45f),
|
||||
ImVec2(center.x, center.y + size),
|
||||
color);
|
||||
return;
|
||||
ImVec2 points[3] = {
|
||||
ImVec2(-baseHalfExtent, -tipExtent),
|
||||
ImVec2(baseHalfExtent, -tipExtent),
|
||||
ImVec2(0.0f, tipExtent)
|
||||
};
|
||||
|
||||
if (!open) {
|
||||
for (ImVec2& point : points) {
|
||||
point = ImVec2(point.y, -point.x);
|
||||
}
|
||||
}
|
||||
|
||||
for (ImVec2& point : points) {
|
||||
point.x += center.x;
|
||||
point.y += center.y;
|
||||
}
|
||||
|
||||
drawList->AddTriangleFilled(
|
||||
ImVec2(center.x - size * 0.45f, center.y - size),
|
||||
ImVec2(center.x - size * 0.45f, center.y + size),
|
||||
ImVec2(center.x + size, center.y),
|
||||
points[0],
|
||||
points[1],
|
||||
points[2],
|
||||
color);
|
||||
}
|
||||
|
||||
|
||||
@@ -120,7 +120,6 @@ private:
|
||||
}
|
||||
|
||||
io.FontDefault = uiFont;
|
||||
atlas->Build();
|
||||
}
|
||||
|
||||
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 UI {
|
||||
|
||||
inline float PopupWindowBorderSize();
|
||||
|
||||
inline ImVec2 DockHostFramePadding() {
|
||||
return ImVec2(4.0f, 2.0f);
|
||||
}
|
||||
@@ -246,6 +248,10 @@ inline float CompactNavigationTreeIndentSpacing() {
|
||||
return 14.0f;
|
||||
}
|
||||
|
||||
inline float HierarchyTreeIndentSpacing() {
|
||||
return 18.0f;
|
||||
}
|
||||
|
||||
inline float NavigationTreeIconSize() {
|
||||
return 18.0f;
|
||||
}
|
||||
@@ -263,7 +269,7 @@ inline float NavigationTreePrefixLabelGap() {
|
||||
}
|
||||
|
||||
inline float DisclosureArrowScale() {
|
||||
return 0.14f;
|
||||
return 0.28f;
|
||||
}
|
||||
|
||||
inline ImVec4 NavigationTreePrefixColor(bool selected = false, bool hovered = false) {
|
||||
@@ -295,7 +301,9 @@ inline TreeViewStyle NavigationTreeStyle() {
|
||||
}
|
||||
|
||||
inline TreeViewStyle HierarchyTreeStyle() {
|
||||
return NavigationTreeStyle();
|
||||
TreeViewStyle style = NavigationTreeStyle();
|
||||
style.indentSpacing = HierarchyTreeIndentSpacing();
|
||||
return style;
|
||||
}
|
||||
|
||||
inline TreeViewStyle ProjectFolderTreeStyle() {
|
||||
@@ -338,6 +346,18 @@ inline float SearchFieldFrameRounding() {
|
||||
return 2.0f;
|
||||
}
|
||||
|
||||
inline ImVec2 InlineRenameFieldFramePadding() {
|
||||
return ImVec2(4.0f, 0.0f);
|
||||
}
|
||||
|
||||
inline float InlineRenameFieldRounding() {
|
||||
return 2.0f;
|
||||
}
|
||||
|
||||
inline float InlineRenameFieldHeight() {
|
||||
return ImGui::GetFontSize() + InlineRenameFieldFramePadding().y * 2.0f;
|
||||
}
|
||||
|
||||
inline ImU32 PanelDividerColor() {
|
||||
return ImGui::GetColorU32(PanelSplitterIdleColor());
|
||||
}
|
||||
@@ -476,6 +496,18 @@ inline float PopupFrameRounding() {
|
||||
return 3.0f;
|
||||
}
|
||||
|
||||
inline ImVec2 ContextMenuItemSpacing() {
|
||||
return ImVec2(0.0f, 0.0f);
|
||||
}
|
||||
|
||||
inline ImVec2 ContextMenuFramePadding() {
|
||||
return ImVec2(8.0f, 5.0f);
|
||||
}
|
||||
|
||||
inline ImVec2 ContextMenuSelectableTextAlign() {
|
||||
return ImVec2(0.0f, 0.5f);
|
||||
}
|
||||
|
||||
inline float PopupSubmenuArrowExtent() {
|
||||
return 8.0f;
|
||||
}
|
||||
|
||||
@@ -5,10 +5,12 @@
|
||||
#include "BuiltInIcons.h"
|
||||
#include "ConsoleFilterState.h"
|
||||
#include "ConsoleLogFormatter.h"
|
||||
#include "ContextMenu.h"
|
||||
#include "Core.h"
|
||||
#include "DockHostStyle.h"
|
||||
#include "DockTabBarChrome.h"
|
||||
#include "DividerChrome.h"
|
||||
#include "MenuCommand.h"
|
||||
#include "PanelChrome.h"
|
||||
#include "PopupState.h"
|
||||
#include "PropertyLayout.h"
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#include "Core.h"
|
||||
#include "MenuCommand.h"
|
||||
#include "StyleTokens.h"
|
||||
|
||||
#include <XCEngine/Debug/Logger.h>
|
||||
|
||||
#include <imgui.h>
|
||||
#include <string>
|
||||
|
||||
@@ -10,6 +13,16 @@ namespace XCEngine {
|
||||
namespace Editor {
|
||||
namespace UI {
|
||||
|
||||
inline void TracePopupSubmenuIfNeeded(const char* label, const std::string& message) {
|
||||
if (!label || std::string(label) != "Create") {
|
||||
return;
|
||||
}
|
||||
|
||||
XCEngine::Debug::Logger::Get().Info(
|
||||
XCEngine::Debug::LogCategory::General,
|
||||
XCEngine::Containers::String(message.c_str()));
|
||||
}
|
||||
|
||||
struct ComponentSectionResult {
|
||||
bool open = false;
|
||||
float contentIndent = 0.0f;
|
||||
@@ -17,7 +30,6 @@ struct ComponentSectionResult {
|
||||
|
||||
struct AssetTileResult {
|
||||
bool clicked = false;
|
||||
bool contextRequested = false;
|
||||
bool openRequested = false;
|
||||
bool hovered = false;
|
||||
ImVec2 min = ImVec2(0.0f, 0.0f);
|
||||
@@ -41,29 +53,11 @@ enum class DialogActionResult {
|
||||
Secondary
|
||||
};
|
||||
|
||||
enum class MenuCommandKind {
|
||||
Action,
|
||||
Separator
|
||||
};
|
||||
|
||||
struct MenuCommand {
|
||||
MenuCommandKind kind = MenuCommandKind::Action;
|
||||
const char* label = nullptr;
|
||||
const char* shortcut = nullptr;
|
||||
bool selected = false;
|
||||
bool enabled = true;
|
||||
|
||||
static MenuCommand Action(
|
||||
const char* label,
|
||||
const char* shortcut = nullptr,
|
||||
bool selected = false,
|
||||
bool enabled = true) {
|
||||
return MenuCommand{ MenuCommandKind::Action, label, shortcut, selected, enabled };
|
||||
}
|
||||
|
||||
static MenuCommand Separator() {
|
||||
return MenuCommand{ MenuCommandKind::Separator, nullptr, nullptr, false, true };
|
||||
}
|
||||
struct InlineRenameFieldResult {
|
||||
bool submitted = false;
|
||||
bool cancelRequested = false;
|
||||
bool deactivated = false;
|
||||
bool active = false;
|
||||
};
|
||||
|
||||
template <typename DrawContentFn>
|
||||
@@ -101,11 +95,13 @@ inline bool DrawPopupSubmenuScope(const char* label, DrawContentFn&& drawContent
|
||||
popupOpen,
|
||||
ImGuiSelectableFlags_NoAutoClosePopups,
|
||||
ImVec2(rowWidth, rowHeight))) {
|
||||
TracePopupSubmenuIfNeeded(label, "Hierarchy create submenu selectable clicked -> OpenPopup");
|
||||
ImGui::OpenPopup(popupId);
|
||||
}
|
||||
|
||||
const bool hovered = ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup);
|
||||
if (hovered && !popupOpen) {
|
||||
TracePopupSubmenuIfNeeded(label, "Hierarchy create submenu hovered -> OpenPopup");
|
||||
ImGui::OpenPopup(popupId);
|
||||
}
|
||||
|
||||
@@ -138,6 +134,15 @@ inline bool DrawPopupSubmenuScope(const char* label, DrawContentFn&& drawContent
|
||||
ImGuiWindowFlags_NoMove |
|
||||
ImGuiWindowFlags_NoResize |
|
||||
ImGuiWindowFlags_NoSavedSettings);
|
||||
if (std::string(label) == "Create") {
|
||||
static bool s_lastCreateOpen = false;
|
||||
if (open != s_lastCreateOpen) {
|
||||
TracePopupSubmenuIfNeeded(
|
||||
label,
|
||||
std::string("Hierarchy create submenu popup ") + (open ? "opened" : "closed"));
|
||||
s_lastCreateOpen = open;
|
||||
}
|
||||
}
|
||||
if (!open) {
|
||||
ImGui::PopID();
|
||||
return false;
|
||||
@@ -146,6 +151,12 @@ inline bool DrawPopupSubmenuScope(const char* label, DrawContentFn&& drawContent
|
||||
drawContent();
|
||||
const bool popupHovered = ImGui::IsWindowHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup);
|
||||
if (!hovered && !popupHovered && !ImGui::IsWindowAppearing()) {
|
||||
TracePopupSubmenuIfNeeded(
|
||||
label,
|
||||
std::string("Hierarchy create submenu auto-close: rowHovered=") +
|
||||
(hovered ? "1" : "0") +
|
||||
", popupHovered=" +
|
||||
(popupHovered ? "1" : "0"));
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
EndPopup();
|
||||
@@ -190,6 +201,50 @@ inline bool ToolbarSearchField(
|
||||
return changed;
|
||||
}
|
||||
|
||||
inline InlineRenameFieldResult DrawInlineRenameField(
|
||||
const char* id,
|
||||
char* buffer,
|
||||
size_t bufferSize,
|
||||
float width = -1.0f,
|
||||
bool requestFocus = false,
|
||||
ImGuiInputTextFlags flags = ImGuiInputTextFlags_EnterReturnsTrue | ImGuiInputTextFlags_AutoSelectAll) {
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, InlineRenameFieldFramePadding());
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, InlineRenameFieldRounding());
|
||||
ImGui::SetNextItemWidth(width);
|
||||
if (requestFocus) {
|
||||
ImGui::SetKeyboardFocusHere();
|
||||
}
|
||||
|
||||
const bool submitted = ImGui::InputText(id, buffer, bufferSize, flags);
|
||||
const bool active = ImGui::IsItemActive();
|
||||
const bool deactivated = ImGui::IsItemDeactivated();
|
||||
const bool cancelRequested = active && ImGui::IsKeyPressed(ImGuiKey_Escape);
|
||||
ImGui::PopStyleVar(2);
|
||||
|
||||
return InlineRenameFieldResult{ submitted, cancelRequested, deactivated, active };
|
||||
}
|
||||
|
||||
inline InlineRenameFieldResult DrawInlineRenameFieldAt(
|
||||
const char* id,
|
||||
const ImVec2& screenPos,
|
||||
char* buffer,
|
||||
size_t bufferSize,
|
||||
float width,
|
||||
bool requestFocus = false,
|
||||
ImGuiInputTextFlags flags = ImGuiInputTextFlags_EnterReturnsTrue | ImGuiInputTextFlags_AutoSelectAll) {
|
||||
const ImVec2 restoreCursor = ImGui::GetCursorPos();
|
||||
ImGui::SetCursorScreenPos(screenPos);
|
||||
const InlineRenameFieldResult result = DrawInlineRenameField(
|
||||
id,
|
||||
buffer,
|
||||
bufferSize,
|
||||
width,
|
||||
requestFocus,
|
||||
flags);
|
||||
ImGui::SetCursorPos(restoreCursor);
|
||||
return result;
|
||||
}
|
||||
|
||||
inline void DrawToolbarLabel(const char* text) {
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::TextColored(HintTextColor(), "%s", text);
|
||||
@@ -271,16 +326,22 @@ inline bool DrawBreadcrumbSegment(const char* label, bool clickable, bool curren
|
||||
const ImVec2 padding = BreadcrumbSegmentPadding();
|
||||
const ImVec2 textSize = ImGui::CalcTextSize(label);
|
||||
const ImVec2 size(textSize.x + padding.x * 2.0f, BreadcrumbItemHeight());
|
||||
bool pressed = false;
|
||||
bool hovered = false;
|
||||
DrawBreadcrumbTextItem(label, size, BreadcrumbSegmentTextColor(current, hovered), clickable, &pressed, &hovered);
|
||||
ImGui::InvisibleButton("##BreadcrumbItem", size);
|
||||
|
||||
const bool hovered = clickable && ImGui::IsItemHovered();
|
||||
const bool pressed = clickable && ImGui::IsItemClicked(ImGuiMouseButton_Left);
|
||||
|
||||
const ImVec2 itemMin = ImGui::GetItemRectMin();
|
||||
const ImVec2 itemMax = ImGui::GetItemRectMax();
|
||||
const float textX = itemMin.x + (size.x - textSize.x) * 0.5f;
|
||||
const float textY = itemMin.y + (itemMax.y - itemMin.y - textSize.y) * 0.5f;
|
||||
|
||||
ImGui::GetWindowDrawList()->AddText(
|
||||
ImVec2(textX, textY),
|
||||
ImGui::GetColorU32(BreadcrumbSegmentTextColor(current, hovered)),
|
||||
label);
|
||||
|
||||
if (hovered) {
|
||||
const ImVec2 itemMin = ImGui::GetItemRectMin();
|
||||
const ImVec2 itemMax = ImGui::GetItemRectMax();
|
||||
const ImVec2 textOnlySize = ImGui::CalcTextSize(label);
|
||||
const float textX = itemMin.x + (size.x - textOnlySize.x) * 0.5f;
|
||||
const float textY = itemMin.y + (itemMax.y - itemMin.y - textOnlySize.y) * 0.5f;
|
||||
ImGui::GetWindowDrawList()->AddText(
|
||||
ImVec2(textX, textY),
|
||||
ImGui::GetColorU32(BreadcrumbSegmentTextColor(current, true)),
|
||||
@@ -301,7 +362,10 @@ inline void DrawToolbarBreadcrumbs(
|
||||
size_t segmentCount,
|
||||
GetNameFn&& getName,
|
||||
NavigateFn&& navigateToSegment) {
|
||||
const float lineY = ImGui::GetCursorPosY();
|
||||
|
||||
if (segmentCount == 0) {
|
||||
ImGui::SetCursorPosY(lineY);
|
||||
DrawBreadcrumbSegment(rootLabel, false, true);
|
||||
return;
|
||||
}
|
||||
@@ -309,8 +373,10 @@ inline void DrawToolbarBreadcrumbs(
|
||||
for (size_t i = 0; i < segmentCount; ++i) {
|
||||
if (i > 0) {
|
||||
ImGui::SameLine(0.0f, BreadcrumbSegmentSpacing());
|
||||
ImGui::SetCursorPosY(lineY);
|
||||
DrawBreadcrumbSeparator();
|
||||
ImGui::SameLine(0.0f, BreadcrumbSegmentSpacing());
|
||||
ImGui::SetCursorPosY(lineY);
|
||||
}
|
||||
|
||||
const std::string label = (i == 0 && rootLabel && rootLabel[0] != '\0')
|
||||
@@ -318,6 +384,7 @@ inline void DrawToolbarBreadcrumbs(
|
||||
: getName(i);
|
||||
const bool current = (i + 1 == segmentCount);
|
||||
|
||||
ImGui::SetCursorPosY(lineY);
|
||||
ImGui::PushID(static_cast<int>(i));
|
||||
if (DrawBreadcrumbSegment(label.c_str(), !current, current)) {
|
||||
navigateToSegment(i);
|
||||
@@ -346,7 +413,6 @@ inline AssetTileResult DrawAssetTile(
|
||||
ImGui::InvisibleButton("##AssetBtn", tileSize);
|
||||
|
||||
const bool clicked = ImGui::IsItemClicked(ImGuiMouseButton_Left);
|
||||
const bool contextRequested = ImGui::IsItemClicked(ImGuiMouseButton_Right);
|
||||
const bool openRequested = ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(0);
|
||||
const bool hovered = ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem);
|
||||
|
||||
@@ -388,7 +454,7 @@ inline AssetTileResult DrawAssetTile(
|
||||
ImGui::PopClipRect();
|
||||
}
|
||||
|
||||
return AssetTileResult{ clicked, contextRequested, openRequested, hovered, min, max, labelMin, labelMax };
|
||||
return AssetTileResult{ clicked, openRequested, hovered, min, max, labelMin, labelMax };
|
||||
}
|
||||
|
||||
template <typename DrawMenuFn>
|
||||
@@ -397,6 +463,7 @@ inline ComponentSectionResult BeginComponentSection(
|
||||
const char* label,
|
||||
DrawMenuFn&& drawMenu,
|
||||
bool defaultOpen = true) {
|
||||
(void)drawMenu;
|
||||
const ImGuiStyle& style = ImGui::GetStyle();
|
||||
const ImVec2 framePadding = InspectorSectionFramePadding();
|
||||
const float availableWidth = ImMax(ImGui::GetContentRegionAvail().x, 1.0f);
|
||||
@@ -453,11 +520,6 @@ inline ComponentSectionResult BeginComponentSection(
|
||||
drawList->PopClipRect();
|
||||
}
|
||||
|
||||
if (BeginPopupContextItem("##ComponentSettings")) {
|
||||
drawMenu();
|
||||
EndPopup();
|
||||
}
|
||||
|
||||
ImGui::PopID();
|
||||
return ComponentSectionResult{ open, InspectorSectionContentIndent() };
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ struct SceneViewportInput {
|
||||
ImVec2 viewportSize = ImVec2(0.0f, 0.0f);
|
||||
ImVec2 mouseDelta = ImVec2(0.0f, 0.0f);
|
||||
float mouseWheel = 0.0f;
|
||||
float flySpeedDelta = 0.0f;
|
||||
float deltaTime = 0.0f;
|
||||
float moveForward = 0.0f;
|
||||
float moveRight = 0.0f;
|
||||
@@ -65,6 +66,10 @@ public:
|
||||
virtual void BeginFrame() = 0;
|
||||
virtual EditorViewportFrame RequestViewport(EditorViewportKind kind, const ImVec2& requestedSize) = 0;
|
||||
virtual void UpdateSceneViewInput(IEditorContext& context, const SceneViewportInput& input) = 0;
|
||||
virtual uint64_t PickSceneViewEntity(
|
||||
IEditorContext& context,
|
||||
const ImVec2& viewportSize,
|
||||
const ImVec2& viewportMousePosition) = 0;
|
||||
virtual SceneViewportOverlayData GetSceneViewOverlayData() const = 0;
|
||||
virtual void RenderRequestedViewports(
|
||||
IEditorContext& context,
|
||||
|
||||
@@ -19,6 +19,7 @@ struct SceneViewportCameraInputState {
|
||||
float panDeltaX = 0.0f;
|
||||
float panDeltaY = 0.0f;
|
||||
float zoomDelta = 0.0f;
|
||||
float flySpeedDelta = 0.0f;
|
||||
float deltaTime = 0.0f;
|
||||
float moveForward = 0.0f;
|
||||
float moveRight = 0.0f;
|
||||
@@ -32,6 +33,7 @@ public:
|
||||
void Reset() {
|
||||
m_focalPoint = Math::Vector3::Zero();
|
||||
m_distance = 6.0f;
|
||||
m_flySpeed = 5.0f;
|
||||
m_yawDegrees = -35.0f;
|
||||
m_pitchDegrees = -20.0f;
|
||||
UpdatePositionFromFocalPoint();
|
||||
@@ -45,6 +47,10 @@ public:
|
||||
return m_distance;
|
||||
}
|
||||
|
||||
float GetFlySpeed() const {
|
||||
return m_flySpeed;
|
||||
}
|
||||
|
||||
float GetYawDegrees() const {
|
||||
return m_yawDegrees;
|
||||
}
|
||||
@@ -95,22 +101,27 @@ public:
|
||||
const Math::Vector3 up = GetUp();
|
||||
const float worldUnitsPerPixel = ComputeWorldUnitsPerPixel(input.viewportHeight);
|
||||
const Math::Vector3 delta =
|
||||
((right * input.panDeltaX) + (up * -input.panDeltaY)) * worldUnitsPerPixel;
|
||||
((right * -input.panDeltaX) + (up * input.panDeltaY)) * worldUnitsPerPixel;
|
||||
m_focalPoint += delta;
|
||||
m_position += delta;
|
||||
}
|
||||
|
||||
if (std::abs(input.flySpeedDelta) > Math::EPSILON) {
|
||||
const float speedFactor = std::pow(1.20f, input.flySpeedDelta);
|
||||
m_flySpeed = std::clamp(m_flySpeed * speedFactor, 0.5f, 500.0f);
|
||||
}
|
||||
|
||||
if (input.deltaTime > 0.0f &&
|
||||
(std::abs(input.moveForward) > Math::EPSILON ||
|
||||
std::abs(input.moveRight) > Math::EPSILON ||
|
||||
std::abs(input.moveUp) > Math::EPSILON)) {
|
||||
const Math::Vector3 movement =
|
||||
GetForward() * -input.moveForward +
|
||||
GetRight() * -input.moveRight +
|
||||
GetForward() * input.moveForward +
|
||||
GetRight() * input.moveRight +
|
||||
Math::Vector3::Up() * input.moveUp;
|
||||
if (movement.SqrMagnitude() > Math::EPSILON) {
|
||||
const float speedMultiplier = input.fastMove ? 4.0f : 1.0f;
|
||||
const float flySpeed = (std::max)(5.0f, m_distance * 2.0f) * speedMultiplier;
|
||||
const float flySpeed = m_flySpeed * speedMultiplier;
|
||||
const Math::Vector3 delta = Math::Vector3::Normalize(movement) * flySpeed * input.deltaTime;
|
||||
m_position += delta;
|
||||
m_focalPoint += delta;
|
||||
@@ -135,7 +146,7 @@ public:
|
||||
private:
|
||||
void ApplyRotationDelta(float deltaX, float deltaY) {
|
||||
m_yawDegrees += deltaX * 0.30f;
|
||||
m_pitchDegrees = std::clamp(m_pitchDegrees + deltaY * 0.20f, -89.0f, 89.0f);
|
||||
m_pitchDegrees = std::clamp(m_pitchDegrees - deltaY * 0.20f, -89.0f, 89.0f);
|
||||
}
|
||||
|
||||
Math::Vector3 GetRight() const {
|
||||
@@ -166,6 +177,7 @@ private:
|
||||
Math::Vector3 m_position = Math::Vector3::Zero();
|
||||
Math::Vector3 m_focalPoint = Math::Vector3::Zero();
|
||||
float m_distance = 6.0f;
|
||||
float m_flySpeed = 5.0f;
|
||||
float m_yawDegrees = -35.0f;
|
||||
float m_pitchDegrees = -20.0f;
|
||||
};
|
||||
|
||||
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/ISelectionManager.h"
|
||||
#include "IViewportHostService.h"
|
||||
#include "SceneViewportPicker.h"
|
||||
#include "SceneViewportCameraController.h"
|
||||
#include "SceneViewportInfiniteGridPass.h"
|
||||
#include "SceneViewportSelectionMaskPass.h"
|
||||
#include "SceneViewportSelectionOutlinePass.h"
|
||||
#include "SceneViewportSelectionUtils.h"
|
||||
#include "UI/ImGuiBackendBridge.h"
|
||||
|
||||
#include <XCEngine/Components/CameraComponent.h>
|
||||
@@ -27,6 +32,12 @@
|
||||
namespace XCEngine {
|
||||
namespace Editor {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr bool kDebugSceneSelectionMask = false;
|
||||
|
||||
} // namespace
|
||||
|
||||
class ViewportHostService : public IViewportHostService {
|
||||
public:
|
||||
void Initialize(UI::ImGuiBackendBridge& backend, RHI::RHIDevice* device) {
|
||||
@@ -44,6 +55,9 @@ public:
|
||||
m_sceneViewCamera = {};
|
||||
m_device = nullptr;
|
||||
m_backend = nullptr;
|
||||
m_sceneGridPass.Shutdown();
|
||||
m_sceneSelectionMaskPass.Shutdown();
|
||||
m_sceneSelectionOutlinePass.Shutdown();
|
||||
m_sceneRenderer.reset();
|
||||
}
|
||||
|
||||
@@ -91,6 +105,7 @@ public:
|
||||
SceneViewportCameraInputState controllerInput = {};
|
||||
controllerInput.viewportHeight = input.viewportSize.y;
|
||||
controllerInput.zoomDelta = input.hovered ? input.mouseWheel : 0.0f;
|
||||
controllerInput.flySpeedDelta = input.hovered ? input.flySpeedDelta : 0.0f;
|
||||
controllerInput.deltaTime = input.deltaTime;
|
||||
controllerInput.moveForward = input.moveForward;
|
||||
controllerInput.moveRight = input.moveRight;
|
||||
@@ -116,6 +131,27 @@ public:
|
||||
ApplySceneViewCameraController();
|
||||
}
|
||||
|
||||
uint64_t PickSceneViewEntity(
|
||||
IEditorContext& context,
|
||||
const ImVec2& viewportSize,
|
||||
const ImVec2& viewportMousePosition) override {
|
||||
if (!EnsureSceneViewCamera()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const Components::Scene* scene = context.GetSceneManager().GetScene();
|
||||
if (scene == nullptr) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
SceneViewportPickRequest request = {};
|
||||
request.scene = scene;
|
||||
request.overlay = GetSceneViewOverlayData();
|
||||
request.viewportSize = Math::Vector2(viewportSize.x, viewportSize.y);
|
||||
request.viewportPosition = Math::Vector2(viewportMousePosition.x, viewportMousePosition.y);
|
||||
return PickSceneViewportEntity(request).entityId;
|
||||
}
|
||||
|
||||
SceneViewportOverlayData GetSceneViewOverlayData() const override {
|
||||
SceneViewportOverlayData data = {};
|
||||
if (m_sceneViewCamera.gameObject == nullptr || m_sceneViewCamera.camera == nullptr) {
|
||||
@@ -159,7 +195,7 @@ public:
|
||||
continue;
|
||||
}
|
||||
|
||||
RenderViewportEntry(entry, scene, renderContext);
|
||||
RenderViewportEntry(entry, context, scene, renderContext);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,10 +211,14 @@ private:
|
||||
RHI::RHIResourceView* colorView = nullptr;
|
||||
RHI::RHITexture* depthTexture = nullptr;
|
||||
RHI::RHIResourceView* depthView = nullptr;
|
||||
RHI::RHITexture* selectionMaskTexture = nullptr;
|
||||
RHI::RHIResourceView* selectionMaskView = nullptr;
|
||||
RHI::RHIResourceView* selectionMaskShaderView = nullptr;
|
||||
D3D12_CPU_DESCRIPTOR_HANDLE imguiCpuHandle = {};
|
||||
D3D12_GPU_DESCRIPTOR_HANDLE imguiGpuHandle = {};
|
||||
ImTextureID textureId = {};
|
||||
RHI::ResourceStates colorState = RHI::ResourceStates::Common;
|
||||
RHI::ResourceStates selectionMaskState = RHI::ResourceStates::Common;
|
||||
std::string statusText;
|
||||
};
|
||||
|
||||
@@ -277,6 +317,10 @@ private:
|
||||
entry.colorView != nullptr &&
|
||||
entry.depthTexture != nullptr &&
|
||||
entry.depthView != nullptr &&
|
||||
(entry.kind != EditorViewportKind::Scene ||
|
||||
(entry.selectionMaskTexture != nullptr &&
|
||||
entry.selectionMaskView != nullptr &&
|
||||
entry.selectionMaskShaderView != nullptr)) &&
|
||||
entry.textureId != ImTextureID{}) {
|
||||
return true;
|
||||
}
|
||||
@@ -338,6 +382,40 @@ private:
|
||||
return false;
|
||||
}
|
||||
|
||||
if (entry.kind == EditorViewportKind::Scene) {
|
||||
RHI::TextureDesc selectionMaskDesc = {};
|
||||
selectionMaskDesc.width = entry.width;
|
||||
selectionMaskDesc.height = entry.height;
|
||||
selectionMaskDesc.depth = 1;
|
||||
selectionMaskDesc.mipLevels = 1;
|
||||
selectionMaskDesc.arraySize = 1;
|
||||
selectionMaskDesc.format = static_cast<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);
|
||||
if (entry.imguiCpuHandle.ptr == 0 || entry.imguiGpuHandle.ptr == 0) {
|
||||
DestroyViewportResources(entry);
|
||||
@@ -357,6 +435,7 @@ private:
|
||||
|
||||
entry.textureId = static_cast<ImTextureID>(entry.imguiGpuHandle.ptr);
|
||||
entry.colorState = RHI::ResourceStates::Common;
|
||||
entry.selectionMaskState = RHI::ResourceStates::Common;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -369,8 +448,18 @@ private:
|
||||
return surface;
|
||||
}
|
||||
|
||||
Rendering::RenderSurface BuildSelectionMaskSurface(const ViewportEntry& entry) const {
|
||||
Rendering::RenderSurface surface(entry.width, entry.height);
|
||||
surface.SetColorAttachment(entry.selectionMaskView);
|
||||
surface.SetDepthAttachment(entry.depthView);
|
||||
surface.SetColorStateBefore(entry.selectionMaskState);
|
||||
surface.SetColorStateAfter(RHI::ResourceStates::PixelShaderResource);
|
||||
return surface;
|
||||
}
|
||||
|
||||
void RenderViewportEntry(
|
||||
ViewportEntry& entry,
|
||||
IEditorContext& context,
|
||||
const Components::Scene* scene,
|
||||
const Rendering::RenderContext& renderContext) {
|
||||
if (entry.colorView == nullptr || entry.depthView == nullptr) {
|
||||
@@ -378,12 +467,6 @@ private:
|
||||
return;
|
||||
}
|
||||
|
||||
if (scene == nullptr) {
|
||||
entry.statusText = "No active scene";
|
||||
ClearViewport(entry, renderContext, 0.07f, 0.08f, 0.10f, 1.0f);
|
||||
return;
|
||||
}
|
||||
|
||||
Rendering::RenderSurface surface = BuildSurface(entry);
|
||||
|
||||
if (entry.kind == EditorViewportKind::Scene) {
|
||||
@@ -394,14 +477,130 @@ private:
|
||||
}
|
||||
|
||||
ApplySceneViewCameraController();
|
||||
if (!m_sceneRenderer->Render(*scene, m_sceneViewCamera.camera, renderContext, surface)) {
|
||||
if (scene == nullptr) {
|
||||
entry.statusText = "No active scene";
|
||||
ClearViewport(entry, renderContext, 0.07f, 0.08f, 0.10f, 1.0f);
|
||||
} else if (!m_sceneRenderer->Render(*scene, m_sceneViewCamera.camera, renderContext, surface)) {
|
||||
entry.statusText = "Scene renderer failed";
|
||||
ClearViewport(entry, renderContext, 0.18f, 0.07f, 0.07f, 1.0f);
|
||||
return;
|
||||
} else {
|
||||
entry.colorState = RHI::ResourceStates::PixelShaderResource;
|
||||
entry.statusText.clear();
|
||||
}
|
||||
|
||||
entry.colorState = RHI::ResourceStates::PixelShaderResource;
|
||||
entry.statusText.clear();
|
||||
const SceneViewportOverlayData overlay = GetSceneViewOverlayData();
|
||||
if (overlay.valid) {
|
||||
RHI::RHICommandList* commandList = renderContext.commandList;
|
||||
bool hasSelection = false;
|
||||
Rendering::RenderCameraData cameraData = {};
|
||||
std::vector<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;
|
||||
}
|
||||
|
||||
@@ -456,6 +655,24 @@ private:
|
||||
m_backend->FreeTextureDescriptor(entry.imguiCpuHandle, entry.imguiGpuHandle);
|
||||
}
|
||||
|
||||
if (entry.selectionMaskShaderView != nullptr) {
|
||||
entry.selectionMaskShaderView->Shutdown();
|
||||
delete entry.selectionMaskShaderView;
|
||||
entry.selectionMaskShaderView = nullptr;
|
||||
}
|
||||
|
||||
if (entry.selectionMaskView != nullptr) {
|
||||
entry.selectionMaskView->Shutdown();
|
||||
delete entry.selectionMaskView;
|
||||
entry.selectionMaskView = nullptr;
|
||||
}
|
||||
|
||||
if (entry.selectionMaskTexture != nullptr) {
|
||||
entry.selectionMaskTexture->Shutdown();
|
||||
delete entry.selectionMaskTexture;
|
||||
entry.selectionMaskTexture = nullptr;
|
||||
}
|
||||
|
||||
if (entry.depthView != nullptr) {
|
||||
entry.depthView->Shutdown();
|
||||
delete entry.depthView;
|
||||
@@ -486,6 +703,7 @@ private:
|
||||
entry.imguiGpuHandle = {};
|
||||
entry.textureId = {};
|
||||
entry.colorState = RHI::ResourceStates::Common;
|
||||
entry.selectionMaskState = RHI::ResourceStates::Common;
|
||||
}
|
||||
|
||||
UI::ImGuiBackendBridge* m_backend = nullptr;
|
||||
@@ -493,6 +711,9 @@ private:
|
||||
std::unique_ptr<Rendering::SceneRenderer> m_sceneRenderer;
|
||||
std::array<ViewportEntry, 2> m_entries = {};
|
||||
SceneViewCameraState m_sceneViewCamera;
|
||||
SceneViewportInfiniteGridPass m_sceneGridPass;
|
||||
SceneViewportSelectionMaskPass m_sceneSelectionMaskPass;
|
||||
SceneViewportSelectionOutlinePass m_sceneSelectionOutlinePass;
|
||||
};
|
||||
|
||||
} // namespace Editor
|
||||
|
||||
@@ -111,11 +111,20 @@ void HierarchyPanel::Render() {
|
||||
RenderEntity(gameObject);
|
||||
}
|
||||
|
||||
Actions::HandleHierarchyBackgroundPrimaryClick(*m_context, m_renameState);
|
||||
Actions::RequestHierarchyBackgroundContextPopup(m_backgroundContextMenu);
|
||||
Actions::DrawHierarchyBackgroundInteraction(*m_context, m_renameState);
|
||||
Actions::DrawHierarchyEntityContextPopup(*m_context, m_itemContextMenu);
|
||||
Actions::DrawHierarchyBackgroundContextPopup(*m_context, m_backgroundContextMenu);
|
||||
Actions::DrawHierarchyRootDropTarget(*m_context);
|
||||
static bool s_backgroundContextOpen = false;
|
||||
if (UI::BeginContextMenuForLastItem("##HierarchyBackgroundContext")) {
|
||||
if (!s_backgroundContextOpen) {
|
||||
Actions::TraceHierarchyPopup("Hierarchy background popup opened via background surface");
|
||||
s_backgroundContextOpen = true;
|
||||
}
|
||||
Actions::DrawHierarchyCreateActions(*m_context, nullptr);
|
||||
UI::EndContextMenu();
|
||||
} else if (s_backgroundContextOpen) {
|
||||
Actions::TraceHierarchyPopup("Hierarchy background popup closed");
|
||||
s_backgroundContextOpen = false;
|
||||
}
|
||||
}
|
||||
ImGui::PopStyleColor(2);
|
||||
}
|
||||
@@ -126,22 +135,16 @@ void HierarchyPanel::RenderEntity(::XCEngine::Components::GameObject* gameObject
|
||||
ImGui::PushID(static_cast<int>(gameObject->GetID()));
|
||||
|
||||
if (m_renameState.IsEditing(gameObject->GetID())) {
|
||||
if (m_renameState.ConsumeFocusRequest()) {
|
||||
ImGui::SetKeyboardFocusHere();
|
||||
}
|
||||
|
||||
ImGui::SetNextItemWidth(-1);
|
||||
if (ImGui::InputText(
|
||||
"##Rename",
|
||||
m_renameState.Buffer(),
|
||||
m_renameState.BufferSize(),
|
||||
ImGuiInputTextFlags_EnterReturnsTrue | ImGuiInputTextFlags_AutoSelectAll)) {
|
||||
CommitRename();
|
||||
}
|
||||
const UI::InlineRenameFieldResult renameField = UI::DrawInlineRenameField(
|
||||
"##Rename",
|
||||
m_renameState.Buffer(),
|
||||
m_renameState.BufferSize(),
|
||||
-1.0f,
|
||||
m_renameState.ConsumeFocusRequest());
|
||||
|
||||
if (ImGui::IsItemActive() && ImGui::IsKeyPressed(ImGuiKey_Escape)) {
|
||||
if (renameField.cancelRequested) {
|
||||
CancelRename();
|
||||
} else if (!ImGui::IsItemActive() && ImGui::IsMouseClicked(0)) {
|
||||
} else if (renameField.submitted || renameField.deactivated) {
|
||||
CommitRename();
|
||||
}
|
||||
} else {
|
||||
@@ -161,10 +164,6 @@ void HierarchyPanel::RenderEntity(::XCEngine::Components::GameObject* gameObject
|
||||
if (node.secondaryClicked) {
|
||||
Actions::HandleHierarchyItemContextRequest(*m_context, gameObject, m_itemContextMenu);
|
||||
}
|
||||
|
||||
if (node.doubleClicked) {
|
||||
BeginRename(gameObject);
|
||||
}
|
||||
};
|
||||
nodeDefinition.callbacks.onRenderExtras = [this, gameObject]() {
|
||||
Actions::BeginHierarchyEntityDrag(gameObject);
|
||||
|
||||
@@ -16,6 +16,32 @@
|
||||
namespace XCEngine {
|
||||
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() {
|
||||
@@ -91,14 +117,24 @@ void InspectorPanel::RenderGameObject(::XCEngine::Components::GameObject* gameOb
|
||||
}
|
||||
|
||||
auto components = gameObject->GetComponents<::XCEngine::Components::Component>();
|
||||
m_deferredContextAction = {};
|
||||
for (auto* component : components) {
|
||||
RenderComponent(component, gameObject);
|
||||
if (m_deferredContextAction) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Actions::DrawInspectorAddComponentButton(m_addComponentPopup, gameObject != nullptr);
|
||||
Actions::DrawInspectorAddComponentPopup(*m_context, m_addComponentPopup, gameObject);
|
||||
|
||||
Actions::FinalizeInspectorInteractiveChangeIfIdle(*m_context);
|
||||
|
||||
if (m_deferredContextAction) {
|
||||
auto deferredAction = std::move(m_deferredContextAction);
|
||||
m_deferredContextAction = {};
|
||||
deferredAction();
|
||||
}
|
||||
}
|
||||
|
||||
void InspectorPanel::RenderEmptyState(const char* title, const char* subtitle) {
|
||||
@@ -119,30 +155,29 @@ void InspectorPanel::RenderComponent(::XCEngine::Components::Component* componen
|
||||
IComponentEditor* editor = ComponentEditorRegistry::Get().FindEditor(component);
|
||||
const std::string name = component->GetName();
|
||||
|
||||
bool removed = false;
|
||||
const UI::ComponentSectionResult section = UI::BeginComponentSection(
|
||||
(void*)typeid(*component).hash_code(),
|
||||
name.c_str(),
|
||||
[&]() {
|
||||
removed = Actions::DrawInspectorComponentMenu(*m_context, component, gameObject, editor);
|
||||
});
|
||||
name.c_str());
|
||||
|
||||
if (removed) {
|
||||
return;
|
||||
if (UI::BeginContextMenuForLastItem("##InspectorComponentContext")) {
|
||||
DrawInspectorComponentContextMenu(*m_context, component, gameObject, m_deferredContextAction);
|
||||
UI::EndContextMenu();
|
||||
}
|
||||
|
||||
if (section.open) {
|
||||
ImGui::Indent(section.contentIndent);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, UI::InspectorComponentControlSpacing());
|
||||
if (editor) {
|
||||
if (editor->Render(component, &m_context->GetUndoManager())) {
|
||||
m_context->GetSceneManager().MarkSceneDirty();
|
||||
if (!m_deferredContextAction) {
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, UI::InspectorComponentControlSpacing());
|
||||
if (editor) {
|
||||
if (editor->Render(component, &m_context->GetUndoManager())) {
|
||||
m_context->GetSceneManager().MarkSceneDirty();
|
||||
}
|
||||
} else {
|
||||
UI::DrawHintText("No registered editor for this component");
|
||||
}
|
||||
} else {
|
||||
UI::DrawHintText("No registered editor for this component");
|
||||
ImGui::PopStyleVar();
|
||||
}
|
||||
ImGui::PopStyleVar();
|
||||
|
||||
|
||||
UI::EndComponentSection(section);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "UI/PopupState.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Components {
|
||||
@@ -31,6 +32,7 @@ private:
|
||||
uint64_t m_selectionHandlerId = 0;
|
||||
uint64_t m_selectedEntityId = 0;
|
||||
UI::DeferredPopupState m_addComponentPopup;
|
||||
std::function<void()> m_deferredContextAction;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -16,6 +16,13 @@ namespace Editor {
|
||||
|
||||
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) {
|
||||
if (!context.drawList) {
|
||||
return;
|
||||
@@ -186,6 +193,7 @@ void ProjectPanel::Render() {
|
||||
|
||||
auto& manager = m_context->GetProjectManager();
|
||||
BeginAssetDragDropFrame();
|
||||
m_deferredContextAction = {};
|
||||
RenderToolbar();
|
||||
|
||||
ImGui::PushStyleColor(ImGuiCol_ChildBg, UI::ProjectBrowserSurfaceColor());
|
||||
@@ -215,6 +223,12 @@ void ProjectPanel::Render() {
|
||||
|
||||
FinalizeAssetDragDrop(manager);
|
||||
ImGui::PopStyleColor();
|
||||
|
||||
if (m_deferredContextAction) {
|
||||
auto deferredAction = std::move(m_deferredContextAction);
|
||||
m_deferredContextAction = {};
|
||||
deferredAction();
|
||||
}
|
||||
}
|
||||
|
||||
void ProjectPanel::RenderToolbar() {
|
||||
@@ -246,6 +260,7 @@ void ProjectPanel::RenderToolbar() {
|
||||
}
|
||||
|
||||
void ProjectPanel::RenderFolderTreePane(IProjectManager& manager) {
|
||||
auto* managerPtr = &manager;
|
||||
ImGui::PushStyleColor(ImGuiCol_ChildBg, UI::ProjectNavigationPaneBackgroundColor());
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, UI::ProjectNavigationPanePadding());
|
||||
const bool open = ImGui::BeginChild("ProjectFolderTree", ImVec2(m_navigationWidth, 0.0f), false);
|
||||
@@ -266,6 +281,17 @@ void ProjectPanel::RenderFolderTreePane(IProjectManager& manager) {
|
||||
UI::DrawEmptyState("No Assets Folder");
|
||||
}
|
||||
|
||||
if (UI::BeginContextMenuForWindow("##ProjectFolderTreeContext")) {
|
||||
Actions::DrawMenuAction(Actions::MakeCreateFolderAction(), [&]() {
|
||||
QueueDeferredAction(m_deferredContextAction, [this, managerPtr]() {
|
||||
if (AssetItemPtr createdFolder = Commands::CreateFolder(*managerPtr, "New Folder")) {
|
||||
BeginRename(createdFolder);
|
||||
}
|
||||
});
|
||||
});
|
||||
UI::EndContextMenu();
|
||||
}
|
||||
|
||||
ImGui::EndChild();
|
||||
}
|
||||
|
||||
@@ -298,9 +324,6 @@ void ProjectPanel::RenderFolderTreeNode(
|
||||
manager.NavigateToFolder(folder);
|
||||
}
|
||||
|
||||
if (node.secondaryClicked) {
|
||||
Actions::HandleProjectItemContextRequest(manager, folder, m_itemContextMenu);
|
||||
}
|
||||
};
|
||||
|
||||
const UI::TreeNodeResult node = UI::DrawTreeNode(
|
||||
@@ -323,6 +346,7 @@ void ProjectPanel::RenderFolderTreeNode(
|
||||
}
|
||||
|
||||
void ProjectPanel::RenderBrowserPane(IProjectManager& manager) {
|
||||
auto* managerPtr = &manager;
|
||||
ImGui::PushStyleColor(ImGuiCol_ChildBg, UI::ProjectBrowserPaneBackgroundColor());
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f));
|
||||
const bool open = ImGui::BeginChild("ProjectBrowser", ImVec2(0.0f, 0.0f), false);
|
||||
@@ -361,7 +385,9 @@ void ProjectPanel::RenderBrowserPane(IProjectManager& manager) {
|
||||
}
|
||||
|
||||
const float tileWidth = UI::AssetTileSize().x;
|
||||
const float tileHeight = UI::AssetTileSize().y;
|
||||
const float spacing = UI::AssetGridSpacing().x;
|
||||
const float rowSpacing = UI::AssetGridSpacing().y;
|
||||
const float panelWidth = ImGui::GetContentRegionAvail().x;
|
||||
int columns = static_cast<int>((panelWidth + spacing) / (tileWidth + spacing));
|
||||
if (columns < 1) {
|
||||
@@ -369,26 +395,33 @@ void ProjectPanel::RenderBrowserPane(IProjectManager& manager) {
|
||||
}
|
||||
|
||||
AssetItemPtr pendingSelection;
|
||||
AssetItemPtr pendingContextTarget;
|
||||
AssetItemPtr pendingOpenTarget;
|
||||
const std::string selectedItemPath = manager.GetSelectedItemPath();
|
||||
const ImVec2 gridOrigin = ImGui::GetCursorPos();
|
||||
for (int visibleIndex = 0; visibleIndex < static_cast<int>(visibleItems.size()); ++visibleIndex) {
|
||||
if (visibleIndex > 0 && visibleIndex % columns != 0) {
|
||||
ImGui::SameLine();
|
||||
}
|
||||
const int column = visibleIndex % columns;
|
||||
const int row = visibleIndex / columns;
|
||||
ImGui::SetCursorPos(ImVec2(
|
||||
gridOrigin.x + column * (tileWidth + spacing),
|
||||
gridOrigin.y + row * (tileHeight + rowSpacing)));
|
||||
|
||||
const AssetItemPtr& item = visibleItems[visibleIndex];
|
||||
const AssetItemInteraction interaction = RenderAssetItem(item, selectedItemPath == item->fullPath);
|
||||
if (interaction.clicked) {
|
||||
pendingSelection = item;
|
||||
}
|
||||
if (interaction.contextRequested) {
|
||||
pendingContextTarget = item;
|
||||
}
|
||||
if (interaction.openRequested) {
|
||||
pendingOpenTarget = item;
|
||||
break;
|
||||
}
|
||||
if (m_deferredContextAction) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!visibleItems.empty()) {
|
||||
const int rowCount = (static_cast<int>(visibleItems.size()) + columns - 1) / columns;
|
||||
ImGui::SetCursorPosY(gridOrigin.y + rowCount * tileHeight + (rowCount - 1) * rowSpacing);
|
||||
}
|
||||
|
||||
if (visibleItems.empty() && !search.empty()) {
|
||||
@@ -397,29 +430,41 @@ void ProjectPanel::RenderBrowserPane(IProjectManager& manager) {
|
||||
"No assets match the current search");
|
||||
}
|
||||
|
||||
Actions::HandleProjectBackgroundPrimaryClick(manager, m_renameState);
|
||||
if (pendingSelection) {
|
||||
manager.SetSelectedItem(pendingSelection);
|
||||
}
|
||||
if (pendingContextTarget) {
|
||||
Actions::HandleProjectItemContextRequest(manager, pendingContextTarget, m_itemContextMenu);
|
||||
}
|
||||
if (pendingOpenTarget) {
|
||||
Actions::OpenProjectAsset(*m_context, pendingOpenTarget);
|
||||
}
|
||||
Actions::DrawProjectItemContextPopup(*m_context, m_itemContextMenu);
|
||||
Actions::RequestProjectEmptyContextPopup(m_emptyContextMenu);
|
||||
Actions::DrawProjectEmptyContextPopup(m_emptyContextMenu, [&]() {
|
||||
if (AssetItemPtr createdFolder = Commands::CreateFolder(manager, "New Folder")) {
|
||||
BeginRename(createdFolder);
|
||||
if (!m_deferredContextAction) {
|
||||
Actions::HandleProjectBackgroundPrimaryClick(manager, m_renameState);
|
||||
if (pendingSelection) {
|
||||
manager.SetSelectedItem(pendingSelection);
|
||||
}
|
||||
});
|
||||
if (pendingOpenTarget) {
|
||||
Actions::OpenProjectAsset(*m_context, pendingOpenTarget);
|
||||
}
|
||||
}
|
||||
|
||||
if (UI::BeginContextMenuForWindow("##ProjectBrowserContext")) {
|
||||
Actions::DrawMenuAction(Actions::MakeCreateFolderAction(), [&]() {
|
||||
QueueDeferredAction(m_deferredContextAction, [this, managerPtr]() {
|
||||
if (AssetItemPtr createdFolder = Commands::CreateFolder(*managerPtr, "New Folder")) {
|
||||
BeginRename(createdFolder);
|
||||
}
|
||||
});
|
||||
});
|
||||
if (manager.CanNavigateBack()) {
|
||||
Actions::DrawMenuSeparator();
|
||||
Actions::DrawMenuAction(Actions::MakeNavigateBackAction(true), [&]() {
|
||||
QueueDeferredAction(m_deferredContextAction, [managerPtr]() {
|
||||
managerPtr->NavigateBack();
|
||||
});
|
||||
});
|
||||
}
|
||||
UI::EndContextMenu();
|
||||
}
|
||||
|
||||
ImGui::EndChild();
|
||||
ImGui::EndChild();
|
||||
}
|
||||
|
||||
void ProjectPanel::RenderBrowserHeader(IProjectManager& manager) {
|
||||
auto* managerPtr = &manager;
|
||||
ImGui::PushStyleColor(ImGuiCol_ChildBg, UI::ProjectBrowserHeaderBackgroundColor());
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(10.0f, 5.0f));
|
||||
const bool open = ImGui::BeginChild(
|
||||
@@ -445,7 +490,11 @@ void ProjectPanel::RenderBrowserHeader(IProjectManager& manager) {
|
||||
"Assets",
|
||||
manager.GetPathDepth(),
|
||||
[&](size_t index) { return manager.GetPathName(index); },
|
||||
[&](size_t index) { manager.NavigateToIndex(index); });
|
||||
[&](size_t index) {
|
||||
QueueDeferredAction(m_deferredContextAction, [managerPtr, index]() {
|
||||
managerPtr->NavigateToIndex(index);
|
||||
});
|
||||
});
|
||||
|
||||
ImDrawList* drawList = ImGui::GetWindowDrawList();
|
||||
const ImVec2 windowMin = ImGui::GetWindowPos();
|
||||
@@ -478,41 +527,54 @@ ProjectPanel::AssetItemInteraction ProjectPanel::RenderAssetItem(const AssetItem
|
||||
UI::DrawAssetIcon(drawList, iconMin, iconMax, iconKind);
|
||||
},
|
||||
tileOptions);
|
||||
const bool secondaryClicked = !isRenaming && ImGui::IsItemClicked(ImGuiMouseButton_Right);
|
||||
|
||||
if (isRenaming) {
|
||||
const ImVec2 restoreCursor = ImGui::GetCursorPos();
|
||||
ImGui::SetCursorScreenPos(tile.labelMin);
|
||||
ImGui::SetNextItemWidth(tile.labelMax.x - tile.labelMin.x);
|
||||
if (m_renameState.ConsumeFocusRequest()) {
|
||||
ImGui::SetKeyboardFocusHere();
|
||||
}
|
||||
|
||||
const bool submitted = ImGui::InputText(
|
||||
const float renameWidth = tile.labelMax.x - tile.labelMin.x;
|
||||
const float renameOffsetY = (std::max)(0.0f, (tile.labelMax.y - tile.labelMin.y - UI::InlineRenameFieldHeight()) * 0.5f);
|
||||
const UI::InlineRenameFieldResult renameField = UI::DrawInlineRenameFieldAt(
|
||||
"##Rename",
|
||||
ImVec2(tile.labelMin.x, tile.labelMin.y + renameOffsetY),
|
||||
m_renameState.Buffer(),
|
||||
m_renameState.BufferSize(),
|
||||
ImGuiInputTextFlags_EnterReturnsTrue | ImGuiInputTextFlags_AutoSelectAll);
|
||||
const bool cancelRequested = ImGui::IsItemActive() && ImGui::IsKeyPressed(ImGuiKey_Escape);
|
||||
const bool deactivated = ImGui::IsItemDeactivated();
|
||||
ImGui::SetCursorPos(restoreCursor);
|
||||
renameWidth,
|
||||
m_renameState.ConsumeFocusRequest());
|
||||
|
||||
if (cancelRequested) {
|
||||
if (renameField.cancelRequested) {
|
||||
CancelRename();
|
||||
} else if (submitted || deactivated) {
|
||||
} else if (renameField.submitted || renameField.deactivated) {
|
||||
CommitRename(m_context->GetProjectManager());
|
||||
}
|
||||
} else {
|
||||
if (tile.clicked) {
|
||||
interaction.clicked = true;
|
||||
}
|
||||
|
||||
if (tile.contextRequested) {
|
||||
interaction.contextRequested = true;
|
||||
if (secondaryClicked && item) {
|
||||
m_context->GetProjectManager().SetSelectedItem(item);
|
||||
}
|
||||
|
||||
RegisterFolderDropTarget(m_context->GetProjectManager(), item);
|
||||
Actions::BeginProjectAssetDrag(item, iconKind);
|
||||
|
||||
if (UI::BeginContextMenuForLastItem("##ProjectItemContext")) {
|
||||
Actions::DrawMenuAction(Actions::MakeOpenAssetAction(Commands::CanOpenAsset(item)), [&]() {
|
||||
QueueDeferredAction(m_deferredContextAction, [this, item]() {
|
||||
Actions::OpenProjectAsset(*m_context, item);
|
||||
});
|
||||
});
|
||||
Actions::DrawMenuAction(Actions::MakeAction("Rename", nullptr, false, item != nullptr), [&]() {
|
||||
QueueDeferredAction(m_deferredContextAction, [this, item]() {
|
||||
BeginRename(item);
|
||||
});
|
||||
});
|
||||
Actions::DrawMenuAction(Actions::MakeDeleteAssetAction(item != nullptr), [&]() {
|
||||
QueueDeferredAction(m_deferredContextAction, [this, item]() {
|
||||
Commands::DeleteAsset(m_context->GetProjectManager(), item);
|
||||
});
|
||||
});
|
||||
UI::EndContextMenu();
|
||||
}
|
||||
|
||||
if (tile.openRequested) {
|
||||
interaction.openRequested = true;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
#include "UI/PopupState.h"
|
||||
#include "UI/TreeView.h"
|
||||
|
||||
#include <functional>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Editor {
|
||||
|
||||
@@ -35,7 +37,6 @@ private:
|
||||
|
||||
struct AssetItemInteraction {
|
||||
bool clicked = false;
|
||||
bool contextRequested = false;
|
||||
bool openRequested = false;
|
||||
};
|
||||
|
||||
@@ -58,9 +59,8 @@ private:
|
||||
float m_navigationWidth = UI::ProjectNavigationDefaultWidth();
|
||||
UI::TreeViewState m_folderTreeState;
|
||||
UI::InlineTextEditState<std::string, 256> m_renameState;
|
||||
UI::DeferredPopupState m_emptyContextMenu;
|
||||
UI::TargetedPopupState<AssetItemPtr> m_itemContextMenu;
|
||||
AssetDragDropState m_assetDragDropState;
|
||||
std::function<void()> m_deferredContextAction;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -1,233 +1,16 @@
|
||||
#include "Actions/ActionRouting.h"
|
||||
#include "Core/IEditorContext.h"
|
||||
#include "Core/ISelectionManager.h"
|
||||
#include "SceneViewPanel.h"
|
||||
#include "Viewport/SceneViewportOverlayRenderer.h"
|
||||
#include "ViewportPanelContent.h"
|
||||
#include "UI/UI.h"
|
||||
|
||||
#include <XCEngine/Core/Math/Matrix4.h>
|
||||
#include <XCEngine/Core/Math/Vector4.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
#include <imgui.h>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Editor {
|
||||
|
||||
namespace {
|
||||
|
||||
float ComputeGridSpacing(float orbitDistance) {
|
||||
const float target = (std::max)(orbitDistance * 0.35f, 0.1f);
|
||||
const float exponent = std::floor(std::log10(target));
|
||||
const float base = std::pow(10.0f, exponent);
|
||||
const float normalized = target / base;
|
||||
if (normalized < 2.0f) {
|
||||
return base;
|
||||
}
|
||||
if (normalized < 5.0f) {
|
||||
return 2.0f * base;
|
||||
}
|
||||
return 5.0f * base;
|
||||
}
|
||||
|
||||
Math::Matrix4x4 BuildOverlayViewMatrix(const SceneViewportOverlayData& overlay) {
|
||||
return Math::Matrix4x4::LookAt(
|
||||
overlay.cameraPosition,
|
||||
overlay.cameraPosition + overlay.cameraForward,
|
||||
overlay.cameraUp);
|
||||
}
|
||||
|
||||
Math::Matrix4x4 BuildOverlayProjectionMatrix(
|
||||
const SceneViewportOverlayData& overlay,
|
||||
const ImVec2& viewportSize) {
|
||||
const float aspect = viewportSize.y > 0.0f
|
||||
? viewportSize.x / viewportSize.y
|
||||
: 1.0f;
|
||||
return Math::Matrix4x4::Perspective(
|
||||
overlay.verticalFovDegrees * Math::DEG_TO_RAD,
|
||||
aspect,
|
||||
overlay.nearClipPlane,
|
||||
overlay.farClipPlane);
|
||||
}
|
||||
|
||||
bool ProjectWorldPoint(
|
||||
const Math::Matrix4x4& viewProjection,
|
||||
const ImVec2& viewportMin,
|
||||
const ImVec2& viewportSize,
|
||||
const Math::Vector3& worldPoint,
|
||||
ImVec2& screenPoint,
|
||||
float& depth) {
|
||||
const Math::Vector4 clip = viewProjection * Math::Vector4(worldPoint, 1.0f);
|
||||
if (clip.w <= 0.001f) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const float invW = 1.0f / clip.w;
|
||||
const float ndcX = clip.x * invW;
|
||||
const float ndcY = clip.y * invW;
|
||||
const float ndcZ = clip.z * invW;
|
||||
if (ndcZ < -0.2f || ndcZ > 1.2f) {
|
||||
return false;
|
||||
}
|
||||
|
||||
screenPoint.x = viewportMin.x + (ndcX * 0.5f + 0.5f) * viewportSize.x;
|
||||
screenPoint.y = viewportMin.y + (-ndcY * 0.5f + 0.5f) * viewportSize.y;
|
||||
depth = ndcZ;
|
||||
return true;
|
||||
}
|
||||
|
||||
void DrawSceneAxisWidget(
|
||||
ImDrawList* drawList,
|
||||
const SceneViewportOverlayData& overlay,
|
||||
const ImVec2& viewportMin,
|
||||
const ImVec2& viewportMax) {
|
||||
if (drawList == nullptr || !overlay.valid) {
|
||||
return;
|
||||
}
|
||||
|
||||
const Math::Matrix4x4 view = BuildOverlayViewMatrix(overlay);
|
||||
const ImVec2 center(viewportMin.x + 52.0f, viewportMax.y - 52.0f);
|
||||
const float radius = 26.0f;
|
||||
|
||||
drawList->AddCircleFilled(center, radius + 10.0f, IM_COL32(18, 20, 24, 170), 24);
|
||||
drawList->AddCircle(center, radius + 10.0f, IM_COL32(255, 255, 255, 28), 24, 1.0f);
|
||||
|
||||
struct AxisLine {
|
||||
Math::Vector3 axis;
|
||||
ImU32 color;
|
||||
};
|
||||
|
||||
const AxisLine axes[] = {
|
||||
{ Math::Vector3::Right(), IM_COL32(239, 83, 80, 255) },
|
||||
{ Math::Vector3::Up(), IM_COL32(102, 187, 106, 255) },
|
||||
{ Math::Vector3::Forward(), IM_COL32(66, 165, 245, 255) }
|
||||
};
|
||||
|
||||
for (const AxisLine& axis : axes) {
|
||||
const Math::Vector3 viewAxis = view.MultiplyVector(axis.axis);
|
||||
const ImVec2 end(
|
||||
center.x + viewAxis.x * radius,
|
||||
center.y - viewAxis.y * radius);
|
||||
drawList->AddLine(center, end, axis.color, 2.2f);
|
||||
drawList->AddCircleFilled(end, 4.0f, axis.color, 12);
|
||||
}
|
||||
}
|
||||
|
||||
void DrawSceneGridOverlay(
|
||||
ImDrawList* drawList,
|
||||
const SceneViewportOverlayData& overlay,
|
||||
const ImVec2& viewportMin,
|
||||
const ImVec2& viewportMax,
|
||||
const ImVec2& viewportSize) {
|
||||
if (drawList == nullptr || !overlay.valid || viewportSize.x <= 1.0f || viewportSize.y <= 1.0f) {
|
||||
return;
|
||||
}
|
||||
|
||||
const Math::Matrix4x4 viewProjection =
|
||||
BuildOverlayProjectionMatrix(overlay, viewportSize) * BuildOverlayViewMatrix(overlay);
|
||||
const float spacing = ComputeGridSpacing(overlay.orbitDistance);
|
||||
const int halfLineCount = 14;
|
||||
const float extent = spacing * static_cast<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") {}
|
||||
|
||||
void SceneViewPanel::Render() {
|
||||
@@ -239,35 +22,76 @@ void SceneViewPanel::Render() {
|
||||
const ViewportPanelContentResult content = RenderViewportPanelContent(*m_context, EditorViewportKind::Scene);
|
||||
if (IViewportHostService* viewportHostService = m_context->GetViewportHostService()) {
|
||||
const ImGuiIO& io = ImGui::GetIO();
|
||||
const bool selectClick =
|
||||
content.hovered &&
|
||||
content.frame.hasTexture &&
|
||||
ImGui::IsMouseClicked(ImGuiMouseButton_Left) &&
|
||||
!m_lookDragging &&
|
||||
!m_panDragging;
|
||||
const bool beginLookDrag =
|
||||
content.hovered && ImGui::IsMouseClicked(ImGuiMouseButton_Right);
|
||||
const bool beginPanDrag =
|
||||
content.hovered && ImGui::IsMouseClicked(ImGuiMouseButton_Middle);
|
||||
|
||||
if (!m_lookDragging && content.hovered && ImGui::IsMouseClicked(ImGuiMouseButton_Right)) {
|
||||
m_lookDragging = true;
|
||||
if (selectClick || beginLookDrag || beginPanDrag) {
|
||||
ImGui::SetWindowFocus();
|
||||
}
|
||||
if (!m_panDragging && content.hovered && ImGui::IsMouseClicked(ImGuiMouseButton_Middle)) {
|
||||
|
||||
if (selectClick) {
|
||||
const ImVec2 localMousePosition(
|
||||
io.MousePos.x - content.itemMin.x,
|
||||
io.MousePos.y - content.itemMin.y);
|
||||
const uint64_t selectedEntity = viewportHostService->PickSceneViewEntity(
|
||||
*m_context,
|
||||
content.availableSize,
|
||||
localMousePosition);
|
||||
if (selectedEntity != 0) {
|
||||
m_context->GetSelectionManager().SetSelectedEntity(selectedEntity);
|
||||
} else {
|
||||
m_context->GetSelectionManager().ClearSelection();
|
||||
}
|
||||
}
|
||||
|
||||
if (beginLookDrag) {
|
||||
m_lookDragging = true;
|
||||
m_lastLookDragDelta = ImVec2(0.0f, 0.0f);
|
||||
}
|
||||
if (beginPanDrag) {
|
||||
m_panDragging = true;
|
||||
m_lastPanDragDelta = ImVec2(0.0f, 0.0f);
|
||||
}
|
||||
|
||||
if (m_lookDragging && !ImGui::IsMouseDown(ImGuiMouseButton_Right)) {
|
||||
m_lookDragging = false;
|
||||
m_lastLookDragDelta = ImVec2(0.0f, 0.0f);
|
||||
}
|
||||
if (m_panDragging && !ImGui::IsMouseDown(ImGuiMouseButton_Middle)) {
|
||||
m_panDragging = false;
|
||||
m_lastPanDragDelta = ImVec2(0.0f, 0.0f);
|
||||
}
|
||||
|
||||
if (m_lookDragging || m_panDragging) {
|
||||
ImGui::SetNextFrameWantCaptureMouse(true);
|
||||
}
|
||||
if (m_lookDragging) {
|
||||
ImGui::SetNextFrameWantCaptureKeyboard(true);
|
||||
}
|
||||
|
||||
SceneViewportInput input = {};
|
||||
input.viewportSize = content.availableSize;
|
||||
input.deltaTime = io.DeltaTime;
|
||||
input.hovered = content.hovered;
|
||||
input.focused = content.focused;
|
||||
input.mouseWheel = content.hovered ? -io.MouseWheel : 0.0f;
|
||||
input.focused = content.focused || m_lookDragging || m_panDragging;
|
||||
input.mouseWheel = (content.hovered && !m_lookDragging) ? io.MouseWheel : 0.0f;
|
||||
input.flySpeedDelta = (content.hovered && m_lookDragging) ? io.MouseWheel : 0.0f;
|
||||
input.looking = m_lookDragging;
|
||||
input.orbiting = false;
|
||||
input.panning = m_panDragging;
|
||||
input.fastMove = io.KeyShift;
|
||||
input.focusSelectionRequested =
|
||||
content.focused && !io.WantTextInput && ImGui::IsKeyPressed(ImGuiKey_F, false);
|
||||
input.focused && !io.WantTextInput && ImGui::IsKeyPressed(ImGuiKey_F, false);
|
||||
|
||||
if (m_lookDragging && content.focused && !io.WantTextInput) {
|
||||
if (m_lookDragging && !io.WantTextInput) {
|
||||
input.moveForward =
|
||||
(ImGui::IsKeyDown(ImGuiKey_W) ? 1.0f : 0.0f) -
|
||||
(ImGui::IsKeyDown(ImGuiKey_S) ? 1.0f : 0.0f);
|
||||
@@ -280,14 +104,30 @@ void SceneViewPanel::Render() {
|
||||
}
|
||||
|
||||
if (m_lookDragging || m_panDragging) {
|
||||
input.mouseDelta = io.MouseDelta;
|
||||
if (m_lookDragging) {
|
||||
const ImVec2 lookDragDelta = ImGui::GetMouseDragDelta(ImGuiMouseButton_Right, 0.0f);
|
||||
input.mouseDelta.x += lookDragDelta.x - m_lastLookDragDelta.x;
|
||||
input.mouseDelta.y += lookDragDelta.y - m_lastLookDragDelta.y;
|
||||
m_lastLookDragDelta = lookDragDelta;
|
||||
} else {
|
||||
m_lastLookDragDelta = ImVec2(0.0f, 0.0f);
|
||||
}
|
||||
|
||||
if (m_panDragging) {
|
||||
const ImVec2 panDragDelta = ImGui::GetMouseDragDelta(ImGuiMouseButton_Middle, 0.0f);
|
||||
input.mouseDelta.x += panDragDelta.x - m_lastPanDragDelta.x;
|
||||
input.mouseDelta.y += panDragDelta.y - m_lastPanDragDelta.y;
|
||||
m_lastPanDragDelta = panDragDelta;
|
||||
} else {
|
||||
m_lastPanDragDelta = ImVec2(0.0f, 0.0f);
|
||||
}
|
||||
}
|
||||
|
||||
viewportHostService->UpdateSceneViewInput(*m_context, input);
|
||||
|
||||
if (content.hasViewportArea && content.frame.hasTexture) {
|
||||
const SceneViewportOverlayData overlay = viewportHostService->GetSceneViewOverlayData();
|
||||
DrawSceneGridOverlay(
|
||||
DrawSceneViewportOverlay(
|
||||
ImGui::GetWindowDrawList(),
|
||||
overlay,
|
||||
content.itemMin,
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
#include "Panel.h"
|
||||
|
||||
#include <imgui.h>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Editor {
|
||||
|
||||
@@ -13,6 +15,8 @@ public:
|
||||
private:
|
||||
bool m_lookDragging = false;
|
||||
bool m_panDragging = false;
|
||||
ImVec2 m_lastLookDragDelta = ImVec2(0.0f, 0.0f);
|
||||
ImVec2 m_lastPanDragDelta = ImVec2(0.0f, 0.0f);
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -5,9 +5,14 @@ project(XCEngine_EditorTests)
|
||||
set(EDITOR_TEST_SOURCES
|
||||
test_action_routing.cpp
|
||||
test_scene_viewport_camera_controller.cpp
|
||||
test_scene_viewport_picker.cpp
|
||||
test_scene_viewport_overlay_renderer.cpp
|
||||
test_scene_viewport_selection_utils.cpp
|
||||
${CMAKE_SOURCE_DIR}/editor/src/Core/UndoManager.cpp
|
||||
${CMAKE_SOURCE_DIR}/editor/src/Managers/SceneManager.cpp
|
||||
${CMAKE_SOURCE_DIR}/editor/src/Managers/ProjectManager.cpp
|
||||
${CMAKE_SOURCE_DIR}/editor/src/Viewport/SceneViewportPicker.cpp
|
||||
${CMAKE_SOURCE_DIR}/editor/src/Viewport/SceneViewportGrid.cpp
|
||||
)
|
||||
|
||||
add_executable(editor_tests ${EDITOR_TEST_SOURCES})
|
||||
|
||||
@@ -41,6 +41,22 @@ TEST(SceneViewportCameraController_Test, ApplyToMatchesComputedPositionAndForwar
|
||||
EXPECT_GT(Vector3::Dot(cameraObject.GetTransform()->GetUp().Normalized(), Vector3::Up()), 0.0f);
|
||||
}
|
||||
|
||||
TEST(SceneViewportCameraController_Test, ApplyToLooksAtControllerFocalPointInViewSpace) {
|
||||
SceneViewportCameraController controller;
|
||||
controller.Reset();
|
||||
controller.Focus(Vector3(2.0f, 1.0f, -3.0f));
|
||||
|
||||
GameObject cameraObject("EditorCamera");
|
||||
controller.ApplyTo(*cameraObject.GetTransform());
|
||||
|
||||
const Vector3 focalPointInViewSpace =
|
||||
cameraObject.GetTransform()->InverseTransformPoint(controller.GetFocalPoint());
|
||||
|
||||
EXPECT_NEAR(focalPointInViewSpace.x, 0.0f, 1e-3f);
|
||||
EXPECT_NEAR(focalPointInViewSpace.y, 0.0f, 1e-3f);
|
||||
EXPECT_NEAR(focalPointInViewSpace.z, controller.GetDistance(), 1e-3f);
|
||||
}
|
||||
|
||||
TEST(SceneViewportCameraController_Test, LookInputRotatesCameraInPlaceAndKeepsDistance) {
|
||||
SceneViewportCameraController controller;
|
||||
controller.Reset();
|
||||
@@ -57,7 +73,7 @@ TEST(SceneViewportCameraController_Test, LookInputRotatesCameraInPlaceAndKeepsDi
|
||||
|
||||
EXPECT_TRUE(NearlyEqual(controller.GetPosition(), initialPosition));
|
||||
EXPECT_FALSE(NearlyEqual(controller.GetFocalPoint(), initialFocus));
|
||||
EXPECT_LT(controller.GetPitchDegrees(), initialPitch);
|
||||
EXPECT_GT(controller.GetPitchDegrees(), initialPitch);
|
||||
EXPECT_TRUE(NearlyEqual(
|
||||
controller.GetFocalPoint(),
|
||||
controller.GetPosition() + controller.GetForward() * controller.GetDistance(),
|
||||
@@ -81,7 +97,7 @@ TEST(SceneViewportCameraController_Test, OrbitInputRotatesAroundFocalPointAndKee
|
||||
|
||||
EXPECT_FALSE(NearlyEqual(controller.GetPosition(), initialPosition));
|
||||
EXPECT_TRUE(NearlyEqual(controller.GetFocalPoint(), initialFocus));
|
||||
EXPECT_LT(controller.GetPitchDegrees(), initialPitch);
|
||||
EXPECT_GT(controller.GetPitchDegrees(), initialPitch);
|
||||
EXPECT_NEAR((controller.GetFocalPoint() - controller.GetPosition()).Magnitude(), initialDistance, 1e-3f);
|
||||
}
|
||||
|
||||
@@ -107,8 +123,8 @@ TEST(SceneViewportCameraController_Test, PanAndZoomUpdateCameraStateConsistently
|
||||
EXPECT_LT(controller.GetDistance(), initialDistance);
|
||||
const Vector3 panDelta = controller.GetFocalPoint() - initialFocus;
|
||||
EXPECT_NEAR(Vector3::Dot(panDelta, controller.GetForward()), 0.0f, 1e-3f);
|
||||
EXPECT_GT(std::abs(Vector3::Dot(panDelta, right)), 0.0f);
|
||||
EXPECT_GT(std::abs(Vector3::Dot(panDelta, up)), 0.0f);
|
||||
EXPECT_LT(Vector3::Dot(panDelta, right), 0.0f);
|
||||
EXPECT_LT(Vector3::Dot(panDelta, up), 0.0f);
|
||||
EXPECT_TRUE(NearlyEqual(
|
||||
controller.GetFocalPoint(),
|
||||
controller.GetPosition() + controller.GetForward() * controller.GetDistance(),
|
||||
@@ -135,11 +151,65 @@ TEST(SceneViewportCameraController_Test, FlyInputMovesCameraAndFocalPointTogethe
|
||||
EXPECT_FALSE(NearlyEqual(controller.GetPosition(), initialPosition));
|
||||
EXPECT_FALSE(NearlyEqual(controller.GetFocalPoint(), initialFocus));
|
||||
const Vector3 positionDelta = controller.GetPosition() - initialPosition;
|
||||
EXPECT_GT(std::abs(Vector3::Dot(positionDelta, forward)), 0.0f);
|
||||
EXPECT_GT(std::abs(Vector3::Dot(positionDelta, right)), 0.0f);
|
||||
EXPECT_GT(Vector3::Dot(positionDelta, forward), 0.0f);
|
||||
EXPECT_GT(Vector3::Dot(positionDelta, right), 0.0f);
|
||||
EXPECT_TRUE(NearlyEqual(controller.GetFocalPoint() - controller.GetPosition(), initialOffset, 1e-3f));
|
||||
}
|
||||
|
||||
TEST(SceneViewportCameraController_Test, ZoomDoesNotChangeFlySpeed) {
|
||||
SceneViewportCameraController zoomedController;
|
||||
zoomedController.Reset();
|
||||
const Vector3 zoomedInitialPosition = zoomedController.GetPosition();
|
||||
|
||||
SceneViewportCameraController baselineController;
|
||||
baselineController.Reset();
|
||||
const Vector3 baselineInitialPosition = baselineController.GetPosition();
|
||||
|
||||
SceneViewportCameraInputState zoomInput = {};
|
||||
zoomInput.viewportHeight = 720.0f;
|
||||
zoomInput.zoomDelta = 8.0f;
|
||||
zoomedController.ApplyInput(zoomInput);
|
||||
|
||||
SceneViewportCameraInputState moveInput = {};
|
||||
moveInput.viewportHeight = 720.0f;
|
||||
moveInput.deltaTime = 0.5f;
|
||||
moveInput.moveForward = 1.0f;
|
||||
zoomedController.ApplyInput(moveInput);
|
||||
baselineController.ApplyInput(moveInput);
|
||||
|
||||
EXPECT_FLOAT_EQ(zoomedController.GetFlySpeed(), baselineController.GetFlySpeed());
|
||||
const float zoomedTravel = (zoomedController.GetPosition() - zoomedInitialPosition).Magnitude();
|
||||
const float baselineTravel = (baselineController.GetPosition() - baselineInitialPosition).Magnitude();
|
||||
EXPECT_NEAR(zoomedTravel, baselineTravel, 1e-3f);
|
||||
}
|
||||
|
||||
TEST(SceneViewportCameraController_Test, FlySpeedDeltaAdjustsMovementSpeedIndependentlyFromZoom) {
|
||||
SceneViewportCameraController fasterController;
|
||||
fasterController.Reset();
|
||||
const Vector3 fasterInitialPosition = fasterController.GetPosition();
|
||||
|
||||
SceneViewportCameraController baselineController;
|
||||
baselineController.Reset();
|
||||
const Vector3 baselineInitialPosition = baselineController.GetPosition();
|
||||
|
||||
SceneViewportCameraInputState speedInput = {};
|
||||
speedInput.viewportHeight = 720.0f;
|
||||
speedInput.flySpeedDelta = 4.0f;
|
||||
fasterController.ApplyInput(speedInput);
|
||||
|
||||
SceneViewportCameraInputState moveInput = {};
|
||||
moveInput.viewportHeight = 720.0f;
|
||||
moveInput.deltaTime = 0.5f;
|
||||
moveInput.moveForward = 1.0f;
|
||||
fasterController.ApplyInput(moveInput);
|
||||
baselineController.ApplyInput(moveInput);
|
||||
|
||||
const float fasterTravel = (fasterController.GetPosition() - fasterInitialPosition).Magnitude();
|
||||
const float baselineTravel = (baselineController.GetPosition() - baselineInitialPosition).Magnitude();
|
||||
EXPECT_GT(fasterController.GetFlySpeed(), baselineController.GetFlySpeed());
|
||||
EXPECT_GT(fasterTravel, baselineTravel);
|
||||
}
|
||||
|
||||
TEST(SceneViewportCameraController_Test, FocusMovesPivotWithoutChangingDistance) {
|
||||
SceneViewportCameraController controller;
|
||||
controller.Reset();
|
||||
|
||||
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