前言
常常游戏开发中会有分享的功能,而分享就是把游戏里的一些画面截图出来,拼凑在一起然后生成一张图片,以下是在UE4中做出的一些实践
先记录以下,供以后有需要使用参考
分享截屏方案
-
直接对游戏屏幕整个或部分截屏分享
实现方法:直接调用UE4本身的截屏API或者系统的截屏功能就能实现
痛点:- 无法实现某些美术效果,可能受到手机分辨率的限制,美术需要将内容尽可能的设计到一屏内,导致信息过多的时候一屏可能过于复杂或根本装不下
- 分享的界面需要单独做出一个界面来进行截图,存在逻辑重复或耦合等问题,并且还会增加资源量等
- 还不能使用一些排版组件,例如滚动框没法截取完整等情况。
优点:
- 简单
- 所见即所得
-
使用RT方案,将游戏中需要的部分截取出来再拼凑
痛点:- 会处理更多屏外刷新逻辑甚至不渲染的问题
- 会增大内存占用(渲染了更多的临时纹理或RT)
优点:
- 自由度高,能够实现所有美术效果
- 可以直接取屏内数据,不能再单独为书写独有分享逻辑,提高了效率,并且避免了部分耦合,
- 可以对特殊的排版控件进行完整长度渲染截取
- 能够支持屏外渲染(后台渲染),同时就支持了屏外数据截取了。
- 避免了一定分辨率的限制,能够渲染大图,长图。
RT方案实践
因为需求的复杂性,这里选择了RT作为实现方案
第一版(将Widget渲成RT)
官方论坛提供的一个方案,返回的是一个RT。
- .h文件
#pragma once
#include "CoreMinimal.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "Engine/TextureRenderTarget2D.h"
#include "Slate/WidgetRenderer.h"
#include "Blueprint/UserWidget.h"
#include "CommonStatics.generated.h"
/**
*
*/
UCLASS()
class WIDGETRT_API UCommonStatics : public UBlueprintFunctionLibrary
{
GENERATED_BODY()
public:
UFUNCTION(BlueprintCallable, Category = "TextureTool")
static UTextureRenderTarget2D* WidgetToTexture(UUserWidget* const widget, const FVector2D& drawSize);
};
- .cpp文件
#include "CommonStatics.h"
#include "Framework/Application/SlateApplication.h"
#include "Widgets/SWidget.h"
UTextureRenderTarget2D* UCommonStatics::WidgetToTexture(UUserWidget* const widget, const FVector2D& drawSize)
{
// As long as the slate application is initialized and the widget passed in is not null continue...
if (FSlateApplication::IsInitialized() && widget != nullptr)
{
// Get the slate widget as a smart pointer. Return if null.
TSharedPtr<SWidget> SlateWidget(widget->TakeWidget());
if (!SlateWidget) return nullptr;
// Create a new widget renderer to render the widget to a texture render target 2D.
FWidgetRenderer* WidgetRenderer = new FWidgetRenderer(false);
if (!WidgetRenderer) return nullptr;
// Update/Create the render target 2D.
UTextureRenderTarget2D* TextureRenderTarget;
TextureRenderTarget = WidgetRenderer->DrawWidget(SlateWidget.ToSharedRef(), drawSize);
// Return the updated render target 2D.
return TextureRenderTarget;
}
return nullptr;
}
第二版(加入了保存的方法)
在官方论坛答案的基础上提供了保存的方法和多平台问题的处理
这里有多个方案
- 直接使用官方提供的ExportToDisk保存RT
- 也可以自己提取颜色数据进行保存
//1. 官方提供的一种保存UTexture的方案,UTextureRenderTarget2D继承于UTexture
bool UCommonStatics::SaveRenderTarget2DWithQuality(UTextureRenderTarget2D* RenderTarget2D, const FString& Path, int32 CompressionQuality)
{
if (FPaths::FileExists(Path)) return true;
if (!RenderTarget2D) return false;
FImageWriteOptions Opt;
Opt.bAsync = false;
Opt.bOverwriteFile = true;
Opt.CompressionQuality = CompressionQuality;
Opt.Format = EDesiredImageFormat::PNG;
UImageWriteBlueprintLibrary::ExportToDisk(RenderTarget2D, Path, Opt);
return true;
}
//2. 对RenderTarget中的颜色数据进行处理,保存二进制到文件中
bool UCommonStatics::SaveRenderTarget2D(UTextureRenderTarget2D* RenderTarget2D, const FString& Path)
{
FTextureRenderTargetResource* rtResource = RenderTarget2D->GameThread_GetRenderTargetResource();
FReadSurfaceDataFlags readPixelFlags(RCM_UNorm);
TArray<FColor> OutImageData;
OutImageData.AddUninitialized(RenderTarget2D->GetSurfaceWidth() * RenderTarget2D->GetSurfaceHeight());
rtResource->ReadPixels(OutImageData, readPixelFlags);
FIntPoint destSize(RenderTarget2D->GetSurfaceWidth(), RenderTarget2D->GetSurfaceHeight());
TArray<uint8> CompressedBitmap;
FImageUtils::CompressImageArray(destSize.X, destSize.Y, OutImageData, CompressedBitmap);
return FFileHelper::SaveArrayToFile(CompressedBitmap, *Path);
}
//3. 渲染UWidget到File中
void UCommonStatics::WidgetToFile(class UWidget* const widget, const FVector2D& drawSize, const FString& SavePath, int32 CompressionQuality)
{
if (FSlateApplication::IsInitialized() && widget != nullptr)
{
FWidgetRenderer* WidgetRenderer = new FWidgetRenderer(false);
if (!WidgetRenderer) return;
UTextureRenderTarget2D* TextureRenderTarget = NewObject<UTextureRenderTarget2D>();
if (!TextureRenderTarget) return;
TextureRenderTarget->InitAutoFormat(drawSize.X, drawSize.Y);
TSharedRef<SWidget> SlateWidget = widget->TakeWidget();
WidgetRenderer->DrawWidget(TextureRenderTarget, SlateWidget, drawSize, 1.0f);
SaveRenderTarget2DWithQuality(TextureRenderTarget, SavePath, CompressionQuality);
UE_LOG(LogTemp, Log, TEXT("WidgetToFile Finish"));
delete WidgetRenderer;
WidgetRenderer = nullptr;
TextureRenderTarget = nullptr;
}
}
第三版(提供WidgetToUTexture2D)
主要用于截取临时纹理用于Image的使用。需要注意销毁创建的UMG
.h
UFUNCTION(BlueprintCallable)
static void RenderWidget(class UWidget* const widget, const FVector2D& drawSize);
UFUNCTION(BlueprintCallable)
static UTexture2D* RenderWidgetToUTexture2D(class UWidget* const widget, const FVector2D& drawSize);
.cpp
//强制RT,触发UMG在屏外渲染
void UCommonStatics::RenderWidget(class UWidget* const widget, const FVector2D& drawSize)
{
if (FSlateApplication::IsInitialized() && widget != nullptr)
{
FWidgetRenderer* WidgetRenderer = new FWidgetRenderer(false);
if (!WidgetRenderer) return;
TSharedRef<SWidget> SlateWidget = widget->TakeWidget();
WidgetRenderer->DrawWidget(SlateWidget, drawSize);
}
}
//将UMG渲染成UTxture2D用于拼装
UTexture2D* UCommonStatics::RenderWidgetToUTexture2D(class UWidget* const widget, const FVector2D& drawSize)
{
if (FSlateApplication::IsInitialized() && widget != nullptr)
{
FWidgetRenderer* WidgetRenderer = new FWidgetRenderer(false);
if (!WidgetRenderer) return nullptr;
UTextureRenderTarget2D* TextureRenderTarget = NewObject<UTextureRenderTarget2D>();
if (!TextureRenderTarget)
{
return nullptr;
}
//将RT的缺省颜色设置为透明
TextureRenderTarget->ClearColor = FLinearColor::Transparent;
TextureRenderTarget->InitAutoFormat(drawSize.X, drawSize.Y);
TSharedRef<SWidget> SlateWidget = widget->TakeWidget();
WidgetRenderer->DrawWidget(TextureRenderTarget, SlateWidget, drawSize, 1.0f);
UTexture2D* renderTex = nullptr;
if (TextureRenderTarget)
{
FTextureRenderTargetResource* rtResource = TextureRenderTarget->GameThread_GetRenderTargetResource();
FReadSurfaceDataFlags readPixelFlags(RCM_UNorm);
TArray<FColor> windowColor;
int32 width = TextureRenderTarget->GetSurfaceWidth();
int32 height = TextureRenderTarget->GetSurfaceHeight();
windowColor.AddUninitialized(width * height);
rtResource->ReadPixels(windowColor, readPixelFlags);
FIntPoint destSize(width, height);
TArray<uint8> ResultData;
EImageFormat ImageFormat = EImageFormat::PNG;
FImageUtils::CompressImageArray(destSize.X, destSize.Y, windowColor, ResultData);
/*int32 MemorySize = windowColor.Num() * 4;
ResultData.SetNum(MemorySize);
int64 step = 0;
for (int i = 0; i < windowColor.Num(); i++)
{
FColor color = windowColor[i];
ResultData[step++] = color.R;
ResultData[step++] = color.G;
ResultData[step++] = color.B;
ResultData[step++] = color.A;
}*/
//UTexture2D* renderTex = UTexture2D::CreateTransient(TextureRenderTarget->SizeX, TextureRenderTarget->SizeY);
//renderTex->SRGB = false;
//renderTex->CompressionSettings = TextureCompressionSettings::TC_VectorDisplacementmap;
//TArray<FColor> SurfData;
//FRenderTarget* RenderTarget = TextureRenderTarget->GameThread_GetRenderTargetResource();
//RenderTarget->ReadPixels(SurfData);
//void* TextureData = renderTex->PlatformData->Mips[0].BulkData.Lock(LOCK_READ_WRITE);
//const int32 TextureDataSize = SurfData.Num() * 4;
//FMemory::Memcpy(TextureData, SurfData.GetData(), TextureDataSize);
//renderTex->PlatformData->Mips[0].BulkData.Unlock();
//renderTex->UpdateResource();
IImageWrapperModule& ImageWrapperModule = FModuleManager::LoadModuleChecked<IImageWrapperModule>("ImageWrapper");
TSharedPtr<IImageWrapper> ImageWrapper = ImageWrapperModule.CreateImageWrapper(ImageFormat);
if (ImageWrapper.IsValid() && ImageWrapper->SetCompressed(ResultData.GetData(), ResultData.GetAllocatedSize()))
{
TArray<uint8> OutRawData;
ImageWrapper->GetRaw(ERGBFormat::BGRA, 8, OutRawData);
int32 Width = ImageWrapper->GetWidth();
int32 Height = ImageWrapper->GetHeight();
renderTex = UTexture2D::CreateTransient(Width, Height, PF_B8G8R8A8);
if (renderTex)
{
void* TextureData = renderTex->PlatformData->Mips[0].BulkData.Lock(LOCK_READ_WRITE);
FMemory::Memcpy(TextureData, OutRawData.GetData(), OutRawData.Num());
renderTex->PlatformData->Mips[0].BulkData.Unlock();
renderTex->UpdateResource();
}
}
}
UE_LOG(LogTemp, Log, TEXT("Render UMG To Texture2D In Backend"));
delete WidgetRenderer;
WidgetRenderer = nullptr;
TextureRenderTarget = nullptr;
return renderTex;
}
return nullptr;
}
总结上述一共提供了几种方法
- Widget -> RT
- Widget -> RT -> 本地
- RT -> 本地
- Widget -> UTexture2D
实现拼装的解决方案
解决方案一:把多张RT合并在一起然后再拍一整张(已实践可行)
- 用蓝图创建一个空Widget
- 然后使用Widget -> UTexture2D方法,将需要拼凑的子截图保存成UTexture2D供Image控件使用。
- 最后用一个排版控件,如VerticalBox, HorizontalBox或UniformGridPanel来进行排布,最后拍一张RT进行保存。
解决方案二:直接把拍下的RT颜色数据进行合并处理(理论可行)
- 直接拍下图存成颜色数据。
- 进行颜色数据数组的合并。
- 转换为二进制进行保存。
长图的截取方案
其只是拼装的一个子集,例如一个VerticalBox控件中有多张Image, 只需要正确的处理VerticalBox的刷新问题,传入正确DrawSize到Widget->RT->PNG方法中,就能正确的截图一张长图。
总结
通过以上方案,能够很好了解UE4中RT的使用、颜色数据的处理以及颜色数据格式之间的转换。
在实际项目中
可能会涉及到RT的使用无非就是以下几个方面
- 3D -> RT
- UI -> RT
- RT -> ... (各种常见图片格式(PNG,JPG)/各种纹理格式(UTexture2D)等)
归根到底就是对一个颜色数据的转换而已