Unity Compute Shader入门初探

在写shader的时候,我们通常会创建一个unlit shader或者standard surface shader,但是,至少在我的工作中,从来没有创建过compute shader,这个东西是干嘛用的呢?带着这个疑问,我们今天来一探究竟。

创建shader面板

谈起compute shader,我们要先了解一个概念,叫GPGPU(General-purpose computing on graphics processing units)。根据Wikipedia的介绍

它是利用处理图形任务的图形处理器来计算原本由中央处理器处理的通用计算任务。这些通用计算任务通常与图形处理没有任何关系。

那么,专门为图形任务所设计的图形处理器是如何怎么能处理通用计算任务的呢?这不是在抢CPU的饭碗么?的确,早期的显卡是没有这种功能的。在很久很久以前,那时老黄还没成立NVIDIA,显卡市场还是Voodoo称霸的时候,显卡里有两种单元,一种专门处理顶点,叫做vertex unit,另一种专门处理像素,称作pixel unit。然而,随着渲染场景越来越复杂(想想游戏史,是不是游戏看起来越来越逼真了?),这种模式不利于负载均衡,而且这时候人们也希望显卡能做一些通用计算的需求,图形渲染不再是唯一的需求,所以GPGPU在这时开始浮出水面。到了2006年底及2007年初,老黄拿出了他的GeForce 8800 GTX,AMD也拿出了Radeon HD 2800,unified shaders的时代来临了。什么叫unified shaders?之前专门处理顶点的vertex unit我们需要为它写vertex shader,专门处理像素的pixel unit我们需要为它写pixel shader,到了unified shaders时代,不管vertex shader也好,pixel shader也好,显卡都会用一种unit来处理,这时,GPGPU变成了可能。

现在回到compute shader,它是一种运行在显卡上却不在普通渲染管线上的程序,利用它可以做大型并行的GPGPU算法,以此来获得比CPU快很多倍的计算能力。不过在获得这种能力之前,我们先来看看如何使用这种shader。

首先是C#脚本,用于掌控全局

public Texture inputTex;
public ComputeShader computeShader;
public RawImage image;

void Start(){
        RenderTexture t = new RenderTexture(inputTex.width,inputTex.height,24);
        t.enableRandomWrite = true;
        t.Create();
        image.texture = t;
        image.SetNativeSize();

        int kernel = computeShader.FindKernel("Gray");
        computeShader.SetTexture(kernel,"inputTexture",inputTex);
        computeShader.SetTexture(kernel,"outputTexture",t);
        computeShader.Dispatch(kernel,inputTex.width / 8, inputTex.height / 8,1);
}  

其次是compute shader,所有的计算都放在这里

// Each #kernel tells which function to compile; you can have many kernels
#pragma kernel Gray

Texture2D inputTexture;
// Create a RenderTexture with enableRandomWrite flag and set it
// with cs.SetTexture
RWTexture2D<float4> outputTexture;



[numthreads(8,8,1)]
void Gray (uint3 id : SV_DispatchThreadID)
{
    float r = inputTexture[id.xy].r;
    float g = inputTexture[id.xy].g;
    float b = inputTexture[id.xy].b;

    float res = r * 0.299 + g * 0.587 + b * 0.114;
    outputTexture[id.xy] = float4(res,res,res,1);
}

这里我实现的功能是将一张彩色图转成灰阶图,虽然体现不出GPU的强大并行计算能力,但能起到如何使用compute shader的作用O(∩_∩)O~

首先我们为要输出的灰阶图准备一个地方,名叫t,t的属性enableRandomWrite必须为true,这样才能将数据写入。然后通过FindKernel方法找kernel,里面传入的string就是compute shader中#pragma kernel Gray定义的名字Gray,当然你想起什么名字就起什么名字,可以随便改的,不过相应的void Gray (uint3 id : SV_DispatchThreadID)这个地方的名字要一致。然后用SetTexture方法将数据设置好,由于inputTexture是读取数据用的,所以对应在compute shader里面的变量类型是只读型的Texture2D,而outputTexture的输出数据用的,所以用读写型的RWTexture2D。有朋友可能要问了,RWTexture2D这个怎么没见过啊。原来,unity中的compute shader是遵循DirectX 11语法的,所以这个RWTexture2D是HLSL里的类型,详见微软文档https://docs.microsoft.com/en-us/windows/win32/direct3dhlsl/sm5-object-rwtexture2d

最后到了调用Dispatch方法的时候,也意味着整段程序都设置完毕,要开始工作了。从文档里可以看出需要传四个参数进去,分别是kernelIndex、threadGroupsX、threadGroupsY、threadGroupsZ,这些是什么意思呢?

文档图片1

