Custom Thumbnails

15 Jan 2025

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.

Right-clicking on a Blueprint and selecting the Capture Thumbnail option.

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

Different editor windows displaying custom icons for objects.

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.

Three custom icons in the content browser. The middle one has a brown background to showcase Unreals enforced borders.

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:

Recipe data assets in the content browser showing custom thumbnails.

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!

This solution might have unforseen consequences and would only work with one renderer. There cannot be multiple alternative blueprint thumbnail renderers at the same time.


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