UPL9-17-f——获取 GPU 数据

本章代码关键字

1
2
3
4
5
6
7
8
computeBuffer.GetData()                    // 同步获取ComputeBuffer对象的计算结果,传入需要获取结果的数组即可
AsyncGPUReadback.Request() // 异步读取GPU计算结果请求,需要传入要异步获取数据的对象,例如ComputeBuffer和RenderTexture
AsyncGPUReadbackRequest // 异步读取GPU计算结果的请求类,通过该类获取计算结果
asyncGPUReadbackRequest.GetData<>() // 获取GPU计算结果,返回NativeArray<>,泛型参数如果是数组传入数组元素的类型,如果是数组传入Color类型
asyncGPUReadbackRequest.hasError // GPU计算是否有报错
RenderTexture.active // 当前GPU输出缓冲的指针,指向一个RenderTexture
texture.ReadPixels() // 从RenderTexture.active指向的内容读取像素数据,读取范围和偏移由Rect结构体决定
texture.SetPixelData() // 从NativeArray<Color>直接读取颜色数据

哪些数据可以被获取

想要在 CPU 侧(C# 中)获取通过 Compute Shader 在 GPU 中计算完成的数据,必须要满足以下的条件:

  1. 数据在 GPU 中必须是 可读写 的资源

    常见的可读写资源包括:

    • RWStructuredBuffer​ / RWByteAddressBuffer​(对应 C# 的 ComputeBuffer
    • RWTexture2D​ / RWTexture3D​(对应 C# 的 RenderTexture

    只有 GPU 写入了这些资源,CPU 才有可获取的结果

  2. 数据在 CPU 和 GPU 之间必须通过 ComputeBuffer​ 或 Texture 形式传递

    注意:

    1. 对于 标量类型、向量类型、矩阵类型 如果想要读取,需要使用可读写缓冲区进行包裹利用 ComputeBuffer 进行传递
    2. cbuffer(常量缓冲区)是单向的,只能 CPU 到 GPU,不可反向读取

获取 GPU 侧计算好的数据 —— ComputeBuffer

假设我们通过 Compute Shader 计算一个 int​ 数组和 Test​ 结构体数组,通过 buffer​ 和 buffer2 接收计算结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#pragma kernel CSMain
#pragma kernel CSMain2

struct Test
{
float3 pos;
float3 v;
float lifeTime;
};

RWStructuredBuffer<int> buffer;
RWStructuredBuffer<Test> buffer2;

[numthreads(32,1,1)]
void CSMain (uint3 id : SV_DispatchThreadID)
{
buffer[id.x] = id.x;
}

[numthreads(32,1,1)]
void CSMain2 (uint3 id : SV_DispatchThreadID)
{
Test t = buffer2[id.x];
t.pos.x = id.x;
t.v.y = id.x * 10;
t.lifeTime = 999;
buffer2[id.x] = t; // 修改完结构体需要再存回去
}

同步获取 ComputeBuffer 数据

该获取方式主要是利用 ComputeBuffer​ 中的 GetData 方法
但是该方法会阻塞 CPU,会等待 GPU 计算完成后得到数据才会继续执行后面的逻辑

注意:GPU 一定是用来处理大量数据的,若数据量不大,则 CPU 和 GPU 的通信成本可能大于 GPU 的计算成本,反而不如直接使用 CPU 循环处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
using UnityEngine;

// size = 12 + 12 + 4 = 28 byte
public struct Test
{
public Vector3 pos;
public Vector3 v;
public float lifetime;
}

public class Lesson98 : MonoBehaviour
{
public ComputeShader computeShader;

void Start()
{
// 设置int数组计算缓冲区数据
var computeBuffer = new ComputeBuffer(1000000, sizeof(int));
var array = new int[1000000];
computeBuffer.SetData(array);
// 设置Test结构体数组计算缓冲区数据
var computeBuffer2 = new ComputeBuffer(2000000, 28);
var array2 = new Test[1000000];
computeBuffer2.SetData(array2);
// 将两个缓冲区设置到ComputeShader中
var index = computeShader.FindKernel("CSMain");
var index2 = computeShader.FindKernel("CSMain2");
computeShader.SetBuffer(index, "buffer", computeBuffer);
computeShader.SetBuffer(index2, "buffer2", computeBuffer2);
// 计算启动线程组数量,启动ComputeShader
int groupNums = Mathf.CeilToInt(1000000f / 32f);
computeShader.Dispatch(index, groupNums, 1, 1);
int groupNums2 = Mathf.CeilToInt(2000000f / 32f);
computeShader.Dispatch(index2, groupNums2, 1, 1);

computeBuffer.GetData(array);
print($"array: [0]: {array[0]}, [50]: {array[50]}, [500]: {array[500]}, [5000]: {array[5000]}, [50000]: {array[50000]}");
computeBuffer2.GetData(array2);
print($"array2[0]: pos: {array2[0].pos}, v: {array2[0].v}, lifeTime: {array2[0].lifetime}");
print($"array2[50]: pos: {array2[50].pos}, v: {array2[50].v}, lifeTime: {array2[50].lifetime}");
print($"array2[500]: pos: {array2[500].pos}, v: {array2[500].v}, lifeTime: {array2[500].lifetime}");
print($"array2[5000]: pos: {array2[5000].pos}, v: {array2[5000].v}, lifeTime: {array2[5000].lifetime}");
}
}

输出:

image

可以看到,两个数组在 GPU 中计算了数据

异步获取 ComputeBuffer 数据

若不想阻塞 CPU,待 GPU 计算完成后自动回调,则可以使用 AsyncGPUReadback.Request()​ 方法来异步读取数据,
需要传入要异步等待获取数据的 ComputeBuffer​ 对象,以及参数为 AsyncGPUReadbackRequest 回调函数,异步获取时最快也得下一帧返回信息

通过 asyncGPUReadbackRequest.hasError​ 可以判断计算过程中是否出错,若未出错再去通过 AsyncGPUReadbackRequest​ 获取数据
通过 asyncGPUReadbackRequest.GetData<>()​ 传入数据元素的类型,获取 NativeArray<> 数组,通过该数组即可读取计算结果

注意:回调函数中参数的 asyncGPUReadbackRequest.GetData<>()​ 方法,传入的泛型是结构体,代表单个元素的类型
asyncGPUReadbackRequest.GetData<>()​ 方法会返回一个 NativeArray<> 数组,建议直接使用,而不建议在此基础上再做任何强转,例如转换为 C# 数组等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
using UnityEngine;
using UnityEngine.Rendering;

// size = 12 + 12 + 4 = 28 byte
public struct Test
{
public Vector3 pos;
public Vector3 v;
public float lifetime;
}

public class Lesson98 : MonoBehaviour
{
public ComputeShader computeShader;

void Start()
{
// 设置int数组计算缓冲区数据
var computeBuffer = new ComputeBuffer(1000000, sizeof(int));
var array = new int[1000000];
computeBuffer.SetData(array);
// 设置Test结构体数组计算缓冲区数据
var computeBuffer2 = new ComputeBuffer(2000000, 28);
var array2 = new Test[1000000];
computeBuffer2.SetData(array2);
// 将两个缓冲区设置到ComputeShader中
var index = computeShader.FindKernel("CSMain");
var index2 = computeShader.FindKernel("CSMain2");
computeShader.SetBuffer(index, "buffer", computeBuffer);
computeShader.SetBuffer(index2, "buffer2", computeBuffer2);
// 计算启动线程组数量,启动ComputeShader
int groupNums = Mathf.CeilToInt(1000000f / 32f);
computeShader.Dispatch(index, groupNums, 1, 1);
int groupNums2 = Mathf.CeilToInt(2000000f / 32f);
computeShader.Dispatch(index2, groupNums2, 1, 1);

AsyncGPUReadback.Request(computeBuffer, (request) =>
{
if (!request.hasError)
{
var array = request.GetData<int>(); // return NativeArray<int>
print($"array: [0]: {array[0]}, [50]: {array[50]}, [500]: {array[500]}, [5000]: {array[5000]}, [50000]: {array[50000]}");
}
});

AsyncGPUReadback.Request(computeBuffer2, (request) =>
{
if (!request.hasError)
{
var array2 = request.GetData<Test>(); // return NativeArray<Test>
print($"array2[0]: pos: {array2[0].pos}, v: {array2[0].v}, lifeTime: {array2[0].lifetime}");
print($"array2[50]: pos: {array2[50].pos}, v: {array2[50].v}, lifeTime: {array2[50].lifetime}");
print($"array2[500]: pos: {array2[500].pos}, v: {array2[500].v}, lifeTime: {array2[500].lifetime}");
print($"array2[5000]: pos: {array2[5000].pos}, v: {array2[5000].v}, lifeTime: {array2[5000].lifetime}");
}
});
}
}

输出:

image

获取 GPU 侧计算好的数据 —— Texture

假设我们通过 Compute Shader 计算一个贴图的颜色,
让贴图的左下角为黑色 RGBA(0,0,0,1)​,右下角为红色 RGBA(1,0,0,1)​,左上角为绿色 RGBA(0,1,0,1)​,右上角为红色绿色混合 RGBA(1,1,0,1)

1
2
3
4
5
6
7
8
9
10
11
12
#pragma kernel CSMain

float textureWidth; // 贴图的宽度
float textureHeight; // 贴图的高度
RWTexture2D<float4> Result;

[numthreads(8,8,1)]
void CSMain (uint3 id : SV_DispatchThreadID)
{
// 根据坐标计算像素颜色
Result[id.xy] = float4(id.x / textureWidth, id.y / textureHeight, 0, 1);
}

同步获取 Texture 数据

利用传入 RenderTexture​ 数据进行写入,写入完成后利用 Texture 装载结果数据

具体做法是,先将 RenderTexture.active​ (相当于当前 GPU 输出缓冲的指针)设置为传入到 Compute Shader 内的 RenderTexture
然后通过 texture.ReadPixels()​ 从 RenderTexture.active​ 读取像素,读取范围就是 RenderTexture​ 的尺寸
读取完毕后需要使用 texture.Apply() 应用读取结果

注意,使用完毕后需要将 RenderTexture.active​ 设置为 null​,释放当前 GPU 输出缓冲的指针,防止影响后续渲染流程
然后,将计算完毕的 RenderTexture​ 释放(Release())掉,避免内存泄漏

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.UI;

public class Lesson98 : MonoBehaviour
{
public ComputeShader computeShader2;
public RawImage image;

void Start()
{
// 创建RenderTexture,设置属性,传递到ComputeShader中
var renderTexture = new RenderTexture(512, 512, 0);
renderTexture.enableRandomWrite = true;
renderTexture.Create(); // 让GPU侧为当前数据分配显存
var index3 = computeShader2.FindKernel("CSMain");
computeShader2.SetFloat("textureWidth", renderTexture.width);
computeShader2.SetFloat("textureHeight", renderTexture.height);
computeShader2.SetTexture(index3, "Result", renderTexture);
// 计算启动线程组数量,确保线程组所有线程覆盖所有像素,启动ComputeShader
var groupNumX = Mathf.CeilToInt(renderTexture.width / 8f);
var groupNumY = Mathf.CeilToInt(renderTexture.height / 8f);
computeShader2.Dispatch(index3, groupNumX, groupNumY, 1);

// 同步获取结果,如果要同步获取贴图结果,一定要使用Texture类型转存,将计算完毕的贴图显示在UI上
var texture = new Texture2D(renderTexture.width, renderTexture.height, TextureFormat.RGBA32, false);
RenderTexture.active = renderTexture; // 将当前GPU输出缓冲的指针指向计算出来的RenderTexture上
// 将当前GPU输出缓冲的指针指向的RenderTexture数据读入texture中
texture.ReadPixels(new Rect(0, 0, renderTexture.width, renderTexture.height), 0, 0);
texture.Apply(); // 让Texture生效
image.texture = texture;
RenderTexture.active = null; // 释放当前GPU输出缓冲的指针,防止影响后续渲染流程
renderTexture.Release(); // 释放使用完毕的RenderTexture资源
}
}

渲染结果:

image

异步获取 Texture 数据

若不想阻塞 CPU,待 GPU 计算完成后自动回调,则可以使用 AsyncGPUReadback​ 来异步读取数据
需要传入要异步等待获取数据的 RenderTexture​ 对象,以及参数为 AsyncGPUReadbackRequest 回调函数,异步获取时最快也得下一帧返回信息

通过 asyncGPUReadbackRequest.hasError​ 可以判断计算过程中是否出错,若未出错再去通过 AsyncGPUReadbackRequest​ 获取数据
通过 asyncGPUReadbackRequest.GetData<>()​ 传入 Color​ 类型,获取 NativeArray<Color>​ 颜色数据信息,
通过 texture.SetPixelData()​ 即可从 NativeArray<Color> 读取计算结果

注意:在纹理回调中可以直接获取颜色数据 NativeArray<Color>​,然后通过 texture.SetPixelData() 赋值给纹理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.UI;

public class Lesson98 : MonoBehaviour
{
public ComputeShader computeShader2;
public RawImage image;

void Start()
{
// 创建RenderTexture,设置属性,传递到ComputeShader中
var renderTexture = new RenderTexture(512, 512, 0);
renderTexture.enableRandomWrite = true;
renderTexture.Create(); // 让GPU侧为当前数据分配显存
var index3 = computeShader2.FindKernel("CSMain");
computeShader2.SetFloat("textureWidth", renderTexture.width);
computeShader2.SetFloat("textureHeight", renderTexture.height);
computeShader2.SetTexture(index3, "Result", renderTexture);
// 计算启动线程组数量,确保线程组所有线程覆盖所有像素,启动ComputeShader
var groupNumX = Mathf.CeilToInt(renderTexture.width / 8f);
var groupNumY = Mathf.CeilToInt(renderTexture.height / 8f);
computeShader2.Dispatch(index3, groupNumX, groupNumY, 1);
// 异步获取结果
AsyncGPUReadback.Request(renderTexture, 0, (request) =>
{
if (!request.hasError)
{
var texture = new Texture2D(renderTexture.width, renderTexture.height, TextureFormat.RGBA32, false);
var data = request.GetData<Color>(); // 获取RenderTexture计算完毕的颜色数据
texture.SetPixelData(data, 0); // 将颜色数据写入texture中
texture.Apply(); // 让Texture生效
image.texture = texture;
renderTexture.Release(); // 释放使用完毕的RenderTexture资源
}
});
}
}

渲染结果:

image