UPL9-17-c——数据输入规则

本章代码关键字

1
2
3
4
5
6
7
8
9
10
11
12
13
computeShader.SetFloat()        // 设置float类型基本数据
computeShader.SetInt() // 设置int类型基本数据,也可以用于设置uint类型数据
computeShader.SetBool() // 设置bool类型基本数据
computeShader.SetVector() // 设置float4(Vector4)向量数据,也可以用于非float4(Vector4)向量数据传递
computeShader.SetFloats() // 可以用于设置float3, float2向量数据,也可以用于设置float数组数据
computeShader.SetInts() // 可以用于设置int3, int2向量数据,也可以用于设置int数组数据
computeShader.SetMatrix() // 设置float4x4(Matrix4x4)矩阵,如果要传递非4x4矩阵需要在ComputeShader内自行截取
computeShader.SetBuffer() // 设置ComputeBuffer缓冲区数据
computeShader.SetTexture() // 设置纹理数据
ComputeBuffer // 存储ComputeShader缓冲区的类,实例化时需要传入缓冲区内存储的元素数量,单个元素字节数,缓冲区类型
ComputeBufferType.Raw // 如果传递的是ByteAddressBuffer/RWByteAddressBuffer,实例化ComputeShader时,传入此枚举
ComputeBufferType.Append // 如果传递的是AppendStructuredBuffer<T>/ConsumeStructuredBuffer<T>,实例化ComputeShader时,传入此枚举
computeBuffer.SetData() // 将待传递的数据设置在缓冲区内

