// Copyright Epic Games, Inc. All Rights Reserved. #include "ThumbnailRendering/ThumbnailManager.h" #include "HAL/FileManager.h" #include "Misc/FileHelper.h" #include "Misc/Paths.h" #include "UObject/ConstructorHelpers.h" #include "Materials/Material.h" #include "ISourceControlOperation.h" #include "SourceControlOperations.h" #include "ISourceControlProvider.h" #include "ISourceControlModule.h" #include "Engine/StaticMesh.h" #include "UnrealClient.h" #include "Engine/TextureCube.h" #include "Engine/Texture2DArray.h" #include "ImageUtils.h" DEFINE_LOG_CATEGORY_STATIC(LogThumbnailManager, Log, All); ////////////////////////////////////////////////////////////////////////// UThumbnailManager* UThumbnailManager::ThumbnailManagerSingleton = nullptr; UThumbnailManager::UThumbnailManager(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) { if (!IsRunningCommandlet()) { // Structure to hold one-time initialization struct FConstructorStatics { ConstructorHelpers::FObjectFinder EditorCubeMesh; ConstructorHelpers::FObjectFinder EditorSphereMesh; ConstructorHelpers::FObjectFinder EditorCylinderMesh; ConstructorHelpers::FObjectFinder EditorPlaneMesh; ConstructorHelpers::FObjectFinder EditorSkySphereMesh; ConstructorHelpers::FObjectFinder FloorPlaneMaterial; ConstructorHelpers::FObjectFinder DaylightAmbientCubemap; FConstructorStatics() : EditorCubeMesh(TEXT("/Engine/EditorMeshes/EditorCube")), EditorSphereMesh(TEXT("/Engine/EditorMeshes/EditorSphere")), EditorCylinderMesh(TEXT("/Engine/EditorMeshes/EditorCylinder")), EditorPlaneMesh(TEXT("/Engine/EditorMeshes/EditorPlane")), EditorSkySphereMesh(TEXT("/Engine/EditorMeshes/EditorSkySphere")), FloorPlaneMaterial(TEXT("/Engine/EditorMaterials/Thumbnails/FloorPlaneMaterial")), DaylightAmbientCubemap(TEXT("/Engine/MapTemplates/Sky/DaylightAmbientCubemap")) { } }; static FConstructorStatics ConstructorStatics; EditorCube = ConstructorStatics.EditorCubeMesh.Object; EditorSphere = ConstructorStatics.EditorSphereMesh.Object; EditorCylinder = ConstructorStatics.EditorCylinderMesh.Object; EditorPlane = ConstructorStatics.EditorPlaneMesh.Object; EditorSkySphere = ConstructorStatics.EditorSkySphereMesh.Object; FloorPlaneMaterial = ConstructorStatics.FloorPlaneMaterial.Object; AmbientCubemap = ConstructorStatics.DaylightAmbientCubemap.Object; SetupCheckerboardTexture(); } } void UThumbnailManager::Initialize(void) { if (bIsInitialized == false) { InitializeRenderTypeArray(RenderableThumbnailTypes); bIsInitialized = true; } } void UThumbnailManager::InitializeRenderTypeArray(TArray& ThumbnailRendererTypes) { // Loop through setting up each thumbnail entry for (int32 Index = 0; Index < ThumbnailRendererTypes.Num(); Index++) { FThumbnailRenderingInfo& RenderInfo = ThumbnailRendererTypes[Index]; // Load the class that this is for if (RenderInfo.ClassNeedingThumbnailName.Len() > 0) { // Try to load the specified class RenderInfo.ClassNeedingThumbnail = LoadObject(nullptr, *RenderInfo.ClassNeedingThumbnailName, nullptr, LOAD_None, nullptr); } if (RenderInfo.RendererClassName.Len() > 0) { // Try to create the renderer object by loading its class and // constructing one UClass* RenderClass = LoadObject(nullptr, *RenderInfo.RendererClassName, nullptr, LOAD_None, nullptr); if (RenderClass != nullptr) { RenderInfo.Renderer = NewObject(GetTransientPackage(), RenderClass); } } // Add this to the map if it created the renderer component if (RenderInfo.Renderer != nullptr) { RenderInfoMap.Add(RenderInfo.ClassNeedingThumbnail, &RenderInfo); } } } FThumbnailRenderingInfo* UThumbnailManager::GetRenderingInfo(UObject* Object) { // If something may have been GCed, empty the map so we don't crash if (bMapNeedsUpdate == true) { RenderInfoMap.Empty(); bMapNeedsUpdate = false; } check(Object); TArray& ThumbnailTypes = RenderableThumbnailTypes; // Get the class to check against. UClass* ClassToCheck = ClassToCheck = Object->GetClass(); // Search for the cached entry and do the slower if not found FThumbnailRenderingInfo* RenderInfo = RenderInfoMap.FindRef(ClassToCheck); if (RenderInfo == nullptr) { // Loop through searching for the right thumbnail entry for (int32 Index = ThumbnailTypes.Num() - 1; (Index >= 0) && (RenderInfo == nullptr); Index--) { RenderInfo = &ThumbnailTypes[Index]; // See if this thumbnail renderer will work for the specified class or // if there is some data reason not to render the thumbnail if ((ClassToCheck->IsChildOf(RenderInfo->ClassNeedingThumbnail) == false) || (RenderInfo->Renderer == nullptr)) { RenderInfo = nullptr; } } // Make sure to add it to the cache if it is missing RenderInfoMap.Add(ClassToCheck, (RenderInfo != nullptr) ? RenderInfo : &NotSupported); } if (RenderInfo && RenderInfo->Renderer && !RenderInfo->Renderer->CanVisualizeAsset(Object)) { // This is an asset with a thumbnail renderer, but it can't visualized (i.e it is something like a blueprint that doesn't contain any visible primitive components) RenderInfo = nullptr; } // Check to see if this object is the "not supported" type or not if (RenderInfo == &NotSupported) { RenderInfo = nullptr; } return RenderInfo; } void UThumbnailManager::Serialize(FArchive& Ar) { Super::Serialize(Ar); // Just mark us as dirty so that the cache is rebuilt bMapNeedsUpdate = true; } void UThumbnailManager::RegisterCustomRenderer(UClass* Class, TSubclassOf RendererClass) { check(Class != nullptr); check(*RendererClass != nullptr); const FString NewClassPathName = Class->GetPathName(); // Verify that this class isn't already registered for (int32 Index = 0; Index < RenderableThumbnailTypes.Num(); ++Index) { if (ensure(RenderableThumbnailTypes[Index].ClassNeedingThumbnailName != NewClassPathName)) { } else { return; } } // Register the new class FThumbnailRenderingInfo& Info = *(new (RenderableThumbnailTypes) FThumbnailRenderingInfo()); Info.ClassNeedingThumbnailName = NewClassPathName; Info.ClassNeedingThumbnail = Class; if (FApp::CanEverRender()) { Info.Renderer = NewObject(GetTransientPackage(), RendererClass); } else { Info.Renderer = nullptr; } Info.RendererClassName = RendererClass->GetPathName(); bMapNeedsUpdate = true; } void UThumbnailManager::UnregisterCustomRenderer(UClass* Class) { check(Class != nullptr); const FString OldClassPathName = Class->GetPathName(); for (int32 Index = 0; Index < RenderableThumbnailTypes.Num();) { if (RenderableThumbnailTypes[Index].ClassNeedingThumbnailName == OldClassPathName) { RenderableThumbnailTypes.RemoveAtSwap(Index); } else { ++Index; } } bMapNeedsUpdate = true; } UThumbnailManager& UThumbnailManager::Get() { // Create it if we need to if (ThumbnailManagerSingleton == nullptr) { FString ClassName = GetDefault()->ThumbnailManagerClassName; if (!ClassName.IsEmpty()) { // Try to load the specified class UClass* Class = LoadObject(nullptr, *ClassName, nullptr, LOAD_None, nullptr); if (Class != nullptr) { // Create an instance of this class ThumbnailManagerSingleton = NewObject(GetTransientPackage(), Class); } } // If the class couldn't be loaded or is the wrong type, fallback to the default if (ThumbnailManagerSingleton == nullptr) { ThumbnailManagerSingleton = NewObject(); } // Keep the singleton alive ThumbnailManagerSingleton->AddToRoot(); // Tell it to load all of its classes ThumbnailManagerSingleton->Initialize(); } return *ThumbnailManagerSingleton; } void UThumbnailManager::SetupCheckerboardTexture() { if (CheckerboardTexture) { return; } CheckerboardTexture = FImageUtils::CreateCheckerboardTexture(FColor(128, 128, 128), FColor(64, 64, 64), 32); } bool UThumbnailManager::CaptureProjectThumbnail(FViewport* Viewport, const FString& OutputFilename, bool bUseSCCIfPossible) { const uint32 AutoScreenshotSize = 192; // capture the thumbnail uint32 SrcWidth = Viewport->GetSizeXY().X; uint32 SrcHeight = Viewport->GetSizeXY().Y; // Read the contents of the viewport into an array. TArray OrigBitmap; if (Viewport->ReadPixels(OrigBitmap)) { check(OrigBitmap.Num() == SrcWidth * SrcHeight); // pin to smallest value int32 CropSize = FMath::Min(SrcWidth, SrcHeight); // pin to max size int32 ScaledSize = FMath::Min(AutoScreenshotSize, CropSize); // calculations for cropping TArray CroppedBitmap; CroppedBitmap.AddUninitialized(CropSize * CropSize); // Crop the image int32 CroppedSrcTop = (SrcHeight - CropSize) / 2; int32 CroppedSrcLeft = (SrcWidth - CropSize) / 2; for (int32 Row = 0; Row < CropSize; ++Row) { // Row*Side of a row*byte per color int32 SrcPixelIndex = (CroppedSrcTop + Row) * SrcWidth + CroppedSrcLeft; const void* SrcPtr = &(OrigBitmap[SrcPixelIndex]); void* DstPtr = &(CroppedBitmap[Row * CropSize]); FMemory::Memcpy(DstPtr, SrcPtr, CropSize * 4); } // Scale image down if needed TArray ScaledBitmap; if (ScaledSize < CropSize) { FImageUtils::ImageResize(CropSize, CropSize, CroppedBitmap, ScaledSize, ScaledSize, ScaledBitmap, true); } else { // just copy the data over. sizes are the same ScaledBitmap = CroppedBitmap; } // Compress the scaled image TArray ScaledPng; FImageUtils::CompressImageArray(ScaledSize, ScaledSize, ScaledBitmap, ScaledPng); // Save to file const FString ScreenShotPath = FPaths::GetPath(OutputFilename); if (IFileManager::Get().MakeDirectory(*ScreenShotPath, true)) { // If source control is available, try to check out the file if necessary. // If not, silently continue. This is just a courtesy. bool bMarkFileForAdd = false; FString AbsoluteFilename = FPaths::ConvertRelativePathToFull(OutputFilename); TArray FilesToBeCheckedOut; FilesToBeCheckedOut.Add(AbsoluteFilename); ISourceControlProvider& SourceControlProvider = ISourceControlModule::Get().GetProvider(); if (bUseSCCIfPossible && ISourceControlModule::Get().IsEnabled() && SourceControlProvider.IsAvailable()) { FSourceControlStatePtr SourceControlState = SourceControlProvider.GetState(AbsoluteFilename, EStateCacheUsage::ForceUpdate); if (SourceControlState.IsValid()) { if (SourceControlState->CanCheckout()) { SourceControlProvider.Execute(ISourceControlOperation::Create(), FilesToBeCheckedOut); } else if (!SourceControlState->IsSourceControlled()) { bMarkFileForAdd = true; } } } if (FFileHelper::SaveArrayToFile(ScaledPng, *OutputFilename)) { if (bMarkFileForAdd) { SourceControlProvider.Execute(ISourceControlOperation::Create(), FilesToBeCheckedOut); } return true; } } } return false; }