99.性能优化-GPU-着色器优化-ComputeShader-数据输入规则
99.1 知识点
CPU 侧(C# 侧)的数据输入规则
上节课学习了 Compute Shader 中的各种数据类型,而这些类型的数据都需要从 CPU 侧(C# 中)进行输入,之后才能在 GPU 中利用这些数据进行计算。本节课将着重讲解 C# 侧如何向 Compute Shader 中输入数据。
标量数据输入
通过 ComputeShader 实例的对应 API 设置标量参数:
SetFloat("name", value)→ 对应floatSetInt("name", value)→ 对应int/uintSetBool("name", value)→ 对应bool
computeShader.SetBool("b", true);
computeShader.SetInt("i", 100);
computeShader.SetInt("ui", 999);
computeShader.SetFloat("f", 0.5f);
注意:
- 虽然 Compute Shader 中支持
double和half,但它们仅在少数 GPU 上可用,C# 侧也没有提供专门的 API 进行数据输入,因此建议不要在 Compute Shader 中使用它们。 - 所有 Set 相关方法都需要在
Dispatch之前调用。 - 所有变量名需要和 Compute Shader 内声明一致。
向量数据输入
向量类型可以通过 SetVector 或 SetFloats / SetInts 进行设置:
float2→SetFloats("name", x, y)或SetVector("name", new Vector4(x, y, 0, 0))float3→SetFloats("name", x, y, z)或SetVector("name", new Vector4(x, y, z, 0))float4→SetVector("name", new Vector4(x, y, z, w))int2/int3/int4→SetInts("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#)都要定义完全相同的结构布局,因此必须满足以下三个关键点:
- 结构体字段顺序必须一致
- 字段类型大小必须匹配
- 长度必须是结构体总字节数
C# 侧输入步骤
- 声明对应的数组或
List,存储对应数据 new ComputeBuffer(对象数量, 单个对象所占字节数)创建缓冲区对象- 利用
ComputeBuffer对象的SetDataAPI 将 C# 数据输入 - 利用
ComputeShader的SetBuffer方法绑定缓冲区数据
注意:
ComputeBuffer的步长参数(stride)必须等于 GPU 端结构体实际字节数。- 特殊缓冲类型
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 = true 的 RenderTexture。
// 创建 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