kernelIndex很好解释,就是刚刚FindKernel方法返回的值。其余的参数,从浅层次上讲,就是要让threadGroupsX * numthreads.x = 图片宽,threadGroupsY * numthreads.y = 图片高,threadGroupsZ大部分时间下都是1。这里的numthreads就是compute shader里的[numthreads(8,8,1)]
另外有个限制条件是在shader model 5的平台下numthreads.x *numthreads.y * numthreads.z <= 1024,numthreads.z <= 64,(在shader model 4.5的平台下这个数字是768,numthreads.z <=1,再往下的shader model则不支持compute shader了)。
还有要注意的是由于架构问题,一个线程组里有几个线程需要结合硬件,NVIDIA的架构下最好是32的倍数个线程,AMD架构下最好为64的倍数个线程。

更进一步,我们来看微软文档里的一张图。

文档图片2

threadGroupsX、threadGroupsY、threadGroupsZ代表着你要开多少组线程,每个线程组里面有多少个线程是由numthreads里的参数决定的。拿我写的这段代码举例,Dispatch的时候开了128 * 128 * 1组的线程组,每组线程组里面有8 * 8 * 1个线程,128 * 8 = 1024,这里我用的图片的长和宽都是1024,即每个线程都在处理图片上的某一个像素。void Gray (uint3 id : SV_DispatchThreadID)这边的id即为每个线程的index。那如果numthreads设为[numthreads(64,4,1)],那么Dispatch的时候可以设为Dispatch(kernel,inputTex.width / 64, inputTex.height / 4,1);

文档图片2中还提到了SV_GroupThreadID,SV_GroupID,SV_GroupIndex,这些也是用来索引线程的,具体关系看彥霖大佬的图就明白了。

Group ID 一看就懂 :

Group Thread ID 一看就懂 :

Group Index 一看就懂 :

接下来我们来看如何用compute shader进行简单计算任务,而不是处理贴图。代码如下

CS脚本

public ComputeShader csBuffer;
ComputeBuffer buffer;
struct MyInt{
        public int val;
        public int index;
    };

void Start()
    {
       
            CSFib();
        
    }

public void CSFib(){
        MyInt[] total = new MyInt[32];
        buffer = new ComputeBuffer(32,8);
        int kernel = csBuffer.FindKernel("Fibonacci");
        csBuffer.SetBuffer(kernel,"buffer",buffer);
        csBuffer.Dispatch(kernel,1,1,1);
        buffer.GetData(total);
        for (int i = 0; i < total.Length; i++)
        {
            Debug.Log(total[i].val);
        }

    }

    private void OnDestroy() {
            buffer.Release();
    }

compute shader

#pragma kernel Fibonacci

struct MyInt{
    int val;
    int index;
};

RWStructuredBuffer<MyInt> buffer;

int Fib(int n){
    int a = 0;
    int b = 1;
    int res = 0;
    for(int i=0;i<n;i++){
        res = a + b;
        a = b;
        b = res;
    }
    return a;
}

[numthreads(32,1,1)]
void Fibonacci (uint3 id : SV_DispatchThreadID)
{
    buffer[id.x].val = Fib(id.x);
    buffer[id.x].index = id.x;
    
}

菲波那切数列我想大家都应该知道吧?那么用GPU来算菲波那切数列就是以上代码在实现的内容了。为了让大家看看在compute shader中如何使用自定义结构体,我舍弃了int类型而使用了自定义结构体MyInt,其中val存菲波那切数列中每一项的值,index存的是值所对应的索引。
这里多出来了一个ComputeBuffer类型的buffer,用于存储计算得到的值,可以看到后面要用
GetData方法把数值从GPU里面拿出来。在new这个ComputeBuffer的时候我们需要传入两个参数,从文档上来看第一个是count,我需要输出32个菲波那切数列,就填32;第二个是stride,代表每个元素的长度,由于自定义结构体MyInt有两个int类型的属性,所以这里的stride为8。其余内容通过上面的讲解,我想应该不难理解了。

文档图片3

当然,compute shader能做的远远不止这些,来看看大佬们把compute shader玩出了什么花样。

GPU粒子系统
GPU布料系统
图片压缩
模型曲面细分
战地3中使用compute shader对点光源、探照灯等进行剔除
知乎大V MaxwellGeng实现的GPU Occlusiong Culling,他使用了Hiz的方法,对Cluster进行遮挡剔除(
刺客信条大革命,在这部游戏中使用了GPUDRP技术,并在Siggraph 2015: Advances in Real-Time Rendering in Games course中发表

还有很多很多应用在这就不一一列举了,以及,以上的我都无力实现,这么菜让大家见笑了(⊙o⊙)…

项目地址

参考
Introduction to compute shaders
【风宇冲】Shader:二十八ComputeShaders
Unity 使用 GPGPU 計算,使用 ComputeShader 將圖片轉成灰階圖
numthreads
Unity 3D : ComputeShader 全面詳解
Compute Shader次世代优化方案

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,402评论 6 499
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,377评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,483评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,165评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,176评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,146评论 1 297
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,032评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,896评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,311评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,536评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,696评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,413评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,008评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,659评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,815评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,698评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,592评论 2 353

推荐阅读更多精彩内容