Unity ComputeShader 使用
Compute Shader与常规 Shader均以图形处理器(GPU)为中心运行。然而与常规 Shader相比其独特之处在于无需经过渲染流水线阶段,并且能够通过充分利用 GPU 的并行处理能力来处理复杂的计算需求。相较于将复杂运算移至 CPU 处理而言这种设计不仅能够显著提升运算效率还能极大缩短处理时间从而实现游戏性能的优化提升。具体而言,在实现时通常采用两种方式:一种基于纹理资源的绑定机制;另一种则是通过动态缓冲区来进行数据交互
1. 纹理(Texture)
ComputeShader 能够将处理结果存储到纹理中,在自定义 Shader 中使用这些纹理。
步骤 :
为了在 Compute Shader 中进行数据的输入与输出操作,请使用 RWTexture2D 或 Texture2D 类。在 C# 编程过程中,请创建一个 RenderTexture 对象,并将此对象传递给相关的 Compute Shader 程序。当编写自定义 shader 程序时,请确保将 RenderTexture 作为纹理源进行采样。
示例 :
1. // Compute Shader
2. #pragma kernel CSMain
3.
4. Texture2D<float4> inputTex; //输入纹理(只读的)
5. RWTexture2D<float4> outputTex; //输出纹理(可读写) 保存我们处理的结果
6.
7. [numthreads(8, 8, 1)]
8. void CSMain (uint3 id : SV_DispatchThreadID) {
9. float a = inputTex[id.xy].a;
10. float gray = dot(inputTex[id.xy].rgb, float3(0.299, 0.587, 0.114));
11. outputTex[id.xy] = float4(gray, gray, gray, a); // 保存每个像素的处理结果
12. }
1. // C# 脚本
2. RenderTexture renderTexture = new RenderTexture(inputTex.width, inputTex.height, 24);
3. renderTexture.enableRandomWrite = true; //只有RT支持随机读写
4. renderTexture.Create();
5.
6. int kernalIndex = computeShader.FindKernel("CSMain");
7. computeShader.SetTexture(kernalIndex, "inputTex", inputTex);
8. computeShader.SetTexture(kernalIndex, "outputTex", renderTexture);
9. computeShader.Dispatch(kernalIndex, inputTex.width / 8, inputTex.height / 8, 1);
10.
11. // 在自定义 Shader 中使用 renderTexture
12. material.SetTexture("_MainTex", renderTexture);
2. 缓冲区(Buffer)
ComputeShader 支持使用 buffer structures(包括 StructuredBuffer 和 RWStructuredBuffer)用于存储数据,并在自定义 Shadert 中进行访问。
步骤 :
在 Compute Shader 中声明一个 RWStructuredBuffer 用于数据写入;在 C# 脚本中生成一个 ComputeBuffer 并将其传递给 Compute Shader;在自定义 Shader 中调用 StructuredBuffer 来读取数据。
示例 :
1. // Compute Shader
2. #pragma kernel CSMain
3. struct VertexData
4. {
5. float3 pos;
6. float4 color;
7. };
8.
9. RWStructuredBuffer<VertexData> vertexBuffer;
10.
11. [numthreads(8, 8, 1)]
12. void CSMain(uint3 gid : SV_GroupID, uint3 id : SV_DispatchThreadID)
13. {
14. // 计算当前线程的位置
15. float2 position = float2(id.x, id.y) / float2(512, 512) * 2.0 - 1.0; // 归一化到 [-1, 1]
16.
17. // 设置点的颜色为绿色
18. float4 color = float4(0, 1, 0, 1);
19.
20. // 将顶点数据写入缓冲区
21. vertexBuffer[id.x + id.y * 512] = VertexData(float3(position, 0), color);
22. }
1. // C# 脚本
2. int width = 512;
3. int height = 512;
4. int vertexCount = width * height;
5. ComputeBuffer computeBuffer = new ComputeBuffer(vertexCount, sizeof(float) * 7); // 每个点包含位置和颜色
6.
7. int kernalIndex = computeShader.FindKernel("CSMain");
8. computeShader.SetBuffer(kernelIndex, "vertexBuffer", computeBuffer);
9. computeShader.Dispatch(kernelIndex, width / 8, height / 8, 1);
10.
11. // 在自定义 Shader 中使用 computeBuffer
12. material.SetBuffer("vertexBuffer", computeBuffer);
看完示例,解释下上面涉及到ComputeShader 中的一些参数:
numthreads
[numthreads(x, y, z)]用于指示各向别x,y,z方向上的线程数目,在该方向上的各向别线程组中的总线程数为xyz;各向别的同一类线程组中的成员可共用相应变量及专用函数来实现通信过程。
注意:
- 每个线程组所包含的线程数量一般受到最大值约束,在当前设置下最大为1024(这一数值可能会因GPU架构的不同而有所变化)。
- 线程组的空间维度一般限定在1到1024范围内;不同GPU类型可能设定不同的范围。
- 为了达到最佳性能,请根据计算需求以及目标硬件的能力设定合适的CUDA kernel群规模。
Dispatch
computeShader.Dispatch(kernelIndex, threadGroupsX, threadGroupsY, threadGroupsZ):用于执行 ComputeShader 的指令
kernelIndex对应于你要执行的计算内核的索引。threadGroupsX、threadGroupsY和threadGroupsZ分别表示XYZ三个方向上需要调度的线程组数量。
注意:
- 这些变量(threadGroupsX、threadGroupsY 和 threadGroupsZ)通常取决于输入数据规模以及 numthreads 参数设置。
- 线程组总数必须能够被 numthreads 定义的线程数量整除,以便确保所有线程都能够得到充分利用。
**CSMain参数**
内核函数参数主要有以下几种:
1. uint3 groupId : SV_GroupID
- 含义 :线程组的位置, 范围(0,0,0) - (dispatchX-1,dispatchY-1,dispatchZ-1)
2. uint3 groupThreadId : SV_GroupThreadID
- 意义:该范围(0, 0, 0)至(numthreadsX-1, numthreadsY-1, numthreadsZ-1)用于表示该线程在该线组中的位置信息
3. uint3 dispatchThreadId : SV_DispatchThreadID
定义:在线程组中的位置 = SV_GroupID * numthreads + SV_GroupThreadID
4. uint index : SV_GroupIndex
定义:该变量代表线程在该线程组中的索引值。其编号顺序是按x轴方向从左到右排列(x),接着是y轴方向从上到下排列(y),最后是z轴方向从前到后排列(z)。具体计算公式为:SV\_GroupThreadID.z \times numthreadsX \times numthreadsY + SV\_GroupThreadID.y \times numthreadsX + SV\_GroupThreadID.x
线程与线程组的关系结构图:

参考文档**:**
