ComputeShader入门精要

前言

随着人工智能技术的迅速普及和发展,对并行计算的需求也日益增长。在游戏开发和图形处理领域,尤其是在 Unity 这样的全球领先的游戏开发平台中,计算着色器(Compute Shader)已成为实现高效并行计算的首选工具。计算着色器不仅能够处理大量数据,还能显著提高渲染效率和性能,是现代图形编程中不可或缺的一环。

计算着色器概述

计算着色器是一种在 GPU 上执行数据并行处理的程序,它不依赖于渲染管线的传统流程。与顶点着色器和片元着色器等主要处理图形的着色器不同,计算着色器被设计用来进行通用计算任务,如物理模拟、AI 计算、图像处理等。

Unity 中的计算着色器可以直接访问缓冲区(Buffer)和纹理(Texture),并通过它们来进行复杂的数据处理。这使得开发者能够利用 GPU 的并行处理能力,执行大规模的数值计算任务。

计算着色器的应用场景

  • 物理模拟:如粒子系统、流体动力学和软体动力学等。
  • AI 计算:在游戏和实时应用中,通过 GPU 加速复杂的 AI 算法,多维数组的运算。
  • 图像处理:应用如图像模糊、锐化、噪声生成和图像效果的实时调整。

入门实例:基础计算着色器

步骤 1:创建计算着色器文件

在 Unity 中,首先需要创建一个 .compute 文件。你可以在项目的 Assets 目录下右键点击,选择 "Create -> Shader -> Compute Shader",并给文件命名。

步骤 2:编写计算着色器代码

打开你创建的 .compute 文件,并输入以下代码:

#pragma kernel CSMain
RWStructuredBuffer<float> Result; // 可读写的浮点数缓冲区
[numthreads(5, 3, 2)]
void CSMain(uint3 id : SV_DispatchThreadID) 
{
    Result[0] = 1.0; // 简单地将第一个元素设置为 1.0
}

这是一个简单的计算着色器示例,其中#pragma kernel CSMain 是一种特殊的编译指令,简单来说通过C#可以通过kernel来调用对应的计算方法,上文中的#pragma kernel CSMain 对应的就是CSMain方法,一个 Compute Shader 文件中可以包含多个 kernel 函数。

RWStructuredBuffer Result; 是一个可读写的浮点数缓冲区,可以理解成一个float[]浮点型数组。

[numthreads(5, 3, 2)],numthreads 定义了一个三维的线程结构;

502d4e6f752044878650bfc2631d4ef1.jpeg

如果我们在程序的Dispatch接口发送了(5,3,2)这样的结构,就会生成5x3x2个线程组,其中每个组的线程结构由ComputeShader中的numthreads定义,图中numthreads定义了10x8x3的三维结构,由此,我们可以分析4个HLSL关键词的定义。

SV_GroupThreadID 表示该线程在该组内的位置;

SV_GroupID 表示整个组所分配的位置;

SV_DispatchThreadID 表示该线程在所有组的线程中的位置;

SV_GroupIndex 表示该线程在该组内的索引;

通过这些关键词,我们可以在并行计算时获取其他线程的输入数据。

步骤 3:在 Unity 脚本中调用计算着色器

创建一个 C# 脚本来调用这个计算着色器,并将其结果输出到控制台。

using UnityEngine;

public class ComputeTest : MonoBehaviour {
    public ComputeShader computeShader;

    void Start() {
        // 找到 Compute Shader 中定义的 Kernel
        int kernelHandle = computeShader.FindKernel("CSMain");

        // 创建一个 ComputeBuffer 来存储结果
        ComputeBuffer resultBuffer = new ComputeBuffer(1, sizeof(float), ComputeBufferType.Default);

        // 将 ComputeBuffer 绑定到 Compute Shader 的对应变量上
        computeShader.SetBuffer(kernelHandle, "Result", resultBuffer);

        // 调用或执行 Compute Shader
        computeShader.Dispatch(kernelHandle, 1, 1, 1);

        // 创建一个数组来接收计算结果
        float[] result = new float[1];

        // 从 GPU 检索计算结果
        resultBuffer.GetData(result);

        // 输出结果到控制台
        Debug.Log("Compute Shader result: " + result[0]);

        // 释放 ComputeBuffer 资源
        resultBuffer.Release();
    }
}

1.Compute Shader 定位:使用 FindKernel 方法根据 Kernel 的名称获取其索引。

2.创建 ComputeBuffer:computebuffer是用于CPU和GPU传递数据的通道,定义一个新的 ComputeBuffer,大小为 1,存储类型为 float,用于从 Compute Shader 接收计算结果。

3.绑定 ComputeBuffer:通过 SetBuffer 方法将创建的 ComputeBuffer 绑定到 Compute Shader 中定义的 Result 变量。

