Unity の Tetxture2D クラスには Int 要素などを持つ Generic な Texture からデータを読むインターフェイスがない。AsyncGPUReadback クラスを利用すると、任意フォーマットの Texture (Texture2D / RenderTexture) から NativeArray 形式でデータを取得できる。
Texture ソースから非同期でデータ取得を行う AsyncCPUTexture クラスを以下のように定義する。Source に読みたい Texture を設定し、Update() を繰り返すことで常に最新のデータを取り出す。
NativeArray として取得したデータは、直後に破棄されるので ”T[]” 配列にコピーしておく。ここで UnsafeCopyTo() 関数は、「Unity - "NativeArray<T> where T:struct" の要素を "T[] where T:struct" へ高速にコピーする」 で定義したものを流用する。
また、Bilinear などの補間や uv 座標でのアクセスもしたいので、TextureData というクラスでデータをラップする。
####"AsyncCPUTexture<T> where T:struct" クラス
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" クラス
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
}