iOS
Unity

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)
            {
                var 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.");
    }


}