From 35f2d342a00888bca692a8e044648d73f3e92d87 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Sun, 26 Mar 2017 21:32:57 +0100 Subject: [PATCH] Added support for ImGui context update and rendering: - Added ImGui Module Manager that that implements module logic and manages other module resources. - Added Texture Manager to manage texture resources and maps them to index that can be passed to ImGui context. - Added Context Proxy that represents and manages a single ImGui context. - Added Slate ImGui Widget to render ImGui output. --- Source/ImGui/ImGui.Build.cs | 4 + Source/ImGui/Private/ImGuiContextProxy.cpp | 96 +++++++++++ Source/ImGui/Private/ImGuiContextProxy.h | 47 +++++ Source/ImGui/Private/ImGuiDemo.cpp | 39 +++++ Source/ImGui/Private/ImGuiDemo.h | 22 +++ Source/ImGui/Private/ImGuiDrawData.cpp | 59 +++++++ Source/ImGui/Private/ImGuiDrawData.h | 58 +++++++ Source/ImGui/Private/ImGuiInteroperability.h | 42 +++++ Source/ImGui/Private/ImGuiModule.cpp | 17 +- Source/ImGui/Private/ImGuiModuleManager.cpp | 172 +++++++++++++++++++ Source/ImGui/Private/ImGuiModuleManager.h | 70 ++++++++ Source/ImGui/Private/ImGuiPrivatePCH.h | 1 + Source/ImGui/Private/SImGuiWidget.cpp | 65 +++++++ Source/ImGui/Private/SImGuiWidget.h | 31 ++++ Source/ImGui/Private/TextureManager.cpp | 76 ++++++++ Source/ImGui/Private/TextureManager.h | 95 ++++++++++ Source/ImGui/Private/Utilities/ScopeGuards.h | 58 +++++++ 17 files changed, 949 insertions(+), 3 deletions(-) create mode 100644 Source/ImGui/Private/ImGuiContextProxy.cpp create mode 100644 Source/ImGui/Private/ImGuiContextProxy.h create mode 100644 Source/ImGui/Private/ImGuiDemo.cpp create mode 100644 Source/ImGui/Private/ImGuiDemo.h create mode 100644 Source/ImGui/Private/ImGuiDrawData.cpp create mode 100644 Source/ImGui/Private/ImGuiDrawData.h create mode 100644 Source/ImGui/Private/ImGuiInteroperability.h create mode 100644 Source/ImGui/Private/ImGuiModuleManager.cpp create mode 100644 Source/ImGui/Private/ImGuiModuleManager.h create mode 100644 Source/ImGui/Private/SImGuiWidget.cpp create mode 100644 Source/ImGui/Private/SImGuiWidget.h create mode 100644 Source/ImGui/Private/TextureManager.cpp create mode 100644 Source/ImGui/Private/TextureManager.h create mode 100644 Source/ImGui/Private/Utilities/ScopeGuards.h diff --git a/Source/ImGui/ImGui.Build.cs b/Source/ImGui/ImGui.Build.cs index a6a6b88..c7697d0 100644 --- a/Source/ImGui/ImGui.Build.cs +++ b/Source/ImGui/ImGui.Build.cs @@ -39,6 +39,10 @@ public class ImGui : ModuleRules PrivateDependencyModuleNames.AddRange( new string[] { + "CoreUObject", + "Engine", + "Slate", + "SlateCore" // ... add private dependencies that you statically link with here ... } ); diff --git a/Source/ImGui/Private/ImGuiContextProxy.cpp b/Source/ImGui/Private/ImGuiContextProxy.cpp new file mode 100644 index 0000000..2362b1a --- /dev/null +++ b/Source/ImGui/Private/ImGuiContextProxy.cpp @@ -0,0 +1,96 @@ +// Distributed under the MIT License (MIT) (see accompanying LICENSE file) + +#include "ImGuiPrivatePCH.h" + +#include "ImGuiContextProxy.h" + + +static constexpr float DEFAULT_CANVAS_WIDTH = 3840.f; +static constexpr float DEFAULT_CANVAS_HEIGHT = 2160.f; + +FImGuiContextProxy::FImGuiContextProxy() +{ + ImGuiIO& IO = ImGui::GetIO(); + + // Use pre-defined canvas size. + IO.DisplaySize.x = DEFAULT_CANVAS_WIDTH; + IO.DisplaySize.y = DEFAULT_CANVAS_HEIGHT; + + // Load texture atlas. + unsigned char* Pixels; + IO.Fonts->GetTexDataAsRGBA32(&Pixels, nullptr, nullptr); + + // Begin frame to complete context initialization (this is to avoid problems with other systems calling to ImGui + // during startup). + BeginFrame(); +} + +FImGuiContextProxy::~FImGuiContextProxy() +{ + ImGui::Shutdown(); +} + +void FImGuiContextProxy::Tick(float DeltaSeconds) +{ + if (bIsFrameStarted) + { + // Broadcast draw event to allow listeners to draw their controls to this context. + if (DrawEvent.IsBound()) + { + DrawEvent.Broadcast(); + } + + // Ending frame will produce render output that we capture and store for later use. This also puts context to + // state in which it does not allow to draw controls, so we want to immediately start a new frame. + EndFrame(); + } + + // Begin a new frame and set the context back to a state in which it allows to draw controls. + BeginFrame(DeltaSeconds); +} + +void FImGuiContextProxy::BeginFrame(float DeltaTime) +{ + if (!bIsFrameStarted) + { + ImGuiIO& IO = ImGui::GetIO(); + IO.DeltaTime = DeltaTime; + + ImGui::NewFrame(); + + bIsFrameStarted = true; + } +} + +void FImGuiContextProxy::EndFrame() +{ + if (bIsFrameStarted) + { + // Prepare draw data (after this call we cannot draw to this context until we start a new frame). + ImGui::Render(); + + // Update our draw data, so we can use them later during Slate rendering while ImGui is in the middle of the + // next frame. + UpdateDrawData(ImGui::GetDrawData()); + + bIsFrameStarted = false; + } +} + +void FImGuiContextProxy::UpdateDrawData(ImDrawData* DrawData) +{ + if (DrawData && DrawData->CmdListsCount > 0) + { + DrawLists.SetNum(DrawData->CmdListsCount, false); + + for (int Index = 0; Index < DrawData->CmdListsCount; Index++) + { + DrawLists[Index].TransferDrawData(*DrawData->CmdLists[Index]); + } + } + else + { + // If we are not rendering then this might be a good moment to empty the array. + DrawLists.Empty(); + } +} diff --git a/Source/ImGui/Private/ImGuiContextProxy.h b/Source/ImGui/Private/ImGuiContextProxy.h new file mode 100644 index 0000000..6c8c359 --- /dev/null +++ b/Source/ImGui/Private/ImGuiContextProxy.h @@ -0,0 +1,47 @@ +// Distributed under the MIT License (MIT) (see accompanying LICENSE file) + +#pragma once + +#include "ImGuiDrawData.h" + +#include + + +// Represents a single ImGui context. All the context updates should be done through this proxy. During update it +// broadcasts draw events to allow listeners draw their controls. After update it stores produced draw data. +// TODO: Add dynamically created contexts, so we can have a better support for multi-PIE. +class FImGuiContextProxy +{ +public: + + FImGuiContextProxy(); + ~FImGuiContextProxy(); + + FImGuiContextProxy(const FImGuiContextProxy&) = delete; + FImGuiContextProxy& operator=(const FImGuiContextProxy&) = delete; + + FImGuiContextProxy(FImGuiContextProxy&&) = delete; + FImGuiContextProxy& operator=(FImGuiContextProxy&&) = delete; + + // Get draw data from the last frame. + const TArray& GetDrawData() const { return DrawLists; } + + // Delegate called right before ending the frame to allows listeners draw their controls. + FSimpleMulticastDelegate& OnDraw() { return DrawEvent; } + + // Tick to advance context to the next frame. + void Tick(float DeltaSeconds); + +private: + + void BeginFrame(float DeltaTime = 1.f / 60.f); + void EndFrame(); + + void UpdateDrawData(ImDrawData* DrawData); + + TArray DrawLists; + + FSimpleMulticastDelegate DrawEvent; + + bool bIsFrameStarted = false; +}; diff --git a/Source/ImGui/Private/ImGuiDemo.cpp b/Source/ImGui/Private/ImGuiDemo.cpp new file mode 100644 index 0000000..4d65904 --- /dev/null +++ b/Source/ImGui/Private/ImGuiDemo.cpp @@ -0,0 +1,39 @@ +// Distributed under the MIT License (MIT) (see accompanying LICENSE file) + +#include "ImGuiPrivatePCH.h" + +#include "ImGuiDemo.h" +#include "ImGuiModuleManager.h" + + +// Demo copied from ImGui examples. See https://github.com/ocornut/imgui. +void FImGuiDemo::DrawControls() +{ + // 1. Show a simple window + // Tip: if we don't call ImGui::Begin()/ImGui::End() the widgets appears in a window automatically called "Debug" + { + static float f = 0.0f; + ImGui::Text("Hello, world!"); + ImGui::SliderFloat("float", &f, 0.0f, 1.0f); + ImGui::ColorEdit3("clear color", (float*)&ClearColor); + if (ImGui::Button("Test Window")) bDemoShowTestWindow = !bDemoShowTestWindow; + if (ImGui::Button("Another Window")) bDemoShowAnotherTestWindow = !bDemoShowAnotherTestWindow; + ImGui::Text("Application average %.3f ms/frame (%.1f FPS)", 1000.0f / ImGui::GetIO().Framerate, ImGui::GetIO().Framerate); + } + + // 2. Show another simple window, this time using an explicit Begin/End pair + if (bDemoShowAnotherTestWindow) + { + ImGui::SetNextWindowSize(ImVec2(200, 100), ImGuiSetCond_FirstUseEver); + ImGui::Begin("Another Window", &bDemoShowAnotherTestWindow); + ImGui::Text("Hello"); + ImGui::End(); + } + + // 3. Show the ImGui test window. Most of the sample code is in ImGui::ShowTestWindow() + if (bDemoShowTestWindow) + { + ImGui::SetNextWindowPos(ImVec2(650, 20), ImGuiSetCond_FirstUseEver); + ImGui::ShowTestWindow(&bDemoShowTestWindow); + } +} diff --git a/Source/ImGui/Private/ImGuiDemo.h b/Source/ImGui/Private/ImGuiDemo.h new file mode 100644 index 0000000..4aa0b90 --- /dev/null +++ b/Source/ImGui/Private/ImGuiDemo.h @@ -0,0 +1,22 @@ +// Distributed under the MIT License (MIT) (see accompanying LICENSE file) + +#pragma once + +#include + + +// Widget drawing ImGui demo. +class FImGuiDemo +{ +public: + + void DrawControls(); + +private: + + ImVec4 ClearColor = ImColor{ 114, 144, 154 }; + + bool bShowDemo = false; + bool bDemoShowTestWindow = true; + bool bDemoShowAnotherTestWindow = false; +}; diff --git a/Source/ImGui/Private/ImGuiDrawData.cpp b/Source/ImGui/Private/ImGuiDrawData.cpp new file mode 100644 index 0000000..479e30a --- /dev/null +++ b/Source/ImGui/Private/ImGuiDrawData.cpp @@ -0,0 +1,59 @@ +// Distributed under the MIT License (MIT) (see accompanying LICENSE file) + +#include "ImGuiPrivatePCH.h" + +#include "ImGuiDrawData.h" + + +void FImGuiDrawList::CopyVertexData(TArray& OutVertexBuffer, const FVector2D VertexPositionOffset, const FSlateRotatedRect& VertexClippingRect) const +{ + // Reset and reserve space in destination buffer. + OutVertexBuffer.SetNumUninitialized(ImGuiVertexBuffer.Size, false); + + // Transform and copy vertex data. + for (int Idx = 0; Idx < ImGuiVertexBuffer.Size; Idx++) + { + const ImDrawVert& ImGuiVertex = ImGuiVertexBuffer[Idx]; + FSlateVertex& SlateVertex = OutVertexBuffer[Idx]; + + // Final UV is calculated in shader as XY * ZW, so we need set all components. + SlateVertex.TexCoords[0] = ImGuiVertex.uv.x; + SlateVertex.TexCoords[1] = ImGuiVertex.uv.y; + SlateVertex.TexCoords[2] = SlateVertex.TexCoords[3] = 1.f; + + // Copy ImGui position and add offset. + SlateVertex.Position[0] = ImGuiVertex.pos.x + VertexPositionOffset.X; + SlateVertex.Position[1] = ImGuiVertex.pos.y + VertexPositionOffset.Y; + + // Set clipping rectangle. + SlateVertex.ClipRect = VertexClippingRect; + + // Unpack ImU32 color. + SlateVertex.Color = ImGuiInterops::UnpackImU32Color(ImGuiVertex.col); + } +} + +void FImGuiDrawList::CopyIndexData(TArray& OutIndexBuffer, const int32 StartIndex, const int32 NumElements) const +{ + // Reset buffer. + OutIndexBuffer.SetNumUninitialized(NumElements, false); + + // Copy elements (slow copy because of different sizes of ImDrawIdx and SlateIndex and because SlateIndex can + // have different size on different platforms). + for (int i = 0; i < NumElements; i++) + { + OutIndexBuffer[i] = ImGuiIndexBuffer[StartIndex + i]; + } +} + +void FImGuiDrawList::TransferDrawData(ImDrawList& Src) +{ + // Move data from source to this list. + Src.CmdBuffer.swap(ImGuiCommandBuffer); + Src.IdxBuffer.swap(ImGuiIndexBuffer); + Src.VtxBuffer.swap(ImGuiVertexBuffer); + + // ImGui seems to clear draw lists in every frame, but since source list can contain pointers to buffers that + // we just swapped, it is better to clear explicitly here. + Src.Clear(); +} diff --git a/Source/ImGui/Private/ImGuiDrawData.h b/Source/ImGui/Private/ImGuiDrawData.h new file mode 100644 index 0000000..9293cc8 --- /dev/null +++ b/Source/ImGui/Private/ImGuiDrawData.h @@ -0,0 +1,58 @@ +// Distributed under the MIT License (MIT) (see accompanying LICENSE file) + +#pragma once + +#include "ImGuiInteroperability.h" + +#include + +#include + + +// ImGui draw command data transformed for Slate. +struct FImGuiDrawCommand +{ + uint32 NumElements; + FSlateRect ClippingRect; + TextureIndex TextureId; +}; + +// Wraps raw ImGui draw list data in utilities that transform them for Slate. +class FImGuiDrawList +{ +public: + + // Get the number of draw commands in this list. + FORCEINLINE int NumCommands() const { return ImGuiCommandBuffer.Size; } + + // Get the draw command by number. + // @param CommandNb - Number of draw command + // @returns Draw command data + FORCEINLINE FImGuiDrawCommand GetCommand(int CommandNb) const + { + const ImDrawCmd& ImGuiCommand = ImGuiCommandBuffer[CommandNb]; + return{ ImGuiCommand.ElemCount, ImGuiInterops::ToSlateRect(ImGuiCommand.ClipRect), ImGuiInterops::ToTextureIndex(ImGuiCommand.TextureId) }; + } + + // Transform and copy vertex data to target buffer (old data in the target buffer are replaced). + // @param OutVertexBuffer - Destination buffer + // @param VertexPositionOffset - Position offset added to every vertex to transform it to different space + // @param VertexClippingRect - Clipping rectangle for Slate vertices + void CopyVertexData(TArray& OutVertexBuffer, const FVector2D VertexPositionOffset, const FSlateRotatedRect& VertexClippingRect) const; + + // Transform and copy index data to target buffer (old data in the target buffer are replaced). + // Internal index buffer contains enough data to match the sum of NumElements from all draw commands. + // @param OutIndexBuffer - Destination buffer + // @param StartIndex - Start copying source data starting from this index + // @param NumElements - How many elements we want to copy + void CopyIndexData(TArray& OutIndexBuffer, const int32 StartIndex, const int32 NumElements) const; + + // Transfers data from ImGui source list to this object. Leaves source cleared. + void TransferDrawData(ImDrawList& Src); + +private: + + ImVector ImGuiCommandBuffer; + ImVector ImGuiIndexBuffer; + ImVector ImGuiVertexBuffer; +}; diff --git a/Source/ImGui/Private/ImGuiInteroperability.h b/Source/ImGui/Private/ImGuiInteroperability.h new file mode 100644 index 0000000..9a98266 --- /dev/null +++ b/Source/ImGui/Private/ImGuiInteroperability.h @@ -0,0 +1,42 @@ +// Distributed under the MIT License (MIT) (see accompanying LICENSE file) + +#pragma once + +#include "TextureManager.h" + +#include + + +// Utilities to help standardise operations between Unreal and ImGui. +namespace ImGuiInterops +{ + //==================================================================================================== + // Conversions + //==================================================================================================== + + // Convert from ImGui packed color to FColor. + FORCEINLINE FColor UnpackImU32Color(ImU32 Color) + { + // We use IM_COL32_R/G/B/A_SHIFT macros to support different ImGui configurations. + return FColor{ ((Color >> IM_COL32_R_SHIFT) & 0xFF), ((Color >> IM_COL32_G_SHIFT) & 0xFF), + ((Color >> IM_COL32_B_SHIFT) & 0xFF), ((Color >> IM_COL32_A_SHIFT) & 0xFF) }; + } + + // Convert from ImVec4 rectangle to FSlateRect. + FORCEINLINE FSlateRect ToSlateRect(const ImVec4& ImGuiRect) + { + return FSlateRect{ ImGuiRect.x, ImGuiRect.y, ImGuiRect.z, ImGuiRect.w }; + } + + // Convert from ImGui Texture Id to Texture Index that we use for texture resources. + FORCEINLINE TextureIndex ToTextureIndex(ImTextureID Index) + { + return static_cast(reinterpret_cast(Index)); + } + + // Convert from Texture Index to ImGui Texture Id that we pass to ImGui. + FORCEINLINE ImTextureID ToImTextureID(TextureIndex Index) + { + return reinterpret_cast(static_cast(Index)); + } +} diff --git a/Source/ImGui/Private/ImGuiModule.cpp b/Source/ImGui/Private/ImGuiModule.cpp index 583abc8..e9da305 100644 --- a/Source/ImGui/Private/ImGuiModule.cpp +++ b/Source/ImGui/Private/ImGuiModule.cpp @@ -2,20 +2,31 @@ #include "ImGuiPrivatePCH.h" +#include "ImGuiModuleManager.h" + #include #define LOCTEXT_NAMESPACE "FImGuiModule" + +static FImGuiModuleManager* ModuleManager = nullptr; + void FImGuiModule::StartupModule() { - // This code will execute after your module is loaded into memory; the exact timing is specified in the .uplugin file per-module + checkf(!ModuleManager, TEXT("Instance of Module Manager already exists. Instance should be created only during module startup.")); + + // Create module manager that implements modules logic. + ModuleManager = new FImGuiModuleManager(); } void FImGuiModule::ShutdownModule() { - // This function may be called during shutdown to clean up your module. For modules that support dynamic reloading, - // we call this function before unloading the module. + checkf(ModuleManager, TEXT("Null Module Manager. Manager instance should be deleted during module shutdown.")); + + // Before we shutdown we need to delete manager that will do all necessary cleanup. + delete ModuleManager; + ModuleManager = nullptr; } #undef LOCTEXT_NAMESPACE diff --git a/Source/ImGui/Private/ImGuiModuleManager.cpp b/Source/ImGui/Private/ImGuiModuleManager.cpp new file mode 100644 index 0000000..20d6e2d --- /dev/null +++ b/Source/ImGui/Private/ImGuiModuleManager.cpp @@ -0,0 +1,172 @@ +// Distributed under the MIT License (MIT) (see accompanying LICENSE file) + +#include "ImGuiPrivatePCH.h" + +#include "ImGuiModuleManager.h" + +#include "ImGuiInteroperability.h" + +#include + + +FImGuiModuleManager::FImGuiModuleManager() +{ + // Bind ImGui demo to proxy, so it can draw controls in its context. + ContextProxy.OnDraw().AddRaw(&ImGuiDemo, &FImGuiDemo::DrawControls); + + // Typically we will use viewport created events to add widget to new game viewports. + ViewportCreatedHandle = UGameViewportClient::OnViewportCreated().AddRaw(this, &FImGuiModuleManager::OnViewportCreated); + + // Initialize resources and start ticking. Depending on loading phase, this may fail if Slate is not yet ready. + Initialize(); + + // We need to add widgets to active game viewports as they won't generate on-created events. This is especially + // important during hot-reloading. + if (bInitialized) + { + AddWidgetToAllViewports(); + } +} + +FImGuiModuleManager::~FImGuiModuleManager() +{ + // We are no longer interested with adding widgets to viewports. + if (ViewportCreatedHandle.IsValid()) + { + UGameViewportClient::OnViewportCreated().Remove(ViewportCreatedHandle); + ViewportCreatedHandle.Reset(); + } + + // Deactivate this manager. + Uninitialize(); +} + +void FImGuiModuleManager::Initialize() +{ + // We rely on Slate, so we can only continue if it is already initialized. + if (!bInitialized && FSlateApplication::IsInitialized()) + { + bInitialized = true; + LoadTextures(); + RegisterTick(); + } +} + +void FImGuiModuleManager::Uninitialize() +{ + if (bInitialized) + { + bInitialized = false; + UnregisterTick(); + } +} + +void FImGuiModuleManager::LoadTextures() +{ + checkf(FSlateApplication::IsInitialized(), TEXT("Slate should be initialized before we can create textures.")); + + // Create an empty texture at index 0. We will use it for ImGui outputs with null texture id. + TextureManager.CreatePlainTexture(FName{ "ImGuiModule_Null" }, 2, 2, FColor::White); + + // Create a font atlas texture. + ImFontAtlas* Fonts = ImGui::GetIO().Fonts; + checkf(Fonts, TEXT("Invalid font atlas.")); + + unsigned char* Pixels; + int Width, Height, Bpp; + Fonts->GetTexDataAsRGBA32(&Pixels, &Width, &Height, &Bpp); + + TextureIndex FontsTexureIndex = TextureManager.CreateTexture(FName{ "ImGuiModule_FontAtlas" }, Width, Height, Bpp, Pixels, false); + + // Set font texture index in ImGui. + Fonts->TexID = ImGuiInterops::ToImTextureID(FontsTexureIndex); +} + +void FImGuiModuleManager::RegisterTick() +{ + checkf(FSlateApplication::IsInitialized(), TEXT("Slate should be initialized before we can register tick listener.")); + + // We will tick on Slate Post-Tick events. They are quite convenient as they happen at the very end of the frame, + // what helps to minimise tearing. + if (!TickDelegateHandle.IsValid()) + { + TickDelegateHandle = FSlateApplication::Get().OnPostTick().AddRaw(this, &FImGuiModuleManager::Tick); + } +} + +void FImGuiModuleManager::UnregisterTick() +{ + if (TickDelegateHandle.IsValid()) + { + if (FSlateApplication::IsInitialized()) + { + FSlateApplication::Get().OnPostTick().Remove(TickDelegateHandle); + } + TickDelegateHandle.Reset(); + } +} + +bool FImGuiModuleManager::IsInUpdateThread() +{ + // We can get ticks from the Game thread and Slate loading thread. In both cases IsInGameThread() is true, so we + // need to make additional test to filter out loading thread. + return IsInGameThread() && !IsInSlateThread(); +} + +void FImGuiModuleManager::Tick(float DeltaSeconds) +{ + if (IsInUpdateThread()) + { + // Update context proxy to advance to next frame. + ContextProxy.Tick(DeltaSeconds); + } +} + +void FImGuiModuleManager::OnViewportCreated() +{ + checkf(FSlateApplication::IsInitialized(), TEXT("We expect Slate to be initialized when game viewport is created.")); + + // Make sure that all resources are initialized to handle configurations where this module is loaded before Slate. + Initialize(); + + // Create widget to viewport responsible for this event. + AddWidgetToViewport(GEngine->GameViewport); +} + +void FImGuiModuleManager::AddWidgetToViewport(UGameViewportClient* GameViewport) +{ + checkf(GameViewport, TEXT("Null game viewport.")); + checkf(FSlateApplication::IsInitialized(), TEXT("Slate should be initialized before we can add widget to game viewports.")); + + if (!ViewportWidget.IsValid()) + { + SAssignNew(ViewportWidget, SImGuiWidget).ModuleManager(this); + checkf(ViewportWidget.IsValid(), TEXT("Failed to create SImGuiWidget.")); + } + + // High enough z-order guarantees that ImGui output is rendered on top of the game UI. + constexpr int32 IMGUI_WIDGET_Z_ORDER = 10000; + + GameViewport->AddViewportWidgetContent(SNew(SWeakWidget).PossiblyNullContent(ViewportWidget), IMGUI_WIDGET_Z_ORDER); +} + +void FImGuiModuleManager::AddWidgetToAllViewports() +{ + checkf(FSlateApplication::IsInitialized(), TEXT("Slate should be initialized before we can add widget to game viewports.")); + + if (GEngine) + { + // Loop as long as we have a valid viewport or until we detect a cycle. + UGameViewportClient* GameViewport = GEngine->GameViewport; + while (GameViewport) + { + AddWidgetToViewport(GameViewport); + + GameViewport = GEngine->GetNextPIEViewport(GameViewport); + if (GameViewport == GEngine->GameViewport) + { + break; + } + } + } +} diff --git a/Source/ImGui/Private/ImGuiModuleManager.h b/Source/ImGui/Private/ImGuiModuleManager.h new file mode 100644 index 0000000..1071199 --- /dev/null +++ b/Source/ImGui/Private/ImGuiModuleManager.h @@ -0,0 +1,70 @@ +// Distributed under the MIT License (MIT) (see accompanying LICENSE file) + +#pragma once + +#include "ImGuiContextProxy.h" +#include "ImGuiDemo.h" +#include "SImGuiWidget.h" +#include "TextureManager.h" + + +// Central manager that implements module logic. It initializes and controls remaining module components. +class FImGuiModuleManager +{ + // Allow module to control life-cycle of this class. + friend class FImGuiModule; + +public: + + // Get ImGui context proxy. + FImGuiContextProxy& GetContextProxy() { return ContextProxy; } + + // Get texture resources manager. + FTextureManager& GetTextureManager() { return TextureManager; } + +private: + + FImGuiModuleManager(); + ~FImGuiModuleManager(); + + FImGuiModuleManager(const FImGuiModuleManager&) = delete; + FImGuiModuleManager& operator=(const FImGuiModuleManager&) = delete; + + FImGuiModuleManager(FImGuiModuleManager&&) = delete; + FImGuiModuleManager& operator=(FImGuiModuleManager&&) = delete; + + void Initialize(); + void Uninitialize(); + + void LoadTextures(); + + void RegisterTick(); + void UnregisterTick(); + + bool IsInUpdateThread(); + + void Tick(float DeltaSeconds); + + void OnViewportCreated(); + + void AddWidgetToViewport(UGameViewportClient* GameViewport); + void AddWidgetToAllViewports(); + + // Proxy controlling ImGui context. + FImGuiContextProxy ContextProxy; + + // ImWidget that draws ImGui demo. + FImGuiDemo ImGuiDemo; + + // Manager for textures resources. + FTextureManager TextureManager; + + // Slate widget that we attach to created game viewports (widget without per-viewport state can be attached to + // multiple viewports). + TSharedPtr ViewportWidget; + + FDelegateHandle TickDelegateHandle; + FDelegateHandle ViewportCreatedHandle; + + bool bInitialized = false; +}; diff --git a/Source/ImGui/Private/ImGuiPrivatePCH.h b/Source/ImGui/Private/ImGuiPrivatePCH.h index 3ab684f..8ca3fce 100644 --- a/Source/ImGui/Private/ImGuiPrivatePCH.h +++ b/Source/ImGui/Private/ImGuiPrivatePCH.h @@ -5,6 +5,7 @@ // Engine #include +#include // You should place include statements to your module's private header files here. You only need to // add includes for headers that are used in most of your module's source files though. diff --git a/Source/ImGui/Private/SImGuiWidget.cpp b/Source/ImGui/Private/SImGuiWidget.cpp new file mode 100644 index 0000000..3316333 --- /dev/null +++ b/Source/ImGui/Private/SImGuiWidget.cpp @@ -0,0 +1,65 @@ +// Distributed under the MIT License (MIT) (see accompanying LICENSE file) + +#include "ImGuiPrivatePCH.h" + +#include "SImGuiWidget.h" + +#include "ImGuiModuleManager.h" +#include "TextureManager.h" +#include "Utilities/ScopeGuards.h" + + +BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION +void SImGuiWidget::Construct(const FArguments& InArgs) +{ + checkf(InArgs._ModuleManager, TEXT("Null Module Manager argument")); + ModuleManager = InArgs._ModuleManager; +} +END_SLATE_FUNCTION_BUILD_OPTIMIZATION + +int32 SImGuiWidget::OnPaint(const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyClippingRect, + FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& WidgetStyle, bool bParentEnabled) const +{ + // Calculate offset that will transform vertex positions to screen space - rounded to avoid half pixel offsets. + const FVector2D VertexPositionOffset{ FMath::RoundToFloat(MyClippingRect.Left), FMath::RoundToFloat(MyClippingRect.Top) }; + + // Convert clipping rectangle to format required by Slate vertex. + const FSlateRotatedRect VertexClippingRect{ MyClippingRect }; + + for (const auto& DrawList : ModuleManager->GetContextProxy().GetDrawData()) + { + DrawList.CopyVertexData(VertexBuffer, VertexPositionOffset, VertexClippingRect); + + // Get access to the Slate scissor rectangle defined in Slate Core API, so we can customize elements drawing. + extern SLATECORE_API TOptional GSlateScissorRect; + + auto GSlateScissorRectSaver = ScopeGuards::MakeStateSaver(GSlateScissorRect); + + int IndexBufferOffset = 0; + for (int CommandNb = 0; CommandNb < DrawList.NumCommands(); CommandNb++) + { + const auto& DrawCommand = DrawList.GetCommand(CommandNb); + + DrawList.CopyIndexData(IndexBuffer, IndexBufferOffset, DrawCommand.NumElements); + + // Advance offset by number of copied elements to position it for the next command. + IndexBufferOffset += DrawCommand.NumElements; + + // Get texture resource handle for this draw command (null index will be also mapped to a valid texture). + const FSlateResourceHandle& Handle = ModuleManager->GetTextureManager().GetTextureHandle(DrawCommand.TextureId); + + // Transform clipping rectangle to screen space and set in Slate, to apply it to elements that we draw. + GSlateScissorRect = FShortRect{ DrawCommand.ClippingRect.OffsetBy(MyClippingRect.GetTopLeft()).IntersectionWith(MyClippingRect) }; + + // Add elements to the list. + FSlateDrawElement::MakeCustomVerts(OutDrawElements, LayerId, Handle, VertexBuffer, IndexBuffer, nullptr, 0, 0); + } + } + + return LayerId; +} + +FVector2D SImGuiWidget::ComputeDesiredSize(float) const +{ + return FVector2D{ 3840.f, 2160.f }; +} diff --git a/Source/ImGui/Private/SImGuiWidget.h b/Source/ImGui/Private/SImGuiWidget.h new file mode 100644 index 0000000..23d6bae --- /dev/null +++ b/Source/ImGui/Private/SImGuiWidget.h @@ -0,0 +1,31 @@ +// Distributed under the MIT License (MIT) (see accompanying LICENSE file) + +#pragma once + +#include + +class FImGuiModuleManager; + +// Slate widget for rendering ImGui output. +class SImGuiWidget : public SLeafWidget +{ +public: + + SLATE_BEGIN_ARGS(SImGuiWidget) + {} + SLATE_ARGUMENT(FImGuiModuleManager*, ModuleManager) + SLATE_END_ARGS() + + void Construct(const FArguments& InArgs); + +private: + + virtual int32 OnPaint(const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyClippingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& WidgetStyle, bool bParentEnabled) const override; + + virtual FVector2D ComputeDesiredSize(float) const override; + + FImGuiModuleManager* ModuleManager = nullptr; + + mutable TArray VertexBuffer; + mutable TArray IndexBuffer; +}; diff --git a/Source/ImGui/Private/TextureManager.cpp b/Source/ImGui/Private/TextureManager.cpp new file mode 100644 index 0000000..83674f9 --- /dev/null +++ b/Source/ImGui/Private/TextureManager.cpp @@ -0,0 +1,76 @@ +// Distributed under the MIT License (MIT) (see accompanying LICENSE file) + +#include "ImGuiPrivatePCH.h" + +#include "TextureManager.h" + +#include + + +TextureIndex FTextureManager::CreateTexture(const FName& Name, int32 Width, int32 Height, uint32 SrcBpp, uint8* SrcData, bool bDeleteSrcData) +{ + checkf(FindTextureIndex(Name) == INDEX_NONE, TEXT("Trying to create texture using resource name '%s' that is already registered."), *Name.ToString()); + + // Create a texture. + UTexture2D* Texture = UTexture2D::CreateTransient(Width, Height); + + // Create a new resource for that texture. + Texture->UpdateResource(); + + // Update texture data. + FUpdateTextureRegion2D* TextureRegion = new FUpdateTextureRegion2D(0, 0, 0, 0, Width, Height); + auto DataCleanup = [bDeleteSrcData](uint8* Data, const FUpdateTextureRegion2D* UpdateRegion) + { + if (bDeleteSrcData) + { + delete Data; + } + delete UpdateRegion; + }; + Texture->UpdateTextureRegions(0, 1u, TextureRegion, SrcBpp * Width, SrcBpp, SrcData, DataCleanup); + + // Create a new entry for the texture. + return TextureResources.Emplace(Name, Texture); +} + +TextureIndex FTextureManager::CreatePlainTexture(const FName& Name, int32 Width, int32 Height, FColor Color) +{ + // Create buffer with raw data. + const uint32 ColorPacked = Color.ToPackedARGB(); + const uint32 Bpp = sizeof(ColorPacked); + const uint32 SizeInPixels = Width * Height; + const uint32 SizeInBytes = SizeInPixels * Bpp; + uint8* SrcData = new uint8[SizeInBytes]; + std::fill(reinterpret_cast(SrcData), reinterpret_cast(SrcData) + SizeInPixels, ColorPacked); + + // Create new texture from raw data (we created the buffer, so mark it for delete). + return CreateTexture(Name, Width, Height, Bpp, SrcData, true); +} + +FTextureManager::FTextureEntry::FTextureEntry(const FName& InName, UTexture2D* InTexture) + : Name{ InName } + , Texture{ InTexture } +{ + // Add texture to root to prevent garbage collection. + Texture->AddToRoot(); + + // Create brush and resource handle for input texture. + Brush.SetResourceObject(Texture); + ResourceHandle = FSlateApplication::Get().GetRenderer()->GetResourceHandle(Brush); +} + +FTextureManager::FTextureEntry::~FTextureEntry() +{ + // Release brush. + if (Brush.HasUObject() && FSlateApplication::IsInitialized()) + { + FSlateApplication::Get().GetRenderer()->ReleaseDynamicResource(Brush); + } + + // Remove texture from root to allow for garbage collection (it might be already invalid if this is application + // shutdown). + if (Texture && Texture->IsValidLowLevel()) + { + Texture->RemoveFromRoot(); + } +} diff --git a/Source/ImGui/Private/TextureManager.h b/Source/ImGui/Private/TextureManager.h new file mode 100644 index 0000000..f90d3be --- /dev/null +++ b/Source/ImGui/Private/TextureManager.h @@ -0,0 +1,95 @@ +// Distributed under the MIT License (MIT) (see accompanying LICENSE file) + +#pragma once + +#include +#include +#include + + +// Index type to be used as a texture handle. +using TextureIndex = int32; + +// Manager for textures resources which can be referenced by a unique name or index. +// Name is primarily for lookup and index provides a direct access to resources. +class FTextureManager +{ +public: + + // Creates an empty manager. + FTextureManager() = default; + + // Copying is disabled to protected resource ownership. + FTextureManager(const FTextureManager&) = delete; + FTextureManager& operator=(const FTextureManager&) = delete; + + // Moving transfers ownership and leaves source empty. + FTextureManager(FTextureManager&&) = default; + FTextureManager& operator=(FTextureManager&&) = default; + + // Find texture index by name. + // @param Name - The name of a texture to find + // @returns The index of a texture with given name or INDEX_NONE if there is no such texture + TextureIndex FindTextureIndex(const FName& Name) const + { + return TextureResources.IndexOfByPredicate([&](const auto& Entry) { return Entry.Name == Name; }); + } + + // Get the name of a texture at given index. Throws exception if index is out of range. + // @param Index - Index of a texture + // @returns The name of a texture at given index + FORCEINLINE FName GetTextureName(TextureIndex Index) const + { + return TextureResources[Index].Name; + } + + // Get the Slate Resource Handle to a texture at given index. Throws exception if index is out of range. + // @param Index - Index of a texture + // @returns The Slate Resource Handle for a texture at given index + FORCEINLINE const FSlateResourceHandle& GetTextureHandle(TextureIndex Index) const + { + return TextureResources[Index].ResourceHandle; + } + + // Create a texture from raw data. Throws exception if there is already a texture with that name. + // @param Name - The texture name + // @param Width - The texture width + // @param Height - The texture height + // @param SrcBpp - The size in bytes of one pixel + // @param SrcData - The source data + // @param bDeleteSrcData - If true, we should delete source data after creating a texture + // @returns The index of a texture that was created + TextureIndex CreateTexture(const FName& Name, int32 Width, int32 Height, uint32 SrcBpp, uint8* SrcData, bool bDeleteSrc = false); + + // Create a plain texture. Throws exception if there is already a texture with that name. + // @param Name - The texture name + // @param Width - The texture width + // @param Height - The texture height + // @param Color - The texture color + // @returns The index of a texture that was created + TextureIndex CreatePlainTexture(const FName& Name, int32 Width, int32 Height, FColor Color); + +private: + + // Entry for texture resources. Only supports explicit construction. + struct FTextureEntry + { + FTextureEntry(const FName& InName, UTexture2D* InTexture); + ~FTextureEntry(); + + // Copying is not supported. + FTextureEntry(const FTextureEntry&) = delete; + FTextureEntry& operator=(const FTextureEntry&) = delete; + + // We rely on TArray and don't implement custom move semantics. + FTextureEntry(FTextureEntry&&) = delete; + FTextureEntry& operator=(FTextureEntry&&) = delete; + + FName Name = NAME_None; + UTexture2D* Texture = nullptr; + FSlateBrush Brush; + FSlateResourceHandle ResourceHandle; + }; + + TArray TextureResources; +}; diff --git a/Source/ImGui/Private/Utilities/ScopeGuards.h b/Source/ImGui/Private/Utilities/ScopeGuards.h new file mode 100644 index 0000000..2b0d962 --- /dev/null +++ b/Source/ImGui/Private/Utilities/ScopeGuards.h @@ -0,0 +1,58 @@ +// Distributed under the MIT License (MIT) (see accompanying LICENSE file) + +#pragma once + +namespace ScopeGuards +{ + // Saves snapshot of the object state and restores it during destruction. + template + class TStateSaver + { + public: + + // Constructor taking target object in state that we want to save. + TStateSaver(T& Target) + : Ptr(&Target) + , Value(Target) + { + } + + // Move constructor allowing to transfer state out of scope. + TStateSaver(TStateSaver&& Other) + : Ptr(Other.Ptr) + , Value(MoveTemp(Other.Value)) + { + // Release responsibility from the other object (std::exchange currently not supported by all platforms). + Other.Ptr = nullptr; + } + + // Non-assignable to enforce acquisition on construction. + TStateSaver& operator=(TStateSaver&&) = delete; + + // Non-copyable. + TStateSaver(const TStateSaver&) = delete; + TStateSaver& operator=(const TStateSaver&) = delete; + + ~TStateSaver() + { + if (Ptr) + { + *Ptr = Value; + } + } + + private: + + T* Ptr; + T Value; + }; + + // Create a state saver for target object. Unless saver is moved, state will be restored at the end of scope. + // @param Target - Target object in state that we want to save + // @returns State saver that unless moved, will restore target's state during scope exit + template + TStateSaver MakeStateSaver(T& Target) + { + return TStateSaver{ Target }; + } +}