4.执行 Compute Shader:通过 Dispatch 方法执行 Compute Shader。参数 (1, 1, 1) 指定执行一组线程,其中每组包含一个线程(总共一个线程),因为我们的任务是非常简单的。

5.检索结果:使用 GetData 方法从 ComputeBuffer 中读取计算结果。

6.资源释放:完成数据检索后,通过调用 Release 方法释放 ComputeBuffer 的资源,这是非常重要的步骤,以避免内存泄露。

通过这种方式,你可以在 Unity 中利用 GPU 的计算能力来执行并行计算任务,并将结果回传到 CPU 进行进一步处理或显示。这段代码提供了一个基本的框架,你可以根据具体需求调整计算逻辑或数据处理方式。

 

数据结构(常用)

1.纹理(Textures)

在 Compute Shader 中,纹理不仅仅用于存储图像,它们也可以用来存储任何需要的数据。纹理可以是一维的、二维的、三维的,甚至是立方图。使用纹理的好处在于它们是经过优化的,可以快速读取和写入,并且支持各种硬件加速的过滤操作。

computeshader:
Texture2D<float4> inputTexture; //四通道纹理(rgba)
Texture2D<float2> inputTexture;//二通道纹理

C#:
shader.SetTexture(kernel, "inputTexture", texture);

2.缓冲区(Buffers)

Compute Buffers 用于存储大量数据,从简单类型到复杂的自定义结构体。

computeshader:
RWStructuredBuffer<float> outputBuffer;  // 可读写的结构化缓冲区 
StructuredBuffer<float> outputBuffer; //不可读的结构化缓冲区 

C#
 buffer = new ComputeBuffer(1, sizeof(float));   
 float[] values = newfloat[1] {0f};   
 buffer.SetData(values);
 
 shader.SetBuffer(kernel, "outputBuffer", buffer);
 
//RWStructuredBuffer可以通过buffer.GetData获取数据
 buffer.GetData(values);

3. 普通数据类型

computeshader
float float_1 = 1.0;
float2 float_2 = float2(1.0, 1.0);
float3 float_3 = float3(1.0, 1.0, 1.0);float4 float_4 = float4(1.0, 1.0, 1.0, 1.0);

int int_1 = 1;
int2 int_2 = int2(1, 1);
int3 int_3 = int3(1, 1, 1);
int4 int_4 = int4(1, 1, 1, 1);

C#
int kernelHandle = shader.FindKernel("CSMain");    
// 设置单个 float 和 int    
shader.SetFloat("float_1", 1.0f);   
shader.SetInt("int_1", 1);   
 // 设置 floats 类型的数据    
shader.SetFloats("float_2", newfloat[] {1.0f, 1.0f}); 
shader.SetFloats("float_3", newfloat[] {1.0f, 1.0f, 1.0f}); 
shader.SetFloats("float_4", newfloat[] {1.0f, 1.0f, 1.0f, 1.0f}); 
    
// 设置整型向量,使用 SetVector 转换成 Vector4 后传递,但这里需要将整型转换为float    
shader.SetInts("int_2", newint[] {1, 1});
shader.SetInts("int_3", newint[] {1, 1, 1});
shader.SetInts("int_4", newint[] {1, 1, 1, 1});

ComputeBuffer

创建ComputeBuffer:

在Unity中创建ComputeBuffer时,需要指定其存储的数据类型的大小和数量。

ComputeBuffer buffer = new ComputeBuffer(count, sizeof(int),ComputeBufferType.Default);

绑定ComputeBuffer到Compute Shader:

将ComputeBuffer传递给Compute Shader之前,需要通过Compute Shader的SetBuffer方法将其绑定。

computeShader.SetBuffer(kernelIndex, "bufferName", buffer);

在Compute Shader中使用ComputeBuffer:

在Compute Shader代码中,可以通过声明的buffer变量来读写数据。

RWStructuredBuffer<int> dataBuffer; // HLSL代码

从ComputeBuffer检索数据:

计算完成后,可以将数据从GPU读回到CPU。

int[] results = newint[count];buffer.GetData(results);

清理:

使用完ComputeBuffer后,应该释放其资源,以避免内存泄漏。

buffer.Release();

ComputeBuffer的类型(ComputeBufferType)

Unity中的ComputeBuffer有几种类型,每种类型都有其特定的用途:

  • Default:最普通的缓冲区类型,用于存储通用数据。
  • Structured:用于存储一系列结构化数据。这是最常用的类型,因为它允许你以结构化的方式存储复杂的数据,如顶点或计算数据。
  • Append:这种类型的缓冲区用于动态添加数据,常用于不确定输出数据数量的情况。
  • Counter:与Append类似,但这种缓冲区包含一个计数器,用于追踪缓冲区中元素的数量。
  • Raw:允许以字节形式存储和访问数据,适用于那些需要非结构化存储数据的高级用途。

 

  • 34
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值