#概要
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解像度高いし。。
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
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.");
}
}