CPU侧(C# 侧)的数据输入规则

我们上一节学习了 Compute Shader 中的各种数据类型
而这些类型的数据都需要从 CPU 侧(C# 中)进行输入之后,才能在 GPU 中利用这些数据进行计算
因此,这里主要阐述 C# 侧如何向 Compute Shader 中输入数据

标量数据输入

  1. float

    1
    computeShader.SetFloat("name", value)
  2. int​、uint

    uint​ 没有专门的输入 API,因此直接使用 SetInt() 方法

    1
    computeShader.SetInt("name", value)
  3. bool

    1
    computeShader.SetBool("name", true)

注意:

  1. 虽然上节课我们提到 ComputeShader 中支持 double​ 和 half
    但是它们仅在少数 GPU 上可用,因此 C# 侧也没有提供专门的 API 进行数据输入
    因此建议不要在 ComputeShader 中使用它们
  2. 所有 Set​ 相关方法都需要在 Dispatch 之前调用
  3. 所有变量名需要和 ComputeShader 内声明一致

假设要输入以下 ComputeShader 变量:

1
2
3
4
5
6
7
bool b;
int i;
uint ui;
float f;

[numthreads(8,8,1)]
void CSMain (uint3 id : SV_DispatchThreadID) { /*...*/ }

在 C# 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
public ComputeShader computeShader;     // 声明 computeShader 变量,可以通过 Inspector 窗口关联,也可以动态加载

void Start()
{
int index = computeShader.FindKernel("CSMain"); // 获取 computeShader 中名为 CSMain 的函数索引

computeShader.SetBool("b", true);
computeShader.SetInt("i", 100);
computeShader.SetInt("ui", 999);
computeShader.SetFloat("f", 1.0f);

computeShader.Dispatch(index, 64, 64, 0); // 启动 CSMain 的计算
}

向量数据输入

向量数据类型都是通过 SetVector​ 进行输入,有几个元素就传几个元素数值进去即可,
但是,如果是非 4 维的向量,建议使用 SetFloatsSetInts(存疑,需要进一步验证)

  1. float2

    需要传入 Vector4 类型向量,Compute Shader 内部会自动截取前两个分量

    1
    computeShader.SetVector("name", new Vector4(x, y, 0, 0))

    建议:

    1
    computeShader.SetFloats("name", ....)
  2. float3

    需要传入 Vector4 类型向量,Compute Shader 内部会自动截取前三个分量

    1
    computeShader.SetVector("name", new Vector4(x, y, z, 0))

    建议:

    1
    computeShader.SetFloats("name", ....)
  3. float4

    1
    computeShader.SetVector("name", new Vector4(x, y, z, w))

假设要输入以下 ComputeShader 变量:

1
2
3
4
5
6
7
8
9
float2 f2;
float3 f3;
float4 f4;
int2 i2;
int3 i3;
int4 i4;

[numthreads(8,8,1)]
void CSMain (uint3 id : SV_DispatchThreadID) { /*...*/ }

在 C# 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public ComputeShader computeShader;     // 声明 computeShader 变量,可以通过 Inspector 窗口关联,也可以动态加载

void Start()
{
int index = computeShader.FindKernel("CSMain"); // 获取 computeShader 中名为 CSMain 的函数索引

computeShader.SetVector("f2", new Vector4(1, 2, 0, 0));
computeShader.SetVector("f3", new Vector4(1, 2, 3, 0));
computeShader.SetVector("f4", new Vector4(1, 2, 3, 4));
// int类型向量也使用Vector4传递
computeShader.SetVector("i2", new Vector4(1, 2, 0, 0));
computeShader.SetVector("i3", new Vector4(1, 2, 3, 0));
computeShader.SetVector("i4", new Vector4(1, 2, 3, 4));

computeShader.Dispatch(index, 64, 64, 0); // 启动 CSMain 的计算
}

对于 Vector2​ 和 Vector3​ 向量以及 int​ 类型向量,使用 SetVector​ 方法传递由于会将值放在 Vector4​ 内,
因此 Vector4​ 内部分分量会被抛弃,在部分平台上有风险,因此,对于非 Vector4​ 数据的传递,建议使用 SetFloats​ 和 SetInts 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public ComputeShader computeShader;     // 声明 computeShader 变量,可以通过 Inspector 窗口关联,也可以动态加载

void Start()
{
int index = computeShader.FindKernel("CSMain"); // 获取 computeShader 中名为 CSMain 的函数索引

computeShader.SetFloats("f2", 1, 2);
computeShader.SetFloats("f3", 1, 2, 3);
computeShader.SetVector("f4", new Vector4(1, 2, 3, 4));
computeShader.SetInts("i2", 1, 2);
computeShader.SetInts("i3", 1, 2, 3);
computeShader.SetInts("i4", 1, 2, 3, 4);

computeShader.Dispatch(index, 64, 64, 0); // 启动 CSMain 的计算
}

对于数组类型变量,也可以使用 SetFloats​ 和 SetInts 方法传递

1
2
3
4
float fArray[10];

[numthreads(8,8,1)]
void CSMain (uint3 id : SV_DispatchThreadID) { /*...*/ }

在 C# 中:

1
2
3
4
5
6
7
8
9
10
public ComputeShader computeShader;     // 声明 computeShader 变量,可以通过 Inspector 窗口关联,也可以动态加载

void Start()
{
int index = computeShader.FindKernel("CSMain"); // 获取 computeShader 中名为 CSMain 的函数索引

computeShader.SetFloats("fArray", 1, 2, 3, 4, 5, 6, 7, 8);

computeShader.Dispatch(index, 64, 64, 0); // 启动 CSMain 的计算
}

矩阵数据输入

  • float4x4

    1
    computeShader.SetMatrix("name", Matrix4x4)

如果想要传小于 4×44 \times 4 的矩阵,还是使用 SetMatrix()​ 传 Matrix4x4 类型矩阵,然后在 Compute Shader 中进行截取转换

1
2
3
4
5
6
7
8
9
// 常量参数
float4x4 _M; // 用 SetMatrix 传

[numthreads(8,8,1)]
void CSMain (uint3 id : SV_DispatchThreadID)
{
// 需要 3x3 时:
float3x3 M3 = (float3x3)_M;
}

假设要输入以下 ComputeShader 变量:

1
2
3
4
5
6
7
8
9
10
11
float4x4 f44;
float4x4 f33; // 传递使用4x4矩阵,实际作为3x3矩阵使用
float4x4 f22; // 传递使用4x4矩阵,实际作为2x2矩阵使用

[numthreads(8,8,1)]
void CSMain (uint3 id : SV_DispatchThreadID)
{
// 对于非4x4的矩阵传递,因为只能传递4x4矩阵,因此我们需要手动去截取
float3x3 m33 = (float3x3)f33; // 需要手动去截取矩阵
float2x2 m22 = (float2x2)f22;
}

在 C# 中:

1
2
3
4
5
6
7
8
9
10
11
12
public ComputeShader computeShader;     // 声明 computeShader 变量,可以通过 Inspector 窗口关联,也可以动态加载

void Start()
{
int index = computeShader.FindKernel("CSMain"); // 获取 computeShader 中名为 CSMain 的函数索引

computeShader.SetMatrix("f44", Matrix4x4.identity);
computeShader.SetMatrix("f33", Matrix4x4.identity);
computeShader.SetMatrix("f22", Matrix4x4.identity);

computeShader.Dispatch(index, 64, 64, 0); // 启动 CSMain 的计算
}

复合类型输入

Compute Shader 的结构体和缓冲区数据需要通过 ComputeBuffer​ 传入
这个 Buffer 相当于 CPU 和 GPU 之间的共享显存区域
GPU 端(HLSL)和 CPU 端(C#)都要定义完全相同的结构布局
因此必须满足以下三个关键点:

  1. 结构体字段顺序必须一致
  2. 字段类型大小必须匹配
  3. 长度必须是结构体总字节数

Compute Shader 中涉及到 Buffer 相关的数据,都可以通过以下方式在 C# 侧输入数据

C# 侧输入步骤:

  1. 声明对应的数组或 List,存储对应数据

  2. 实例化 ComputeBuffer​ 对象:new ComputeBuffer(对象数量, 单个对象所占字节数, 缓冲区类型)​,不同的缓冲区使用 ComputeBufferType 参数

    • StructuredBuffer<T>​ / RWStructuredBuffer<T>​ 使用 ComputeBufferType.Default(默认值,可以不传)
    • ByteAddressBuffer​ / RWByteAddressBuffer​ 使用 ComputeBufferType.Raw
    • AppendStructuredBuffer<T>​ / ConsumeStructuredBuffer<T>​ 使用 ComputeBufferType.Append
  3. 利用 ComputeBuffer​ 对象的 SetData​ API,将 C# 数据输入

  4. 利用 ComputeShader​ 的 SetBuffer​ 方法,输入 ComputeBuffer 数据

注意:

  1. ComputeBuffer​ 的步长参数(stride)必须等于 GPU 端结构体(或者基本类型)实际字节数

  2. 特殊缓冲类型 AppendStructuredBuffer<T>​ / ConsumeStructuredBuffer<T>

    在 C# 端需要使用 new ComputeBuffer(count, stride, ComputeBufferType.Append)​,其中 count​ 代表缓冲区最多有几个元素
    其中 AppendStructuredBuffer​ 在使用前调用 buffer.SetCounterValue(0)​,而 ConsumeStructuredBuffer 需要输入数据

传递结构体时,在 C# 需要定义相同的结构体,传递时必须要使用缓冲区,最常用的就是 StructuredBuffer<T>​ / RWStructuredBuffer<T>

注意:新版本中有一个 GraphicsBuffer​ 类,它的用法和 ComputeBuffer 类似,它可以和 SRP 等管线更好地配合

假设要输入以下 ComputeShader​ 变量(Test​ 结构体通过 StructuredBuffer<T> 传递):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct Test
{
float3 position;
float3 velocity;
float lifetime;
};

StructuredBuffer<Test> buffer;
// RWStructuredBuffer<Test> buffer2; // 传递方式和StructuredBuffer<T>一致
ByteAddressBuffer buffer3;
// RWByteAddressBuffer buffer4; // 传递方式和ByteAddressBuffer一致
AppendStructuredBuffer<float> buffer5;
ConsumeStructuredBuffer<float> buffer6;

[numthreads(8,8,1)]
void CSMain (uint3 id : SV_DispatchThreadID) { /*...*/ }

在 C# 中:

  • 传递结构体 / 多个结构体时,使用 StructuredBuffer<T>​ / RWStructuredBuffer<T>

    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
    public ComputeShader computeShader;     // 声明 computeShader 变量,可以通过 Inspector 窗口关联,也可以动态加载

    // 一个 Test 结构体大小为 28 byte
    struct Test
    {
    Vector3 position; // 12 byte
    Vector3 velocity; // 12 byte
    float lifetime; // 4 byte
    }

    void Start()
    {
    int index = computeShader.FindKernel("CSMain"); // 获取 computeShader 中名为 CSMain 的函数索引

    // StructuredBuffer / RWStructuredBuffer
    // 假设要传递3个结构体数据,结构体大小为 28 byte,通过 StructuredBuffer 传递
    var list = new List<Test>();
    list.Add(new Test());
    list.Add(new Test());
    list.Add(new Test());
    var computeBuffer = new ComputeBuffer(list.Count, 28);
    computeBuffer.SetData(list);
    computeShader.SetBuffer(index, "buffer", computeBuffer);

    computeShader.Dispatch(index, 64, 64, 0); // 启动 CSMain 的计算
    }
  • 传递多个基本类型时,可以使用 ByteAddressBuffer​ / RWByteAddressBuffer

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    public ComputeShader computeShader;     // 声明 computeShader 变量,可以通过 Inspector 窗口关联,也可以动态加载

    void Start()
    {
    int index = computeShader.FindKernel("CSMain"); // 获取 computeShader 中名为 CSMain 的函数索引

    // ByteAddressBuffer / RWByteAddressBuffer
    // 假设要传递64个int数据,通过 ByteAddressBuffer 传递
    var list2 = new List<int>();
    for (int i = 0; i < 64; i++)
    {
    list2.Add(i);
    }
    var computeBuffer2 = new ComputeBuffer(64, 4, ComputeBufferType.Raw);
    computeBuffer2.SetData(list2);
    computeShader.SetBuffer(index2, "buffer3", computeBuffer2);

    computeShader.Dispatch(index, 64, 64, 0); // 启动 CSMain 的计算
    }
  • 传递只能加或者只能减的 AppendStructuredBuffer<T>​ / ConsumeStructuredBuffer<T> 的缓冲区时

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    public ComputeShader computeShader;     // 声明 computeShader 变量,可以通过 Inspector 窗口关联,也可以动态加载

    void Start()
    {
    int index = computeShader.FindKernel("CSMain"); // 获取 computeShader 中名为 CSMain 的函数索引

    // AppendStructuredBuffer / ConsumeStructuredBuffer
    // 假设 AppendStructuredBuffer 最多能加 10 个 float(4byte) 数据,ConsumeStructuredBuffer 包含 10 个float(4byte)数据
    var computeBuffer3 = new ComputeBuffer(10, 4, ComputeBufferType.Append);
    var computeBuffer4 = new ComputeBuffer(10, 4, ComputeBufferType.Append);
    // 如果是只能加的 AppendStructuredBuffer,需要设置为0,
    computeBuffer3.SetCounterValue(0); // 告诉ComputeShader,AppendStructuredBuffer内无数据
    computeShader.SetBuffer(index2, "buffer5", computeBuffer3);
    // 如果是只能减的 ConsumeStructuredBuffer,需要设置其数据
    var list3 = new List<float>();
    for (int i = 0; i < 10; i++)
    {
    list2.Add(i);
    }
    computeBuffer4.SetData(list3);
    computeShader.SetBuffer(index2, "buffer6", computeBuffer4);

    computeShader.Dispatch(index, 64, 64, 0); // 启动 CSMain 的计算
    }

纹理类型输入

1
computeShader.SetTexture(入口函数索引, 变量名, 纹理对象)

其中输入的纹理对象类型:

  • 普通 Texture2D​:只能读,不能写,适合作为输入,ComputeShader 使用 Texture2D<T> 接收

  • RenderTexture​:可随机写,适合作为输出,ComputeShader 使用 RWTexture2D<T> 接收

    注意:

    如果需要传入 RenderTexture​ 对象,需要将其 enableRandomWrite​ 属性设置为 true​,代表可随机写入,一般是用于写入的纹理资源
    RWTexture2D​ / RWTexture3D​ 绑定的纹理必须是 enableRandomWrite = true​ 的 RenderTexture

假设要输入以下 ComputeShader 变量:

1
2
3
4
5
Texture2D<float4> tex;
RWTexture2D<float4> tex2;

[numthreads(8,8,1)]
void CSMain (uint3 id : SV_DispatchThreadID) { /*...*/ }

在 C# 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public ComputeShader computeShader;     // 声明 computeShader 变量,可以通过 Inspector 窗口关联,也可以动态加载
public Texture2D texture;

void Start()
{
int index = computeShader.FindKernel("CSMain"); // 获取 computeShader 中名为 CSMain 的函数索引

// 传递只读的Texture2D
computeShader.SetTexture(index, "tex", texture);
// 传递可读写的RenderTexture
var renderTexture = new RenderTexture(512, 512, 1);
renderTexture.enableRandomWrite = true; // 对于可读可写的RenderTexture,必须设置其可以随机写入
computeShader.SetTexture(index, "tex2", renderTexture);

computeShader.Dispatch(index, 64, 64, 0); // 启动 CSMain 的计算
}

常量缓冲区输入

只需要匹配 cbuffer​ 中声明的变量名直接使用 SetXXX()​ 方法进行设置即可,和 C# 设置 Compute Shader 基本类型变量一样

假设要输入以下 ComputeShader 常量缓冲区:

1
2
3
4
5
6
7
8
9
cbuffer Params
{
float deltaTime;
float intensity;
int count;
};

[numthreads(8,8,1)]
void CSMain (uint3 id : SV_DispatchThreadID) { /*...*/ }

在 C# 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public ComputeShader computeShader;     // 声明 computeShader 变量,可以通过 Inspector 窗口关联,也可以动态加载
public Texture2D texture;

void Start()
{
int index = computeShader.FindKernel("CSMain"); // 获取 computeShader 中名为 CSMain 的函数索引

// 设置常量缓冲区的值
computeShader.SetFloat("deltaTime", 1.0f);
computeShader.SetFloat("intensity", 2.5f);
computeShader.SetInt("count", 10);

computeShader.Dispatch(index, 64, 64, 0); // 启动 CSMain 的计算
}