Skip to content

Commit 5581c07

Browse files
committed
Implemented Materials Support as alternative to textures; Added 'Force Update Image' function to let developers handle runtime changes.
1 parent 218360e commit 5581c07

6 files changed

Lines changed: 146 additions & 36 deletions

File tree

README.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ The `Custom Shape Button` plugin revolutionizes the way buttons are designed in
44

55
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.
66

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

99
## 📚 Documentation
1010

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

1717
## 📅 Changelog
18+
#### 2024-04-25
19+
- Updated to **Unreal Engine 5.4**
20+
- Implemented **Materials Support** as alternative to textures: [doc](https://docs.google.com/document/d/1Ws76obIHRMtsdOjB6YP9K7LTjJR-R56h2uv65PKUBL4/edit#heading=h.jlxkng80vqbe):
21+
> ![image](https://github.com/JanSeliv/CustomShapeButton/assets/20540872/c4a083d2-494e-400f-b363-1ffa795024fa)
22+
1823
#### 2023-10-21
1924
- Updated to **Unreal Engine 5.3**.
2025
#### 2023-06-04

Source/CustomShapeButton/CustomShapeButton.Build.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ public CustomShapeButton(ReadOnlyTargetRules Target) : base(Target)
88
{
99
PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs;
1010
CppStandard = CppStandardVersion.Latest;
11+
bEnableNonInlinedGenCppWarnings = true;
1112

1213
PublicDependencyModuleNames.AddRange(new[]
1314
{

Source/CustomShapeButton/Private/CustomShapeButton.cpp

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,15 @@ TSharedPtr<SCustomShapeButton> UCustomShapeButton::GetSlateCustomShapeButton() c
2121
return StaticCastSharedPtr<SCustomShapeButton>(MyButton);
2222
}
2323

24+
// Forces to update the Raw Colors (pixels data) about current image
25+
void UCustomShapeButton::ForceUpdateImage()
26+
{
27+
if (const TSharedPtr<SCustomShapeButton> CustomShapeButton = GetSlateCustomShapeButton())
28+
{
29+
CustomShapeButton->ForceUpdateImage();
30+
}
31+
}
32+
2433
// Is called when the underlying SWidget needs to be constructed
2534
TSharedRef<SWidget> UCustomShapeButton::RebuildWidget()
2635
{

Source/CustomShapeButton/Private/SCustomShapeButton.cpp

Lines changed: 100 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,20 @@
77
#include "RHIResources.h"
88
#include "TextureResource.h"
99
#include "Engine/Texture2D.h"
10+
#include "Engine/World.h"
11+
#include "Kismet/KismetRenderingLibrary.h"
12+
#include "Materials/MaterialInterface.h"
1013

1114
// Virtual destructor, unregister data
1215
SCustomShapeButton::~SCustomShapeButton()
1316
{
1417
RawColorsPtr.Reset();
18+
19+
if (IsValid(RenderTarget.Get()))
20+
{
21+
RenderTarget->ConditionalBeginDestroy();
22+
RenderTarget.Reset();
23+
}
1524
}
1625

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

47+
// Updates the internal texture size
48+
void SCustomShapeButton::SetTextureSize(const FIntPoint& InSize)
49+
{
50+
if (!ensureMsgf(InSize.X > 0 && InSize.Y > 0, TEXT("ASSERT: [%i] %hs:\nTexture Size is not valid!"), __LINE__, __FUNCTION__)
51+
|| TextureRes == InSize)
52+
{
53+
// Is already set
54+
return;
55+
}
56+
57+
TextureRes = InSize;
58+
59+
if (!RawColorsPtr)
60+
{
61+
RawColorsPtr = MakeShared<TArray<FColor>, ESPMode::ThreadSafe>();
62+
}
63+
}
64+
65+
// Forces to update the Raw Colors (pixels data) about current image
66+
void SCustomShapeButton::ForceUpdateImage()
67+
{
68+
if (RawColorsPtr)
69+
{
70+
RawColorsPtr->Empty();
71+
TryUpdateRawColorsOnce();
72+
}
73+
}
74+
3875
FReply SCustomShapeButton::OnMouseButtonDown(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent)
3976
{
4077
UpdateMouseData(MyGeometry, MouseEvent);
@@ -118,8 +155,7 @@ bool SCustomShapeButton::IsAlphaPixelHovered() const
118155
}
119156

120157
const TArray<FColor>* RawColors = RawColorsPtr.Get();
121-
if (!RawColors
122-
|| !RawColors->Num())
158+
if (!RawColors)
123159
{
124160
// Raw Colors are not set
125161
return false;
@@ -139,65 +175,98 @@ bool SCustomShapeButton::IsAlphaPixelHovered() const
139175
return false;
140176
}
141177

142-
constexpr int32 Alpha = 1;
143-
const bool bIsAlphaPixelHovered = RawColorsArray[BufferPosition].A > Alpha;
144-
return bIsAlphaPixelHovered;
178+
const uint8 HoveredPixel = RawColorsArray[BufferPosition].A;
179+
const uint8 HoveredPixelNormalized = HoveredPixel > 0 ? 1 : 0;
180+
181+
constexpr uint8 TextureAlpha = 1;
182+
constexpr uint8 MaterialAlpha = 0;
183+
184+
return HoveredPixelNormalized == (RenderTarget ? MaterialAlpha : TextureAlpha);
145185
}
146186

147-
// Set once on render thread the buffer data about all pixels of current texture if was not set before
187+
// Set once on render thread the buffer data about all pixels of current image if was not set before
148188
void SCustomShapeButton::TryUpdateRawColorsOnce()
149189
{
150-
if (RawColorsPtr)
190+
if (RawColorsPtr && !RawColorsPtr->IsEmpty())
151191
{
152-
// Is already valid
192+
// Buffer data was already created (valid) AND contains pixels
153193
return;
154194
}
155195

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

163-
TextureRes = FIntPoint(ButtonTexture->GetSizeX(), ButtonTexture->GetSizeY());
203+
if (const UTexture2D* Texture = Cast<UTexture2D>(InImage))
204+
{
205+
UpdateRawColors_Texture(*Texture);
206+
}
207+
else if (UMaterialInterface* Material = Cast<UMaterialInterface>(InImage))
208+
{
209+
UpdateRawColors_Material(*Material);
210+
}
211+
else
212+
{
213+
ensureMsgf(false, TEXT("ASSERT: [%i] %hs:\n'No image' is set!"), __LINE__, __FUNCTION__);
214+
}
215+
}
164216

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

169222
// Get Raw Colors data on Render thread
170-
const TWeakPtr<TArray<FColor>, ESPMode::ThreadSafe> InOutRawColorsWeakPtr = RawColorsPtr;
171-
const TWeakObjectPtr<const UTexture2D> WeakTexture = ButtonTexture;
172-
ENQUEUE_RENDER_COMMAND(TryUpdateRawColorsOnce)([InOutRawColorsWeakPtr, WeakTexture](FRHICommandListImmediate&)
223+
const TWeakPtr<TArray<FColor>> InOutRawColorsWeakPtr = RawColorsPtr;
224+
const TWeakObjectPtr<const UTexture2D> WeakTexture = &Texture;
225+
const FIntRect TextureSize(0, 0, TextureRes.X, TextureRes.Y);
226+
ENQUEUE_RENDER_COMMAND(TryUpdateRawColorsOnce)([InOutRawColorsWeakPtr, WeakTexture, TextureSize](FRHICommandListImmediate& RHICmdList)
173227
{
174228
TArray<FColor>* RawColors = InOutRawColorsWeakPtr.Pin().Get();
175-
if (!ensureMsgf(RawColors, TEXT("%s: 'RawColors' is null, can not obtain its data"), *FString(__FUNCTION__)))
229+
if (!ensureMsgf(RawColors, TEXT("%hs: 'RawColors' is null, can not obtain its data"), __FUNCTION__))
176230
{
177231
return;
178232
}
179233

180234
const UTexture2D* Texture2D = WeakTexture.Get();
181235
const FTextureResource* TextureResource = Texture2D ? Texture2D->GetResource() : nullptr;
182236
FRHITexture2D* RHITexture2D = TextureResource ? TextureResource->GetTexture2DRHI() : nullptr;
183-
if (!ensureMsgf(RHITexture2D, TEXT("%s: 'RHITexture2D' is not valid"), *FString(__FUNCTION__)))
237+
if (ensureMsgf(RHITexture2D, TEXT("%hs: 'RHITexture2D' is not valid"), __FUNCTION__))
184238
{
185-
return;
239+
// Copy data to cache
240+
RHICmdList.ReadSurfaceData(RHITexture2D, TextureSize, /*out*/*RawColors, FReadSurfaceDataFlags());
186241
}
242+
});
243+
}
187244

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

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

198-
// Unlock
199-
RHIUnlockTexture2D(RHITexture2D, MipIndex, bLockWithinMipTail);
200-
});
256+
// Create new Render Target
257+
if (!RenderTarget)
258+
{
259+
RenderTarget = TStrongObjectPtr(UKismetRenderingLibrary::CreateRenderTarget2D(GWorld, TextureRes.X, TextureRes.Y));
260+
}
261+
262+
// Clear created Render Target now before rendering material
263+
UKismetRenderingLibrary::ClearRenderTarget2D(GWorld, RenderTarget.Get());
264+
265+
// Render our material first before copying pixels data
266+
UKismetRenderingLibrary::DrawMaterialToRenderTarget(GWorld, RenderTarget.Get(), &Material);
267+
268+
// Copy pixels data from Render Target to our cache
269+
UKismetRenderingLibrary::ReadRenderTarget(GWorld, RenderTarget.Get(), /*out*/*RawColorsPtr);
201270
}
202271

203272
// Try register On Hovered and On Unhovered events

Source/CustomShapeButton/Public/CustomShapeButton.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,13 @@ class CUSTOMSHAPEBUTTON_API UCustomShapeButton : public UButton
2828
/** Returns the slate shape button. */
2929
TSharedPtr<SCustomShapeButton> GetSlateCustomShapeButton() const;
3030

31+
/** Forces to update the Raw Colors (pixels data) about current image.
32+
* @warning Is not recommended to use at all since often updates are expensive.
33+
* However, can be useful if button changes in runtime (new texture set or material is changing dynamically).
34+
* By default, image is cached only once at the beginning. */
35+
UFUNCTION(BlueprintCallable, Category = "Custom Shape Button")
36+
void ForceUpdateImage();
37+
3138
protected:
3239
/** Is called when the underlying SWidget needs to be constructed. */
3340
virtual TSharedRef<SWidget> RebuildWidget() override;

Source/CustomShapeButton/Public/SCustomShapeButton.h

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@
33
#pragma once
44

55
#include "Widgets/Input/SButton.h"
6-
7-
class UTexture2D;
6+
//---
7+
#include "UObject/StrongObjectPtr.h"
8+
#include "Engine/TextureRenderTarget2D.h"
89

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

23+
/** Updates the internal texture size. */
24+
virtual void SetTextureSize(const FIntPoint& InSize);
25+
26+
/** Forces to update the Raw Colors (pixels data) about current image.
27+
* @warning Is not recommended to use at all since often updates are expensive.
28+
* However, can be useful if button changes in runtime (new texture set or material is changing dynamically).
29+
* By default, image is cached only once at the beginning. */
30+
void ForceUpdateImage();
31+
2232
protected:
2333
/** Cached buffer data about all pixels of current texture, is set once on render thread. */
24-
TSharedPtr<TArray<FColor>, ESPMode::ThreadSafe> RawColorsPtr = nullptr;
34+
TSharedPtr<TArray<FColor>> RawColorsPtr = nullptr;
35+
36+
/** Is created once if no render target was set before, cleanups on destruction. */
37+
TStrongObjectPtr<UTextureRenderTarget2D> RenderTarget = nullptr;
2538

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

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

67+
/** Copies the buffer data from the texture. */
68+
virtual void UpdateRawColors_Texture(const class UTexture2D& Texture);
69+
70+
/** Copies the buffer data from the material. */
71+
virtual void UpdateRawColors_Material(class UMaterialInterface& Material);
72+
5473
/** Try register On Hovered and On Unhovered events. */
5574
virtual void TryDetectOnHovered();
5675

0 commit comments

Comments
 (0)