Skip to content

Commit

Permalink
Implemented Materials Support as alternative to textures; Added 'Forc…
Browse files Browse the repository at this point in the history
…e Update Image' function to let developers handle runtime changes.
  • Loading branch information
JanSeliv committed Apr 25, 2024
1 parent 0cb3d4b commit 6e8430a
Show file tree
Hide file tree
Showing 9 changed files with 147 additions and 37 deletions.
Binary file modified Binaries/Win64/UnrealEditor-CustomShapeButton.dll
Binary file not shown.
Binary file modified Binaries/Win64/UnrealEditor-CustomShapeButton.pdb
Binary file not shown.
2 changes: 1 addition & 1 deletion Binaries/Win64/UnrealEditor.modules
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"BuildId": "32235091",
"BuildId": "33043543",
"Modules":
{
"CustomShapeButton": "UnrealEditor-CustomShapeButton.dll"
Expand Down
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ The `Custom Shape Button` plugin revolutionizes the way buttons are designed in

With the `Custom Shape Button` plugin, you can now create buttons of any shape or form you envision. Whether you want a circular button, a star-shaped one, or one in the shape of a custom image, this plugin allows you to bring that vision to life. Additionally, the plugin ensures the hover and press behavior works flawlessly with the custom shapes, ensuring a seamless user experience.

<img width="480" alt="CustomShapeButton" src="https://github.com/JanSeliv/CustomShapeButton/assets/20540872/09a504a4-b1aa-4992-8c18-4b2865f65340">
![CustomShapeButton](https://github.com/JanSeliv/CustomShapeButton/assets/20540872/46c3be2c-b325-4528-a626-16a4bb2b4d9c)

## 📚 Documentation

Expand All @@ -15,6 +15,11 @@ Detailed documentation about the Custom Shape Button can be found [here](https:/
Check out our [Release](https://github.com/JanSeliv/CustomShapeButton/releases) page for a sample project showcasing the Custom Shape Button plugin.

## 📅 Changelog
#### 2024-04-25
- Updated to **Unreal Engine 5.4**
- Implemented **Materials Support** as alternative to textures: [doc](https://docs.google.com/document/d/1Ws76obIHRMtsdOjB6YP9K7LTjJR-R56h2uv65PKUBL4/edit#heading=h.jlxkng80vqbe):
> ![image](https://github.com/JanSeliv/CustomShapeButton/assets/20540872/c4a083d2-494e-400f-b363-1ffa795024fa)
#### 2023-10-21
- Updated to **Unreal Engine 5.3**.
#### 2023-06-04
Expand Down
1 change: 1 addition & 0 deletions Source/CustomShapeButton/CustomShapeButton.Build.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ public CustomShapeButton(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs;
CppStandard = CppStandardVersion.Latest;
bEnableNonInlinedGenCppWarnings = true;

PublicDependencyModuleNames.AddRange(new[]
{
Expand Down
9 changes: 9 additions & 0 deletions Source/CustomShapeButton/Private/CustomShapeButton.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,15 @@ TSharedPtr<SCustomShapeButton> UCustomShapeButton::GetSlateCustomShapeButton() c
return StaticCastSharedPtr<SCustomShapeButton>(MyButton);
}

// Forces to update the Raw Colors (pixels data) about current image
void UCustomShapeButton::ForceUpdateImage()
{
if (const TSharedPtr<SCustomShapeButton> CustomShapeButton = GetSlateCustomShapeButton())
{
CustomShapeButton->ForceUpdateImage();
}
}

// Is called when the underlying SWidget needs to be constructed
TSharedRef<SWidget> UCustomShapeButton::RebuildWidget()
{
Expand Down
131 changes: 100 additions & 31 deletions Source/CustomShapeButton/Private/SCustomShapeButton.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,20 @@
#include "RHIResources.h"
#include "TextureResource.h"
#include "Engine/Texture2D.h"
#include "Engine/World.h"
#include "Kismet/KismetRenderingLibrary.h"
#include "Materials/MaterialInterface.h"

// Virtual destructor, unregister data
SCustomShapeButton::~SCustomShapeButton()
{
RawColorsPtr.Reset();

if (IsValid(RenderTarget.Get()))
{
RenderTarget->ConditionalBeginDestroy();
RenderTarget.Reset();
}
}

/** Allows button to be hovered. */
Expand All @@ -35,6 +44,34 @@ void SCustomShapeButton::SetCanHover(bool bAllow)
TryDetectOnHovered();
}

// Updates the internal texture size
void SCustomShapeButton::SetTextureSize(const FIntPoint& InSize)
{
if (!ensureMsgf(InSize.X > 0 && InSize.Y > 0, TEXT("ASSERT: [%i] %hs:\nTexture Size is not valid!"), __LINE__, __FUNCTION__)
|| TextureRes == InSize)
{
// Is already set
return;
}

TextureRes = InSize;

if (!RawColorsPtr)
{
RawColorsPtr = MakeShared<TArray<FColor>, ESPMode::ThreadSafe>();
}
}

// Forces to update the Raw Colors (pixels data) about current image
void SCustomShapeButton::ForceUpdateImage()
{
if (RawColorsPtr)
{
RawColorsPtr->Empty();
TryUpdateRawColorsOnce();
}
}

FReply SCustomShapeButton::OnMouseButtonDown(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent)
{
UpdateMouseData(MyGeometry, MouseEvent);
Expand Down Expand Up @@ -118,8 +155,7 @@ bool SCustomShapeButton::IsAlphaPixelHovered() const
}

const TArray<FColor>* RawColors = RawColorsPtr.Get();
if (!RawColors
|| !RawColors->Num())
if (!RawColors)
{
// Raw Colors are not set
return false;
Expand All @@ -139,65 +175,98 @@ bool SCustomShapeButton::IsAlphaPixelHovered() const
return false;
}

constexpr int32 Alpha = 1;
const bool bIsAlphaPixelHovered = RawColorsArray[BufferPosition].A > Alpha;
return bIsAlphaPixelHovered;
const uint8 HoveredPixel = RawColorsArray[BufferPosition].A;
const uint8 HoveredPixelNormalized = HoveredPixel > 0 ? 1 : 0;

constexpr uint8 TextureAlpha = 1;
constexpr uint8 MaterialAlpha = 0;

return HoveredPixelNormalized == (RenderTarget ? MaterialAlpha : TextureAlpha);
}

// Set once on render thread the buffer data about all pixels of current texture if was not set before
// Set once on render thread the buffer data about all pixels of current image if was not set before
void SCustomShapeButton::TryUpdateRawColorsOnce()
{
if (RawColorsPtr)
if (RawColorsPtr && !RawColorsPtr->IsEmpty())
{
// Is already valid
// Buffer data was already created (valid) AND contains pixels
return;
}

const FSlateBrush* ImageBrush = GetBorderImage();
const UTexture2D* ButtonTexture = ImageBrush ? Cast<UTexture2D>(ImageBrush->GetResourceObject()) : nullptr;
if (!ensureMsgf(ButtonTexture, TEXT("%s: 'HitTexture' is null, most likely no texture is set in the Button Style"), *FString(__FUNCTION__)))
UObject* InImage = ImageBrush ? ImageBrush->GetResourceObject() : nullptr;
if (!ensureMsgf(InImage, TEXT("%s: 'InImage' is null, most likely no texture is set in the Button Style"), *FString(__FUNCTION__)))
{
return;
}

TextureRes = FIntPoint(ButtonTexture->GetSizeX(), ButtonTexture->GetSizeY());
if (const UTexture2D* Texture = Cast<UTexture2D>(InImage))
{
UpdateRawColors_Texture(*Texture);
}
else if (UMaterialInterface* Material = Cast<UMaterialInterface>(InImage))
{
UpdateRawColors_Material(*Material);
}
else
{
ensureMsgf(false, TEXT("ASSERT: [%i] %hs:\n'No image' is set!"), __LINE__, __FUNCTION__);
}
}

// Create
RawColorsPtr = MakeShared<TArray<FColor>, ESPMode::ThreadSafe>();
RawColorsPtr->SetNum(TextureRes.X * TextureRes.Y);
// Copies the buffer data from the texture
void SCustomShapeButton::UpdateRawColors_Texture(const UTexture2D& Texture)
{
SetTextureSize(FIntPoint(Texture.GetSizeX(), Texture.GetSizeY()));

// Get Raw Colors data on Render thread
const TWeakPtr<TArray<FColor>, ESPMode::ThreadSafe> InOutRawColorsWeakPtr = RawColorsPtr;
const TWeakObjectPtr<const UTexture2D> WeakTexture = ButtonTexture;
ENQUEUE_RENDER_COMMAND(TryUpdateRawColorsOnce)([InOutRawColorsWeakPtr, WeakTexture](FRHICommandListImmediate&)
const TWeakPtr<TArray<FColor>> InOutRawColorsWeakPtr = RawColorsPtr;
const TWeakObjectPtr<const UTexture2D> WeakTexture = &Texture;
const FIntRect TextureSize(0, 0, TextureRes.X, TextureRes.Y);
ENQUEUE_RENDER_COMMAND(TryUpdateRawColorsOnce)([InOutRawColorsWeakPtr, WeakTexture, TextureSize](FRHICommandListImmediate& RHICmdList)
{
TArray<FColor>* RawColors = InOutRawColorsWeakPtr.Pin().Get();
if (!ensureMsgf(RawColors, TEXT("%s: 'RawColors' is null, can not obtain its data"), *FString(__FUNCTION__)))
if (!ensureMsgf(RawColors, TEXT("%hs: 'RawColors' is null, can not obtain its data"), __FUNCTION__))
{
return;
}

const UTexture2D* Texture2D = WeakTexture.Get();
const FTextureResource* TextureResource = Texture2D ? Texture2D->GetResource() : nullptr;
FRHITexture2D* RHITexture2D = TextureResource ? TextureResource->GetTexture2DRHI() : nullptr;
if (!ensureMsgf(RHITexture2D, TEXT("%s: 'RHITexture2D' is not valid"), *FString(__FUNCTION__)))
if (ensureMsgf(RHITexture2D, TEXT("%hs: 'RHITexture2D' is not valid"), __FUNCTION__))
{
return;
// Copy data to cache
RHICmdList.ReadSurfaceData(RHITexture2D, TextureSize, /*out*/*RawColors, FReadSurfaceDataFlags());
}
});
}

// Lock
uint32 DestPitch = 0;
constexpr int32 MipIndex = 0;
constexpr bool bLockWithinMipTail = false;
const uint8* MappedTextureMemory = static_cast<const uint8*>(RHILockTexture2D(RHITexture2D, MipIndex, RLM_ReadOnly, DestPitch, bLockWithinMipTail));
// Copies the buffer data from the material
void SCustomShapeButton::UpdateRawColors_Material(UMaterialInterface& Material)
{
checkf(GWorld, TEXT("ERROR: [%i] %hs:\n'GWorld' is null!"), __LINE__, __FUNCTION__);

// Copy data
const int32 Count = RawColors->Num() * sizeof(FColor);
FMemory::Memcpy(/*dest*/RawColors->GetData(), /*source*/MappedTextureMemory, Count);
const FSlateBrush* Image = GetBorderImage();
checkf(Image, TEXT("ERROR: [%i] %hs:\n'Image' is null!"), __LINE__, __FUNCTION__);
const FVector2f ImageSize = Image->GetImageSize();
SetTextureSize(FIntPoint(ImageSize.X, ImageSize.Y));
checkf(RawColorsPtr.Get(), TEXT("ERROR: [%i] %hs:\n'RawColorsPtr' was not created!"), __LINE__, __FUNCTION__);

// Unlock
RHIUnlockTexture2D(RHITexture2D, MipIndex, bLockWithinMipTail);
});
// Create new Render Target
if (!RenderTarget)
{
RenderTarget = TStrongObjectPtr(UKismetRenderingLibrary::CreateRenderTarget2D(GWorld, TextureRes.X, TextureRes.Y));
}

// Clear created Render Target now before rendering material
UKismetRenderingLibrary::ClearRenderTarget2D(GWorld, RenderTarget.Get());

// Render our material first before copying pixels data
UKismetRenderingLibrary::DrawMaterialToRenderTarget(GWorld, RenderTarget.Get(), &Material);

// Copy pixels data from Render Target to our cache
UKismetRenderingLibrary::ReadRenderTarget(GWorld, RenderTarget.Get(), /*out*/*RawColorsPtr);
}

// Try register On Hovered and On Unhovered events
Expand Down
7 changes: 7 additions & 0 deletions Source/CustomShapeButton/Public/CustomShapeButton.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@ class CUSTOMSHAPEBUTTON_API UCustomShapeButton : public UButton
/** Returns the slate shape button. */
TSharedPtr<SCustomShapeButton> GetSlateCustomShapeButton() const;

/** Forces to update the Raw Colors (pixels data) about current image.
* @warning Is not recommended to use at all since often updates are expensive.
* However, can be useful if button changes in runtime (new texture set or material is changing dynamically).
* By default, image is cached only once at the beginning. */
UFUNCTION(BlueprintCallable, Category = "Custom Shape Button")
void ForceUpdateImage();

protected:
/** Is called when the underlying SWidget needs to be constructed. */
virtual TSharedRef<SWidget> RebuildWidget() override;
Expand Down
27 changes: 23 additions & 4 deletions Source/CustomShapeButton/Public/SCustomShapeButton.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
#pragma once

#include "Widgets/Input/SButton.h"

class UTexture2D;
//---
#include "UObject/StrongObjectPtr.h"
#include "Engine/TextureRenderTarget2D.h"

/**
* Implements slate button with one difference:
Expand All @@ -19,9 +20,21 @@ class CUSTOMSHAPEBUTTON_API SCustomShapeButton : public SButton
/** Allows button to be hovered. */
virtual void SetCanHover(bool bAllow);

/** Updates the internal texture size. */
virtual void SetTextureSize(const FIntPoint& InSize);

/** Forces to update the Raw Colors (pixels data) about current image.
* @warning Is not recommended to use at all since often updates are expensive.
* However, can be useful if button changes in runtime (new texture set or material is changing dynamically).
* By default, image is cached only once at the beginning. */
void ForceUpdateImage();

protected:
/** Cached buffer data about all pixels of current texture, is set once on render thread. */
TSharedPtr<TArray<FColor>, ESPMode::ThreadSafe> RawColorsPtr = nullptr;
TSharedPtr<TArray<FColor>> RawColorsPtr = nullptr;

/** Is created once if no render target was set before, cleanups on destruction. */
TStrongObjectPtr<UTextureRenderTarget2D> RenderTarget = nullptr;

/** Contains the size of current texture. */
FIntPoint TextureRes = FIntPoint::ZeroValue;
Expand All @@ -48,9 +61,15 @@ class CUSTOMSHAPEBUTTON_API SCustomShapeButton : public SButton
/** Returns true if cursor is hovered on a texture. */
virtual bool IsAlphaPixelHovered() const;

/** Set once on render thread the buffer data about all pixels of current texture if was not set before. */
/** Set once on render thread the buffer data about all pixels of current image if was not set before. */
virtual void TryUpdateRawColorsOnce();

/** Copies the buffer data from the texture. */
virtual void UpdateRawColors_Texture(const class UTexture2D& Texture);

/** Copies the buffer data from the material. */
virtual void UpdateRawColors_Material(class UMaterialInterface& Material);

/** Try register On Hovered and On Unhovered events. */
virtual void TryDetectOnHovered();

Expand Down

0 comments on commit 6e8430a

Please sign in to comment.