Refine editor viewport and interaction workflow

This commit is contained in:
2026-03-29 15:12:38 +08:00
parent b0427b7091
commit 2651bad080
42 changed files with 3888 additions and 570 deletions

View File

@@ -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
)

View File

@@ -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

View File

@@ -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();

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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
View 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

View File

@@ -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);
}

View File

@@ -120,7 +120,6 @@ private:
}
io.FontDefault = uiFont;
atlas->Build();
}
std::string m_iniPath;

View 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

View File

@@ -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;
}

View File

@@ -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"

View File

@@ -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() };
}

View File

@@ -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,

View File

@@ -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;
};

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View File

@@ -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

View File

@@ -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);

View File

@@ -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);
}
}

View File

@@ -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;
};
}

View File

@@ -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;
}

View File

@@ -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;
};
}

View File

@@ -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,

View File

@@ -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);
};
}

View File

@@ -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})

View File

@@ -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();

View 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));
}

View 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

View 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