99.ComputeShader数据输入规则

99.性能优化-GPU-着色器优化-ComputeShader-数据输入规则


99.1 知识点

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

上节课学习了 Compute Shader 中的各种数据类型,而这些类型的数据都需要从 CPU 侧(C# 中)进行输入,之后才能在 GPU 中利用这些数据进行计算。本节课将着重讲解 C# 侧如何向 Compute Shader 中输入数据。

标量数据输入

通过 ComputeShader 实例的对应 API 设置标量参数:

  • SetFloat("name", value) → 对应 float
  • SetInt("name", value) → 对应 int / uint
  • SetBool("name", value) → 对应 bool
computeShader.SetBool("b", true);
computeShader.SetInt("i", 100);
computeShader.SetInt("ui", 999);
computeShader.SetFloat("f", 0.5f);

注意:

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

向量数据输入

向量类型可以通过 SetVectorSetFloats / SetInts 进行设置:

  • float2SetFloats("name", x, y)SetVector("name", new Vector4(x, y, 0, 0))
  • float3SetFloats("name", x, y, z)SetVector("name", new Vector4(x, y, z, 0))
  • float4SetVector("name", new Vector4(x, y, z, w))
  • int2 / int3 / int4SetInts("name", ...)SetVector

建议非 4 维的向量使用 SetFloats / SetInts,避免 SetVector 的隐式多余分量。

// 浮点数组:对应 float fff[8]
computeShader.SetFloats("fff", 1, 2, 3, 4, 5, 6, 7, 8);

// float2:建议使用 SetFloats
// computeShader.SetVector("f2", new Vector4(1, 2, 0, 0));
computeShader.SetFloats("f2", 1, 2);

// float3:建议使用 SetFloats
// computeShader.SetVector("f3", new Vector4(1, 2, 3, 0));
computeShader.SetFloats("f3", 1, 2, 3);

// float4:使用 SetVector
computeShader.SetVector("f4", new Vector4(1, 2, 3, 4));

// int2:建议使用 SetInts
// computeShader.SetVector("i2", new Vector4(1, 2, 0, 0));
computeShader.SetInts("i2", 1, 2);

// int3:建议使用 SetInts
// computeShader.SetVector("i3", new Vector4(1, 2, 3, 0));
computeShader.SetInts("i3", 1, 2, 3);

// int4:建议使用 SetInts
// computeShader.SetVector("i4", new Vector4(1, 2, 3, 4));
computeShader.SetInts("i4", 1, 2, 3, 4);

矩阵数据输入

使用 SetMatrix("name", Matrix4x4) 设置矩阵参数,对应 Compute Shader 中的 float4x4

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

如果想要传小于 4×4 的矩阵,仍使用 SetMatrix 传 4×4,然后在 Compute Shader 中进行截取转换:

// 常量参数
float4x4 _M;   // 用 SetMatrix 传
// 需要 3x3 时:
float3x3 M3 = (float3x3)_M;

复合类型输入

结构体和缓冲区传入规则

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

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

C# 侧输入步骤

  1. 声明对应的数组或 List,存储对应数据
  2. new ComputeBuffer(对象数量, 单个对象所占字节数) 创建缓冲区对象
  3. 利用 ComputeBuffer 对象的 SetData API 将 C# 数据输入
  4. 利用 ComputeShaderSetBuffer 方法绑定缓冲区数据

注意:

  1. ComputeBuffer 的步长参数(stride)必须等于 GPU 端结构体实际字节数。
  2. 特殊缓冲类型 AppendStructuredBuffer / ConsumeStructuredBuffer 在 C# 端需要使用 new ComputeBuffer(count, stride, ComputeBufferType.Append),并且在使用前调用 buffer.SetCounterValue(0)

C# 侧声明对应的结构体

struct Test
{
    Vector3 position; // 12 字节
    Vector3 v;        // 12 字节
    float lifeTime;   // 4 字节
    // 一个结构体对象共占 28 字节
}

三种缓冲区传递示例

1. StructuredBuffer / RWStructuredBuffer

// 创建用于存储 Test 结构体的列表,每个 Test 结构体大小为 28 字节
List<Test> list = new List<Test>();
list.Add(new Test());
list.Add(new Test());
list.Add(new Test());

// 创建计算缓冲区:元素数量 3,每个元素 28 字节
ComputeBuffer computeBuffer = new ComputeBuffer(3, 28);
computeBuffer.SetData(list); // 将列表数据上传到 GPU 缓冲区
computeShader.SetBuffer(index, "buffer", computeBuffer); // 将缓冲区绑定到计算着色器

2. ByteAddressBuffer / RWByteAddressBuffer

// 创建包含 64 个整数的列表
List<int> list2 = new List<int>();
for (int i = 0; i < 64; i++)
{
    list2.Add(i);
}

// 创建原始 (Raw) 计算缓冲区:元素数量 64,每个元素 4 字节 (32 位整数)
// ComputeBufferType.Raw:创建原始内存缓冲区,不进行结构体打包
// 适合字节地址寻址,允许在计算着色器中使用 Load/Store 操作
ComputeBuffer computeBuffer2 = new ComputeBuffer(64, 4, ComputeBufferType.Raw);
computeBuffer2.SetData(list2); // 将整数列表上传到 GPU
computeShader.SetBuffer(index, "buffer3", computeBuffer2); // 绑定到计算着色器

3. AppendStructuredBuffer / ConsumeStructuredBuffer

// 创建追加 (Append) 类型计算缓冲区:元素数量 10,每个元素 4 字节
// ComputeBufferType.Append:创建追加缓冲区,支持在计算着色器末尾添加元素
// 适合需要动态添加结果的场景,如粒子系统生成
ComputeBuffer computeBuffer3 = new ComputeBuffer(10, 4, ComputeBufferType.Append);

// 设置计数器的初始值为 0,表示缓冲区当前为空
// 追加缓冲区内部维护一个计数器,跟踪已添加的元素数量
computeBuffer3.SetCounterValue(0);
computeShader.SetBuffer(index, "buffer5", computeBuffer3); // 绑定到计算着色器

纹理类型输入

使用 SetTexture(入口函数索引, 变量名, 纹理对象) 设置纹理参数:

  • 普通 Texture2D:只能读,不能写,适合作为输入
  • RenderTexture:可随机写,适合作为输出

注意: 如果需要传入 RenderTexture 对象,需要将其 enableRandomWrite 属性设置为 true,代表可随机写入。RWTexture2D / RWTexture3D 绑定的纹理必须是 enableRandomWrite = trueRenderTexture

// 创建 1024×1024 的渲染纹理
RenderTexture renderTexture = new RenderTexture(1024, 1024, 1);

// 启用随机写入,Compute Shader 才能通过 RWTexture2D 写入此纹理
renderTexture.enableRandomWrite = true;

// 将渲染纹理绑定到 Compute Shader
// "Result" 对应 Compute Shader 中声明的 RWTexture2D<float4> Result
computeShader.SetTexture(index, "Result", renderTexture);

// 如果需要绑定只读纹理(如输入贴图):
// computeShader.SetTexture(index, "Result2", tex);
// 其中 "Result2" 对应 Compute Shader 中的 Texture2D<float4> Result2

常量缓冲区输入

对于 cbuffer 中声明的变量,只需要匹配变量名,使用对应的 Set 方法进行设置即可。

computeShader.SetFloat("deltaTime", 1.0f);
computeShader.SetFloat("intensity", 2.5f);
computeShader.SetInt("count", 10);

99.2 知识点代码

Lesson99_性能优化_GPU_着色器优化_ComputeShader_数据输入规则.cs

using System.Collections.Generic;
using UnityEngine;

public class Lesson99_性能优化_GPU_着色器优化_ComputeShader_数据输入规则 : MonoBehaviour
{
    public ComputeShader computeShader;
    public Texture2D texture2D;

    struct Test
    {
        Vector3 position; // 12 字节
        Vector3 v;        // 12 字节

        float lifeTime;   // 4 字节
        // 一个结构体对象共占 28 字节
    }

    void Start()
    {
        // 1. 在 C# 代码中找到对应的 ComputeShader 文件

        // 2. 利用 API 找到入口函数索引
        //    FindKernel("入口函数名")
        int index = computeShader.FindKernel("CSMain");
        int index2 = computeShader.FindKernel("CSMain2");

        #region 知识点一 CPU 侧(C# 侧)的数据输入规则

        // 我们上节课学习了 Compute Shader 中的各种数据类型
        // 而这些类型的数据都需要从 CPU 侧(C# 中)进行输入
        // 之后才能在 GPU 中利用这些数据进行计算
        // 因此我们本节课将着重讲解 C# 侧如何向 Compute Shader 中输入数据

        #endregion

        #region 知识点二 标量数据输入

        // 1. float
        //    SetFloat("name", value)
        // 2. int、uint
        //    SetInt("name", value)
        // 3. bool
        //    SetBool("name", true)

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

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

        #endregion

        #region 知识点三 向量数据输入

        // 1. float2
        //    SetVector("name", new Vector4(x, y, 0, 0))
        //    建议 SetFloats("name", ....)
        // 2. float3
        //    SetVector("name", new Vector4(x, y, z, 0))
        //    建议 SetFloats("name", ....)
        // 3. float4
        //    SetVector("name", new Vector4(x, y, z, w))

        // 设置计算着色器的标量、向量和数组参数

        // 1. 设置浮点数组参数
        // 参数对应计算着色器中的:float fff[8];
        // 设置数组的 8 个元素值,用于传递一组浮点参数
        computeShader.SetFloats("fff", 1, 2, 3, 4, 5, 6, 7, 8);

        // 2. 设置二维浮点向量(两种方式)
        // 方式一:使用 SetVector,Vector4 的后两个分量会被忽略
        // computeShader.SetVector("f2", new Vector4(1, 2, 0, 0));
        // 方式二:使用 SetFloats 传递 2 个 float 值,对应 float2 类型
        // 参数对应计算着色器中的:float2 f2;
        computeShader.SetFloats("f2", 1, 2);

        // 3. 设置三维浮点向量(两种方式)
        // 方式一:使用 SetVector,Vector4 的第四个分量会被忽略
        // computeShader.SetVector("f3", new Vector4(1, 2, 3, 0));
        // 方式二:使用 SetFloats 传递 3 个 float 值,对应 float3 类型
        // 参数对应计算着色器中的:float3 f3;
        computeShader.SetFloats("f3", 1, 2, 3);

        // 4. 设置四维浮点向量
        // 使用 SetVector 传递 Vector4,4 个分量对应 float4 的 x, y, z, w
        // 参数对应计算着色器中的:float4 f4;
        computeShader.SetVector("f4", new Vector4(1, 2, 3, 4));

        // 5. 设置二维整数向量(两种方式)
        // 方式一:使用 SetVector,但需注意 Vector4 是 float 类型
        // computeShader.SetVector("i2", new Vector4(1, 2, 0, 0));
        // 方式二:使用 SetInts 传递 2 个 int 值,对应 int2 类型
        // 参数对应计算着色器中的:int2 i2;
        computeShader.SetInts("i2", 1, 2);

        // 6. 设置三维整数向量(两种方式)
        // 方式一:使用 SetVector,但需注意 Vector4 是 float 类型
        // computeShader.SetVector("i3", new Vector4(1, 2, 3, 0));
        // 方式二:使用 SetInts 传递 3 个 int 值,对应 int3 类型
        // 参数对应计算着色器中的:int3 i3;
        computeShader.SetInts("i3", 1, 2, 3);

        // 7. 设置四维整数向量(两种方式)
        // 方式一:使用 SetVector,但需注意 Vector4 是 float 类型
        // computeShader.SetVector("i4", new Vector4(1, 2, 3, 4));
        // 方式二:使用 SetInts 传递 4 个 int 值,对应 int4 类型
        // 参数对应计算着色器中的:int4 i4;
        computeShader.SetInts("i4", 1, 2, 3, 4);

        // 向量数据类型都是通过 SetVector 进行输入
        // 有几个元素就传几个元素数值进去即可
        // 但是建议非 4 维使用 SetFloats、SetInts

        #endregion

        #region 知识点四 矩阵数据输入

        // float4x4
        //   SetMatrix("name", Matrix4x4)

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

        // 如果想要传小于 4x4 的矩阵
        // 还是使用该 API,然后在 ComputeShader 中进行截取转换
        // 比如:
        // 常量参数
        // float4x4 _M;   // 用 SetMatrix 传
        // 需要 3x3 时:
        // float3x3 M3 = (float3x3)_M;

        #endregion

        #region 知识点五 复合类型输入

        // 结构体和缓冲区
        //   Compute Shader 的结构体和缓冲区数据需要通过 ComputeBuffer 传入
        //   这个 Buffer 相当于 CPU 和 GPU 之间的共享显存区域
        //   GPU 端(HLSL)和 CPU 端(C#)都要定义完全相同的结构布局
        //   因此必须满足以下三个关键点:
        //   1. 结构体字段顺序必须一致
        //   2. 字段类型大小必须匹配
        //   3. 长度必须是结构体总字节数

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

        // C# 侧输入步骤
        // 1. 声明对应的数组或 List,存储对应数据
        // 2. new ComputeBuffer(对象数量, 单个对象所占字节数) 对象
        // 3. 利用 ComputeBuffer 对象的 SetData API,将 C# 数据输入
        // 4. 利用 ComputeShader 的 SetBuffer 方法,输入 ComputeBuffer 数据

        // 注意:
        //   1. ComputeBuffer 的步长参数(stride)必须等于 GPU 端结构体实际字节数
        //   2. 特殊缓冲类型 AppendStructuredBuffer / ConsumeStructuredBuffer
        //      在 C# 端需要使用 new ComputeBuffer(count, stride, ComputeBufferType.Append)
        //      并且在使用前调用 buffer.SetCounterValue(0)

        // 设置计算着色器缓冲区参数示例

        // 1. StructuredBuffer / RWStructuredBuffer
        // 创建用于存储 Test 结构体的列表,每个 Test 结构体大小为 28 字节
        List<Test> list = new List<Test>();
        list.Add(new Test());
        list.Add(new Test());
        list.Add(new Test());

        // 创建计算缓冲区:元素数量 3,每个元素 28 字节
        // ComputeBuffer 构造函数参数:
        // 参数 1: count - 缓冲区中元素的数量 (3 个 Test 结构体)
        // 参数 2: stride - 每个元素占用的字节数 (Test 结构体大小为 28 字节)
        ComputeBuffer computeBuffer = new ComputeBuffer(3, 28);
        computeBuffer.SetData(list); // 将列表数据上传到 GPU 缓冲区
        computeShader.SetBuffer(index, "buffer", computeBuffer); // 将缓冲区绑定到计算着色器

        // 2. ByteAddressBuffer / RWByteAddressBuffer
        // 创建包含 64 个整数的列表
        List<int> list2 = new List<int>();
        for (int i = 0; i < 64; i++)
        {
            list2.Add(i);
        }

        // 创建原始 (Raw) 计算缓冲区:元素数量 64,每个元素 4 字节 (32 位整数)
        // ComputeBufferType.Raw:创建原始内存缓冲区,不进行结构体打包
        // 适合字节地址寻址,允许在计算着色器中使用 Load/Store 操作
        ComputeBuffer computeBuffer2 = new ComputeBuffer(64, 4, ComputeBufferType.Raw);
        computeBuffer2.SetData(list2); // 将整数列表上传到 GPU
        computeShader.SetBuffer(index, "buffer3", computeBuffer2); // 绑定到计算着色器

        // 3. AppendStructuredBuffer / ConsumeStructuredBuffer
        // 创建追加 (Append) 类型计算缓冲区:元素数量 10,每个元素 4 字节
        // ComputeBufferType.Append:创建追加缓冲区,支持在计算着色器末尾添加元素
        // 适合需要动态添加结果的场景,如粒子系统生成
        ComputeBuffer computeBuffer3 = new ComputeBuffer(10, 4, ComputeBufferType.Append);

        // 设置计数器的初始值为 0,表示缓冲区当前为空
        // 追加缓冲区内部维护一个计数器,跟踪已添加的元素数量
        computeBuffer3.SetCounterValue(0);
        computeShader.SetBuffer(index, "buffer5", computeBuffer3); // 绑定到计算着色器

        #endregion

        #region 知识点六 纹理类型输入

        // SetTexture(入口函数索引, 变量名, 纹理对象)
        // 普通 Texture2D:只能读,不能写,适合作为输入
        // RenderTexture:可随机写,适合作为输出
        // 注意:
        //   如果需要传入 RenderTexture 对象
        //   需要将其 enableRandomWrite 属性设置为 true
        //   代表可随机写入,一般是用于写入的纹理资源
        //   RWTexture2D / RWTexture3D 绑定的纹理必须是
        //   enableRandomWrite = true 的 RenderTexture

        #endregion

        #region 知识点七 常量缓冲区输入

        // 只需要匹配 cbuffer 中声明的变量名进行设置即可

        #endregion
    }
}

Lesson99_ComputeShader_DataInput.compute

// 每个 #kernel 指令声明一个可编译为 Compute Shader 入口点的函数
// 一个文件中可以声明多个内核函数,分别对应不同的计算任务
#pragma kernel CSMain
#pragma kernel CSMain2

// Compute Shader 支持的变量类型示例
// 基本标量类型
bool b;    // 布尔类型
int i;     // 32 位有符号整数
uint ui;   // 32 位无符号整数
float f;   // 32 位浮点数
// double d;  // 64 位双精度浮点数(部分平台支持)
// half h;    // 16 位半精度浮点数

// 数组类型
float fff[8]; // 包含 8 个 float 元素的数组

// 向量类型(常用于坐标、颜色等)
float2 f2; // 二维浮点向量
float3 f3; // 三维浮点向量(常用于空间坐标)
float4 f4; // 四维浮点向量(常用于 RGBA 颜色)
int2 i2;   // 二维整数向量
int3 i3;   // 三维整数向量
int4 i4;   // 四维整数向量

// 矩阵类型(常用于变换矩阵)
float4x4 f44; // 4x4 浮点矩阵(常用于模型-视图-投影矩阵)
float2x2 f22; // 2x2 浮点矩阵
float3x3 f33; // 3x3 浮点矩阵
int3x3 i33;   // 3x3 整数矩阵

// 自定义结构体(用于组织相关数据)
struct Test
{
    float3 position; // 位置坐标(12 字节)
    float3 v;        // 速度向量(12 字节)
    float lifeTime;  // 生命周期(4 字节)
    // 结构体总大小:28 字节(需注意内存对齐)
};

// Compute Shader 缓冲区类型示例
StructuredBuffer<Test> buffer;           // 只读结构化缓冲区(存储 Test 结构体数组)
// RWStructuredBuffer<Test> buffer2;     // 可读写结构化缓冲区(注释状态)

ByteAddressBuffer buffer3;               // 字节地址缓冲区(按字节寻址的只读缓冲区)
// RWByteAddressBuffer buffer4;          // 可读写字节地址缓冲区(注释状态)

AppendStructuredBuffer<float> buffer5;   // 追加结构化缓冲区(仅支持追加操作的缓冲区)
// ConsumeStructuredBuffer<float> buffer6; // 消费结构化缓冲区(支持原子取出操作)

// 纹理缓冲区类型
RWTexture2D<float4> Result; // 可读写二维纹理(存储 float4,通常用于输出图像)
// Texture2D<float4> Result2; // 只读二维纹理(注释状态)

// 常量缓冲区(存储每次 Dispatch 调用中保持不变的参数)
// 这些参数在同一个 Dispatch 调用中的所有线程间共享
cbuffer Params
{
    float deltaTime; // 帧时间间隔(用于与时间相关的计算)
    float intensity; // 强度系数(可用于调节效果强度)
    int count;       // 元素数量(可用于控制处理的数据量)
}

// 线程组共享内存(允许同一线程组内的线程间共享数据)
// 声明为 groupshared 的变量仅在同一个线程组内可见
groupshared float localData[64]; // 大小为 64 的浮点数组

// 线程组配置:指定每个线程组包含的线程数量
// [numthreads(X, Y, Z)] 定义一个线程组包含 X*Y*Z 个线程
// 常见配置如 8x8x1=64 个线程,适合处理二维图像数据
[numthreads(8,8,1)]
void CSMain(uint3 id : SV_DispatchThreadID,  // 全局线程 ID(在整个 Dispatch 调用中唯一)
            uint3 id2 : SV_GroupID,          // 线程组 ID(标识当前线程组的位置)
            uint3 id3 : SV_GroupThreadID)     // 线程组内线程 ID(在线程组内唯一)
{
    // 类型转换示例:将 4x4 矩阵转换为 2x2 矩阵(截取左上角 2x2 部分)
    f22 = (float2x2)f44;

    // 核心计算逻辑:根据线程 ID 生成棋盘格图案
    // R 通道:id.x 和 id.y 的按位与结果
    // G 通道:id.x 的低 4 位映射到 [0,1] 范围
    // B 通道:id.y 的低 4 位映射到 [0,1] 范围
    // A 通道:固定为 0.0(完全透明)
    Result[id.xy] = float4(id.x & id.y, (id.x & 15) / 15.0, (id.y & 15) / 15.0, 0.0);
}

// 第二个计算着色器内核函数
// 使用 1x1x1 线程组配置,适合执行单次原子操作或全局控制逻辑
[numthreads(1,1,1)]
void CSMain2(uint3 id : SV_DispatchThreadID) // 仅需全局线程 ID
{
    // 此处可编写第二个内核的核心逻辑
    // 例如:初始化缓冲区、执行全局同步操作等
}


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com

×

喜欢就点赞,疼爱就打赏