Custom Thumbnails
This article is in part tutorial and in part, so I don’t forget all the different ways in which assets can get custom thumbnails in Unreal. (It already happened a few times) If I come across more ways to do it I’ll expand this article.
Capture Thumbnail
This one is the easiest, but also entirely manual. In Unreal simply right-click on an asset and choose Asset Actions -> Capture Thumbnail. This will take a screenshot of the current level viewport and make it the asset’s thumbnail.
Class Icon / Class Thumbnail
To create a default thumbnail like Unreal does for Bluperints or Data Assets you’ll need an editor slate style. There’s a good article on creating and using slate styles on Minifloppy.it, so I’m only going to cover the important section here. Unreal will also generate a boilerplate style if you create a new editor mode plugin via the plugin window.
The important part is to set Brushes in the format ClassIcon.ClassName and ClassThumbnail.ClassName and assign .png or .svg files. The code below loads MyPlugin/Resources/myclass_icon.png which is 16 by 16 pixels and MyPlugin/Resources/myclass_thumbnail.svg.
Though SVGs are vector graphics Unreal code always passes along a 64 size. Inspecting the code reveals that it goes into the FDeprecateSlateVector2D structure, so maybe it’s no longer needed. I’ve not delved further into the code to find out so I’ll keep it for now.
TSharedRef< FSlateStyleSet > FMyPluginEditorStyle::Create()
{
// This is how Unreal code does it for sizes, as far as I know there's only 16, 20 and 64
const FVector2D Icon16x16(16.0f, 16.0f);
const FVector2D Icon64x64(64.0f, 64.0f);
TSharedRef< FSlateStyleSet > Style = MakeShareable(new FSlateStyleSet("MyPluginEditorStyle"));
// Set the content root of the style to the plugins resource directory
Style->SetContentRoot(IPluginManager::Get().FindPlugin("MyPlugin")->GetBaseDir() / TEXT("Resources"));
// Loads a png image as a class icon for UMyClass or AMyClass
Style->Set("ClassIcon.MyClass", new IMAGE_BRUSH(TEXT("myclass_icon"), Icon16x16));
// Loads an svg image as a thumbnail icon for UMyClass or AMyClass
Style->Set("ClassThumbnail.MyClass", new IMAGE_BRUSH_SVG(TEXT("myclass_thumbnail"), Icon64x64));
return Style;
}
Thumbnails appear in the content browser (1) and class icons just about everywhere else (2 - 5).
Style wise I’ve observed that Unreal’s thumbnails always have an empty background and border around them. If the image has no borders it almost fills the entire thumbnail, but leaves a few pixels free. Personally I’d recommend to always keep some space.
Class icons should always be in greyscale, but even better in black and white since they get tinted differently all over the place.
Custom Thumbnail Renderer
This approach is arguably the most versatile but also most complicated. Some assets like Textures or Meshes get thumbnails based on their content. In order to do this there is a class called UThumbnailRenderer (or it’s more often used derived class UDefaultSizedThumbnailRenderer) which allows the thumbnail size to be set via config. The main methods to override are CanVisualizeAsset and Draw. In order for the renderer to be used it needs to be registered with the UThumbnailManager.
CanVisualizeAsset gets given the loaded asset before any rendering takes place. Returning true continues the render process, false aborts and the thumbnail stays empty.
Draw is where the actual thumbnail texture is being rendered. In my use case the thumbnail simply draws a background, the icon of a data asset and a tag text to show it’s a recipe.
Here’s the header for a custom thumbnail renderer for Recipe Data Assets in Garden Witch Life:
#pragma once
#include "ThumbnailRendering/DefaultSizedThumbnailRenderer.h"
#include "GWLRecipeAssetThumbnailRenderer.generated.h"
/**
* Thumbnail renderer for Recipe Assets
*/
UCLASS()
class GARDENWITCHLIFEEDITOR_API UGWLRecipeAssetThumbnailRenderer : public UDefaultSizedThumbnailRenderer
{
GENERATED_BODY()
public:
virtual bool CanVisualizeAsset(UObject* Object) override;
virtual void Draw(UObject* Object, int32 X, int32 Y, uint32 Width, uint32 Height, FRenderTarget* Viewport, FCanvas* Canvas, bool bAdditionalViewFamily) override;
};
And the Implementation:
#include "GWLRecipeAssetThumbnailRenderer.h"
#include "Crafting/GWLRecipeDataAsset.h"
#include "CanvasItem.h"
#include "CanvasTypes.h"
#include "ThumbnailRendering/ThumbnailManager.h"
bool UGWLRecipeAssetThumbnailRenderer::CanVisualizeAsset(UObject* Object)
{
// Only render thumbnail for recipes that have a valid icon
if(const auto Recipe = Cast<UGWLRecipeDataAsset>(Object))
{
if(Recipe->Icon.LoadSynchronous())
{
return true;
}
}
return false;
}
void UGWLRecipeAssetThumbnailRenderer::Draw(UObject* Object, int32 X, int32 Y, uint32 Width, uint32 Height,
FRenderTarget* Viewport, FCanvas* Canvas, bool bAdditionalViewFamily)
{
const UGWLRecipeDataAsset* Recipe = Cast<UGWLRecipeDataAsset>(Object);
const UTexture2D* RecipeIcon = Recipe->Icon.LoadSynchronous();
// Draw a checkerboard background
constexpr int32 CheckerDensity = 8;
const auto Checker = UThumbnailManager::Get().CheckerboardTexture;
Canvas->DrawTile(
0.0f, 0.0f, Width, Height, // Dimensions
0.0f, 0.0f, CheckerDensity, CheckerDensity, // UVs
FLinearColor::Gray, Checker->GetResource()); // Tint & Texture
// Draw the recipe's icon centered and filling out the thumbnail
FCanvasTileItem CanvasTile(FVector2D(X, Y), RecipeIcon->GetResource(), FVector2D(Width, Height), FLinearColor::White);
CanvasTile.BlendMode = SE_BLEND_Translucent;
CanvasTile.Draw(Canvas);
// Figure out how big the rendered text would be
auto RecipeChars = TEXT("Recipe");
int32 RecipeTextWidth = 0;
int32 RecipeHeight = 0;
StringSize(GEngine->GetLargeFont(), RecipeTextWidth, RecipeHeight, RecipeChars);
float PaddingX = Width / 128.0f;
float PaddingY = Height / 128.0f;
float ScaleX = Width / 64.0f; //Text is 1/64'th of the size of the thumbnails
float ScaleY = Height / 64.0f;
// Draw the "Recipe" overlay text
FCanvasTextItem TextItem(FVector2D(Width - PaddingX - RecipeTextWidth * ScaleX, Height - PaddingY - RecipeHeight * ScaleY), FText::FromString(RecipeChars), GEngine->GetLargeFont(), FLinearColor::White);
TextItem.EnableShadow(FLinearColor::Black);
TextItem.Scale = FVector2D(ScaleX, ScaleY);
TextItem.Draw(Canvas);
}
In order for the thumbnail renderer to be called by Unreal it needs to be registered, preferrably on module startup.
#include "ThumbnailRendering/ThumbnailManager.h"
void FGardenWitchLifeEditorModule::StartupModule()
{
UThumbnailManager::Get().RegisterCustomRenderer(UGWLRecipeDataAsset::StaticClass(), UGWLRecipeAssetThumbnailRenderer::StaticClass());
}
And now it’s doing its job:
Custom Thumbnail Renderer (for Blueprints)
Sadly this approach doesn’t work for Blueprints. Registering a thumbnail renderer for AMyActor wouldn’t work, because Unreal wraps all Blueprint assets into UBlueprints and there already is a UBlueprintThumbnailRenderer. In order to solve this, a new renderer can be injected. Thanks to @Schadek on the Unreal Forums for this solution!
The trick is to derive from UBlueprintThumbnailRenderer and replace it with the derived class.
// GWLBlueprintThumbnailRenderer.h
#include "ThumbnailRendering/BlueprintThumbnailRenderer.h"
class UGWLBlueprintThumbnailRenderer : public UBlueprintThumbnailRenderer
// GardenWitchLifeEditorModule.cpp
void FGardenWitchLifeEditorModule::StartupModule()
{
// Unregister default renderer
UThumbnailManager::Get().UnregisterCustomRenderer(UBlueprint::StaticClass());
// Register our custom renderer instead
UThumbnailManager::Get().RegisterCustomRenderer(UBlueprint::StaticClass(), UGWLBlueprintThumbnailRenderer::StaticClass());
}
and that’s it!
Ok one last thing, the Draw method gets the UBlueprint passed in as the Object to draw, not the native class. To get the actual default object and native class there’s some more work required.
void UMisBlueprintThumbnailRenderer::Draw(UObject* Object, int32 X, int32 Y, uint32 Width, uint32 Height, FRenderTarget* RenderTarget, FCanvas* Canvas, bool bAdditionalViewFamily)
{
const UBlueprint* Blueprint = Cast<UBlueprint>(Object);
if (Blueprint)
{
const UClass* GeneratedClass = Blueprint->GeneratedClass;
if (Blueprint->GeneratedClass->IsChildOf(AMyActor::StaticClass()))
{
if (const AMyActor* CDO = Class->GetDefaultObject<AMyActor>())
{
// Do rendering
return;
}
}
}
// Fall back to original renderer
Super::Draw(Object, X, Y, Width, Height, RenderTarget, Canvas, bAdditionalViewFamily);
}