Edited at

AsyncGPUReadbackでRenderTextureをTexture2Dに変換する


概要

RenderTextureをTexture2Dに変換するときに良く使われるTexture2d.ReadPixelsが重すぎてカッとなったので他の方法を調べてみた所、AsyncGPUReadbackが見つかったのでこれを試してみた話。


これまでの方法

RenderTextureをTexture2Dに変換する方法としてよくあるのがTexture2d.ReadPixelsを用いる方法です。ただしこの方法だとめちゃめちゃ処理が遅いです。

"Texture2d.ReadPixels slow"でググってみると泣き言書いてる人が結構います。

https://stackoverflow.com/questions/45100993/rendertexture-to-texture2d-is-very-slowly

https://forum.unity.com/threads/encodetopng-super-slow-readpixels-also-quite-slow-any-faster-ways-to-capturing-camera-shots.464813/

super slowとかvery slowとか言われてますね。

試しに下記のコードで毎フレームRenderTexture->Texture2D変換を手持ちのiPhoneXで試してみましたが15FPSぐらいしか出ませんでした。まあiPhoneX解像度高いし。。


SyncCapture.cs

using System.Collections;

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class SyncCapture : MonoBehaviour {

Texture2D _tex;
RenderTexture _renderTex;

private void Start()
{
_tex = new Texture2D(Screen.currentResolution.width, Screen.currentResolution.height, TextureFormat.RGBA32, false);
_renderTex = new RenderTexture(_tex.width, _tex.height, 24, RenderTextureFormat.ARGB32);

}

private void OnPostRender()
{
Graphics.Blit(null, _renderTex);
RenderTexture.active = _renderTex;
//ここがめっちゃ重い!
_tex.ReadPixels(new Rect(0, 0, _tex.width, _tex.height), 0, 0);
_tex.Apply();
}
}



AsyncGPUReadbackを使う

Unity2018.1からAsyncGPUReadbackが追加されてGPUから非同期でデータを取得する事ができるようになりました。なおUnity2018.1時点ではWindowsしか対応してませんでしたが、Unity2018.2のいつ頃から分かりませんがMetal対応され、MacでもiOSでも動作するようになっています。また、AsyncGPUReadbackに対応しているかどうかは下記で確認できるようです。

if(SystemInfo.supportsAsyncGPUReadback){

//AsyncGPUReadback使える!
}

自分の環境だとUnity2018.2.9f1ですがMacでもiOSでもTrueを返してました。

というわけでAsyncGPUReadbackを試してみます。

処理の流れとしては以下のようにAsyncGPUReadback.Requestでリクエストを投げ、

var reqest = AsyncGPUReadback.Request(renderTexture)

Request.doneがtrueになるまで待機し、

void Update()

{
if(reqest.done){
}
}

Requestからbufferを受け取ってTexture2Dに設定でいけます。

取得するデータはNativeArrayなのでTexture2D.LoadRawTextureData()でロードしてあげるのが一番効率良いです。

Unity.Collections.NativeArray<Color32> buffer = reqest.GetData<Color32>();

_tex.LoadRawTextureData(buffer);
_tex.Apply();

全コードは以下のようになります。これをビルドしてiosでFPSを確認すると20FPSだったのが30FPSまで改善しました。でもやっぱiPhoneXの解像度高い分重いっすね。。毎フレームTexture2Dに変換するのはやめて、数フレーム毎に1回しか変換しない等はした方が良さげ。

あと注意点として、Unityのマニュアルにもあるけどリクエスト投げてからデータ取得できるまで数フレームの遅延があるとの事。遅延しても影響ないような使い方(スクリーンショット撮影とか)にしておくのが無難かも。

https://docs.unity3d.com/ScriptReference/Rendering.AsyncGPUReadback.html


AsyncCapture.cs

using UnityEngine;

using System.Collections.Generic;
using UnityEngine.UI;
using UnityEngine.Rendering;

public class AsyncCapture : MonoBehaviour
{

Queue<AsyncGPUReadbackRequest> _requests = new Queue<AsyncGPUReadbackRequest>();

Texture2D _tex;
RenderTexture _renderTex;

void Update()
{
while (_requests.Count > 0)
{
var req = _requests.Peek();

if (req.hasError)
{
Debug.Log("GPU readback error detected.");
_requests.Dequeue();
}
else if (req.done)
{
Unity.Collections.NativeArray<Color32> buffer = req.GetData<Color32>();
_tex.LoadRawTextureData(buffer);
_tex.Apply();
_requests.Dequeue();
}
else
{
break;
}
}
}

private void Start()
{
_tex = new Texture2D(Screen.currentResolution.width, Screen.currentResolution.height, TextureFormat.RGBA32, false);
_renderTex = new RenderTexture(_tex.width, _tex.height, 24, RenderTextureFormat.ARGB32);

}

private void OnPostRender()
{
Graphics.Blit(null, _renderTex);
if (_requests.Count < 8)
{
_requests.Enqueue(AsyncGPUReadback.Request(_renderTex));
}
else
Debug.Log("Too many requests.");
}

}