Calmer的文章

  • 首页
  • 文章归档
  • 关于页面

  • 搜索
体验游戏 笔记 推荐 工具链 工具使用 小游戏 插件 UI 软件 教程

UE4/5 UMG中截屏保存Widget方法

发表于 2021-07-28 | 分类于 游戏开发 | 0 | 阅读次数 2143

前言

常常游戏开发中会有分享的功能,而分享就是把游戏里的一些画面截图出来,拼凑在一起然后生成一张图片,以下是在UE4中做出的一些实践

先记录以下,供以后有需要使用参考


分享截屏方案

  1. 直接对游戏屏幕整个或部分截屏分享
    实现方法:直接调用UE4本身的截屏API或者系统的截屏功能就能实现
    痛点:

    • 无法实现某些美术效果,可能受到手机分辨率的限制,美术需要将内容尽可能的设计到一屏内,导致信息过多的时候一屏可能过于复杂或根本装不下
    • 分享的界面需要单独做出一个界面来进行截图,存在逻辑重复或耦合等问题,并且还会增加资源量等
    • 还不能使用一些排版组件,例如滚动框没法截取完整等情况。

    优点:

    • 简单
    • 所见即所得
  2. 使用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;
}

第二版(加入了保存的方法)

在官方论坛答案的基础上提供了保存的方法和多平台问题的处理
这里有多个方案

  1. 直接使用官方提供的ExportToDisk保存RT
  2. 也可以自己提取颜色数据进行保存
//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;
}

总结上述一共提供了几种方法

  1. Widget -> RT
  2. Widget -> RT -> 本地
  3. RT -> 本地
  4. Widget -> UTexture2D

实现拼装的解决方案

解决方案一:把多张RT合并在一起然后再拍一整张(已实践可行)

  1. 用蓝图创建一个空Widget
  2. 然后使用Widget -> UTexture2D方法,将需要拼凑的子截图保存成UTexture2D供Image控件使用。
  3. 最后用一个排版控件,如VerticalBox, HorizontalBox或UniformGridPanel来进行排布,最后拍一张RT进行保存。

解决方案二:直接把拍下的RT颜色数据进行合并处理(理论可行)

  1. 直接拍下图存成颜色数据。
  2. 进行颜色数据数组的合并。
  3. 转换为二进制进行保存。

长图的截取方案

其只是拼装的一个子集,例如一个VerticalBox控件中有多张Image, 只需要正确的处理VerticalBox的刷新问题,传入正确DrawSize到Widget->RT->PNG方法中,就能正确的截图一张长图。


总结

通过以上方案,能够很好了解UE4中RT的使用、颜色数据的处理以及颜色数据格式之间的转换。

在实际项目中
可能会涉及到RT的使用无非就是以下几个方面

  • 3D -> RT
  • UI -> RT
  • RT -> ... (各种常见图片格式(PNG,JPG)/各种纹理格式(UTexture2D)等)

归根到底就是对一个颜色数据的转换而已


参考文章

Is there a way to Render UMG to a texture

  • 本文作者: Calmer
  • 本文链接: https://mytechplayer.com/archives/ue45umg中截屏保存widget方法
  • 版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明出处!
# 工具链
GAMES101(P1-P4)
Python Tkinter基础框架
  • 文章目录
  • 站点概览
Calmer

Calmer

88 日志
7 分类
10 标签
RSS
Creative Commons
0%
© 2020 — 2025 Calmer
由 Halo 强力驱动
蜀ICP备20010026号-1川公网安备51019002006543
Copyright © 2020-2025 Calmer的文章