[Unity] AsyncGPUReadback を利用して、非 Color 形式の Texture からデータを取り出す

Unity の Tetxture2D クラスには Int 要素などを持つ Generic な Texture からデータを読むインターフェイスがない。AsyncGPUReadback クラスを利用すると、任意フォーマットの Texture (Texture2D / RenderTexture) から NativeArray<T> 形式でデータを取得できる。

Texture ソースから非同期でデータ取得を行う AsyncCPUTexture クラスを以下のように定義する。Source に読みたい Texture を設定し、Update() を繰り返すことで常に最新のデータを取り出す。

NativeArray として取得したデータは、直後に破棄されるので ”T[]” 配列にコピーしておく。ここで UnsafeCopyTo() 関数は、「[Unity - "NativeArray\<T\> where T:struct" の要素を "T[] where T:struct" へ高速にコピーする](https://qiita.com/nobnak/items/a55385e22758731f4532)」 で定義したものを流用する。

また、Bilinear などの補間や uv 座標でのアクセスもしたいので、TextureData というクラスでデータをラップする。

####"AsyncCPUTexture\<T\> where T:struct" クラス

```csharp
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;

public class AsyncCPUTexture<T> where T:struct {

    protected bool active = false;
    protected AsyncGPUReadbackRequest req;

    protected Vector2Int size;
    protected T defaultValue;
    protected T[] data;
    protected TextureData<T> output;

    public AsyncCPUTexture(T defaultValue = default(T)) {
        this.defaultValue = defaultValue;
    }

    #region interface
    public virtual Texture Source { get; set; }
    public virtual void Update() {

        if (active) {
            if (req.hasError) {
                Debug.Log($"Failed to read back from GPU async");
                active = false;
            } else if (req.done) {
                Release();
                var nativeData = req.GetData<T>();
                System.Array.Resize(ref data, nativeData.Length);
                nativeData.UnsafeCopyTo(data);
                output = GenerateCPUTexture(data, size);
            }
        } else {
            if (Source != null) {
                active = true;
                req = AsyncGPUReadback.Request(Source);
                size = new Vector2Int(Source.width, Source.height);
            }
        }
    }

    public virtual Vector2Int Size => size;
    public virtual System.Func<float, float, T> Interpolation { get; set; }
    public virtual T this[Vector2 uv] => (output != null ? output[uv] : defaultValue);
    public virtual T this[float nx, float ny] => (output != null ? output[nx, ny] : defaultValue);
    public virtual T this[int x, int y] => (output != null ? output[x, y] : defaultValue);
    #endregion

    #region member
    protected virtual TextureData<T> GenerateCPUTexture(IList<T> data, Vector2Int size) {
        var tex = new TextureData<T>(data, size);
        tex.Interpolation = Interpolation;
        return tex;
    }
    protected virtual void Release() {
        if (output != null) {
            output.Dispose();
            output = null;
        }
        active = false;
    }
    #endregion
}
```

####"TextureData\<T\> where T:struct" クラス

```csharp
using System.Collections.Generic;
using UnityEngine;

public class TextureData<T> : System.IDisposable where T:struct {
    public delegate T BilinearFunc(T v00, T v01, T v10, T v11, float s, float t);
    public event System.Action<TextureData<T>> OnLoad;

    protected Vector2Int size;
    protected IList<T> pixels;
    protected Vector2 uvToIndex;
    protected System.Func<float, float, T> interpolation;

    public TextureData(IList<T> pixels, Vector2Int size,
        System.Func<float, float, T> interpolation = null) {
        this.Size = size;
        this.Interpolation = interpolation;
        Load(pixels);
    }

    #region interface
    public System.Func<float, float, T> Interpolation {
        get { return interpolation; }
        set {
            interpolation = (value ?? PointInterpolation);
        }
    }
    public virtual void Load(IList<T> pixels) {
        lock (this) {
            this.pixels = pixels;
        }
        NotifyOnLoad();
    }
    public T PointInterpolation(float nx, float ny) {
        var x = Mathf.RoundToInt(nx * uvToIndex.x);
        var y = Mathf.RoundToInt(ny * uvToIndex.y);
        ClampPixelPos(ref x, ref y);
        return GetPixelDirect(x, y);
    }
    public System.Func<float, float, T> GenerateInterpolation(BilinearFunc bilinear) {
        return (float nx, float ny) => {
            int x0, y0, x1, y1;
            float t, s;
            Bridge(nx, ny, out x0, out y0, out x1, out y1, out t, out s);
            ClampPixelPos(ref x0, ref y0);
            ClampPixelPos(ref x1, ref y1);
            return bilinear(
                GetPixelDirect(x0, y0),
                GetPixelDirect(x0, y1),
                GetPixelDirect(x1, y0),
                GetPixelDirect(x1, y1),
                s, t);
        };
    }

    public virtual T this[int x, int y] {
        get {
            lock (this) {
                ClampPixelPos(ref x, ref y);
                return GetPixelDirect(x, y);
            }
        }
        set {
            lock (this) {
                ClampPixelPos(ref x, ref y);
                SetPixelDirect(x, y, value);
            }
        }
    }

    public virtual T this[float nx, float ny] {
        get {
            lock (this) {
                return interpolation(nx, ny);
            }
        }
    }

    public virtual T this[Vector2 uv] {
        get { return this[uv.x, uv.y]; }
    }

    public virtual Vector2Int Size {
        get { return size; }
        set {
            size = value;
            uvToIndex = new Vector2(value.x - 1, value.y - 1);
        }
    }
    #endregion

    #region static
    public static Color Bilinear(Color v00, Color v01, Color v10, Color v11, float s, float t) {
        return t * (s * v00 + (1f - s) * v01)
            + (1f - t) * (s * v10 + (1f - s) * v11);
    }
    public static float Bilinear(float v00, float v01, float v10, float v11, float s, float t) {
        return t * (s * v00 + (1f - s) * v01)
            + (1f - t) * (s * v10 + (1f - s) * v11);
    }
    #endregion

    #region member
    protected virtual void NotifyOnLoad() {
        OnLoad?.Invoke(this);
    }

    protected virtual void Bridge(float nx, float ny, out int x0, out int y0, out int x1, out int y1, out float t, out float s) {
        var x = uvToIndex.x * nx;
        var y = uvToIndex.y * ny;
        x0 = Mathf.FloorToInt(x);
        y0 = Mathf.FloorToInt(y);
        x1 = x0 + 1;
        y1 = y0 + 1;
        t = x1 - x;
        s = y1 - y;
    }
    protected virtual void ClampPixelPos(ref int x, ref int y) {
        x = (x < 0 ? 0 : (x < size.x ? x : size.x - 1));
        y = (y < 0 ? 0 : (y < size.y ? y : size.y - 1));
    }

    protected virtual int GetLinearIndex(int x, int y) {
        return x + y * size.x;
    }
    protected virtual int GetLinearIndex(Vector2Int index) {
        return GetLinearIndex(index.x, index.y);
    }
    protected virtual T GetPixelDirect(int x, int y) {
        return pixels[GetLinearIndex(x, y)];
    }
    protected virtual void SetPixelDirect(int x, int y, T c) {
        pixels[GetLinearIndex(x, y)] = c;
    }
    #endregion

    #region IDisposable
    public void Dispose() {
        if (pixels != null) {
            pixels = null;
        }
    }
    #endregion
}
```