虚幻引擎:如何在不阻塞游戏线程的情况下访问/保存游戏内图像到磁盘

5
我正在开发一个基于虚幻引擎的开源无人机模拟器(Microsoft AirSim),尝试从连接到无人机的摄像头中捕获并保存图像。下面的图片展示了游戏的外观,底部最右边的视图是来自摄像头的实际视图,其他两个是相同图像的处理版本。

UAV simulation

目前的设置如下:有一个相机资源,通过代码作为捕获组件进行读取。屏幕截图中的三个视图与此捕获组件相关联。在游戏中飞行无人机时,这些视图可以流畅地传输。但是,在记录屏幕截图方面,当前的代码从此捕获组件设置了TextureRenderTargetResource,并随后调用ReadPixels将数据保存为图像(请参见下面的代码流程)。直接使用ReadPixels()会阻塞游戏线程并严重降低整个游戏的速度:当我开始录制时,帧率从约120 FPS降至不到10 FPS。
bool saveImage() {
  USceneCaptureComponent2D* capture = getCaptureComponent(camera_type, true);
  FTextureRenderTargetResource* RenderResource = capture->TextureTarget->GameThread_GetRenderTargetResource();
  width = capture->TextureTarget->GetSurfaceWidth();
  height = capture->TextureTarget->GetSurfaceHeight();

  TArray<FColor> imageColor;
  imageColor.AddUninitialized(width * height);
  RenderResource->ReadPixels(bmp);
}

看起来this article,ReadPixels()函数“会阻塞游戏线程直到渲染线程追上”。文章提供了一个“非阻塞”读取像素的示例代码(通过删除FlushRenderingCommands()并使用RenderCommandFence标志确定任务何时完成),但它并没有显著改善性能:保存图像的速率略有提高,但游戏线程仍然只运行约20 FPS,因此很难控制UAV。是否有更有效的异步方法可以实现我想要做的事情,也许在单独的线程中?我还有点困惑为什么代码在屏幕上流式传输这些图像速度非常快,但保存图像似乎更加复杂。即使图像以每秒15帧的速度保存到磁盘上,只要不过多干扰游戏的本机FPS,这也是可以接受的。
4个回答

1
我来晚了,但如果有人仍然遇到问题:我建立了一个非阻塞版本,灵感来自于AirSim和UnrealCV插件的代码,因为我在编译和版本控制方面遇到了一些严重的问题,但需要在自己捕获图像时拍摄流畅的相机。这也基于unreal API提供的async.h类,就像被接受的答案建议的那样。 我为此设置了一个教程存储库: https://github.com/TimmHess/UnrealImageCapture
希望能对你有所帮助。

1
(...) 显然,ReadPixels() "将阻塞游戏线程直到渲染线程赶上"

使用试图访问和复制GPU纹理数据的函数总是很痛苦。我发现这些函数的完成时间取决于硬件或驱动程序版本。您可以尝试在不同(最好更快或更新)的GPU平台上运行代码。

回到代码:为了绕过单线程问题,我会尝试使用TextureRenderTarget2D::ConstructTexture2D方法将UTextureRenderTarget2D复制到UTexture2D。当您复制完图像后(希望速度更快),您可以按15 FPS比率将其推送给消费者线程。线程可以使用FRunnable或Async引擎模块(使用所谓的任务图形)创建。消费者线程可以通过使用纹理的PlatformData->Mips[0]->BulkData访问克隆纹理。确保您在BulkData->Lock(...)BulkData->Unlock()之间读取纹理数据。

我不确定,但这可能会有所帮助,因为您复制的纹理不会被渲染线程使用,因此您可以在不阻塞渲染线程的情况下锁定其数据。我只担心Lock和Unlock操作的线程安全性(没有找到任何线索),除非有引擎RHI调用,否则应该是可行的。
这里是一些相关代码(链接),可能会有所帮助。

感谢您的详细评论:不幸的是,这种方法也会使我的游戏帧率降至约10帧,而这仅仅是使用ConstructTexture2D()函数。也许可以在单独的线程中完成整个操作吗?不过,ConstructTexture2D()函数本身内部有一些调用,检查它是否从游戏线程中调用。 - HighVoltage

1
您可以将保存到磁盘的操作从游戏线程移动到其他线程。
请查看Async.h了解详细信息。
线程的限制是您无法在其他线程中修改/添加/删除uobject/uactor。 UE4的多线程

0
也许你应该将屏幕截图存储在TArray中,直到“游戏”完成,然后处理所有存储的数据?
或者看看是否可以从无人机相机中获取屏幕截图,而不是操作像素数据?我假设左侧和中间的无人机相机图像是基础相机图像的材质修改版本。
当你保存一张图片时,它是写入一个图像格式文件还是 .uasset 文件?
最后——这只是猜测——创建一个自定义的UActorComponent,将其添加到无人机中,并使其等待来自游戏的某些指令。当您想要编写图像时,请将RenderTarget数据推送到自定义UActorComponent中。由于演员组件可以在任何线程上进行打勾(在构造函数中使用BCanTickOnAnyThread=true,我认为那是变量名称),演员组件的打勾可以监视传入的图像数据并在那里处理它。我从未这样做过,但我已经让演员组件在任何线程上打勾了(我的是检查物理效果)。

你能详细解释一下你所说的“考虑从无人机相机中截取屏幕截图而不是操纵像素数据”吗?因为我所知道的唯一方法是通过ReadPixels()指令来获取捕获的图像数据。而现在,它正在编写压缩的PNG文件。如果我要将它们存储在TArray中,我猜你建议我在一个单独的线程中进行处理? - HighVoltage
我刚刚在我的帖子中添加了另一个想法 :) 您可以调用控制台命令从玩家相机拍摄屏幕截图。也许您可以找到该命令的源代码,然后发现如何为其提供不同的源相机。屏幕截图命令保存一个文件,我认为是targa格式。抱歉,渲染不是我的专业领域。问题在于您正在记录视频而不仅仅是拍摄单个图像。也许可以考虑添加第三方媒体库? - JonS
我也认为在游戏结束后保存图像不会导致帧率下降,如果你想要的话。顺便说一句,可以尝试使用虚幻引擎的捕获功能 https://docs.unrealengine.com/latest/INT/Engine/Basics/Screenshots/ ,它可以自动保存图片,比你现在做的更快。 - LumbusterTick

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接