前言

Compute Shader是现在比拟风行的一种技术,例如之前的《天刀手游》,还有最近大火的《永劫无间》,在分享技术的时候都有提到它。

Unity官网对Compute Shader的介绍如下:https://docs.unity3d.com/Manu...

Compute Shader和其余Shader一样是运行在GPU上的,然而它是独立于渲染管线之外的。咱们能够利用它实现大量且并行的GPGPU算法,用来减速咱们的游戏。

在Unity中,咱们在Project中右键,即可创立出一个Compute Shader文件:

生成的文件属于一种Asset文件,并且都是以 .compute作为文件后缀的。

咱们来看下外面的默认内容:

#pragma kernel CSMainRWTexture2D<float4> Result;[numthreads(8,8,1)]void CSMain (uint3 id : SV_DispatchThreadID){    Result[id.xy] = float4(id.x & id.y, (id.x & 15)/15.0, (id.y & 15)/15.0, 0.0);}

本文的次要目标就是让和我一样的萌新可能看懂这区区几行代码的含意,学好了根底才可能看更牛的代码。

语言

Unity应用的是DirectX 11的HLSL语言,会被主动编译到所对应的平台。

kernel

而后咱们来看看第一行:

#pragma kernel CSMain

CSMain其实就是一个函数,在代码前面能够看到,而kernel是内核的意思,这一行即把一个名为CSMain的函数申明为内核,或者称之为核函数。这个核函数就是最终会在GPU中被执行。

一个Compute Shader中至多要有一个kernel才可能被唤起。申明办法即为:
#pragma kernel functionName

咱们也可用它在一个Compute Shader里申明多个内核,此外咱们还能够再该指令前面定义一些预处理的宏命令,如下:
#pragma kernel KernelOne SOME_DEFINE DEFINE_WITH_VALUE=1337
#pragma kernel KernelTwo OTHER_DEFINE

咱们不能把正文写在该命令前面,而应该换行写正文,例如上面写法会造成编译的报错:
#pragma kernel functionName // 一些正文

RWTexture2D

接着咱们再来看看第二行:

RWTexture2D<float4> Result;

看着像是申明了一个和纹理无关的变量,具体来看一下这些关键字的含意。

RWTexture2D中,RW其实是ReadWrite的意思,Texture2D就是二维纹理,因而它的意思就是一个能够被Compute Shader读写的二维纹理

如果咱们只想读不想写,那么能够应用Texture2D的类型。

咱们晓得纹理是由一个个像素组成的,每个像素都有它的下标,因而咱们就能够通过像素的下标来拜访它们,例如:Result[uint2(0,0)]。

同样的每个像素会有它的一个对应值,也就是咱们要读取或者要写入的值。这个值的类型就被写在了<>当中,通常对应的是一个RGBA的值,因而是float4类型。通常状况下,咱们会在Compute Shader中解决好纹理,而后在FragmentShader中来对解决后的纹理进行采样。

这样咱们就大抵了解这行代码的意思了,申明了一个名为Result的可读写二维纹理,其中每个像素的值为float4。

在Compute Shader中可读写的类型除了RWTexture以外还有RWBufferRWStructuredBuffer,前面会介绍。

RWTexture2D - Win32 apps

numthreads

而后是上面一句(很重要!):

[numthreads(8,8,1)]

又是num,又是thread的,必定和线程数量无关。没错,它就是定义一个线程组(Thread Group)中能够被执行的线程(Thread)总数量,格局如下:
numthreads(tX, tY, tZ)
注:X,Y,Z前加个t不便和后续Group的X,Y,Z进行辨别

其中tXtYtZ的值即线程的总数量,例如numthreads(4, 4, 1)和numthreads(16, 1, 1)都代表着有16个线程。那么为什么不间接应用numthreads(num)这种模式定义,而非要分成tX、tY、tZ这种三维的模式呢?看到前面天然就懂其中的神秘了。

每个核函数后面咱们都须要定义numthreads,否则编译会报错。

其中tX,tY,tZ三个值也并不是能够轻易乱填的,比方来一刀tX=99999暴击一下,这是不行的。它们在不同的版本里有如下的束缚:

在Direct11中,能够通过ID3D11DeviceContext::Dispatch(gX,gY,gZ)办法创立gXgYgZ个线程组,一个线程组里又会蕴含多个线程(数量即numthreads定义)。

留神程序,先numthreads定义好每个核函数对应线程组里线程的数量(tXtYtZ),再用Dispatch定义用多少线程组(gXgYgZ)来解决这个核函数。其中每个线程组内的线程都是并行的,不同线程组的线程可能同时执行,也可能不同时执行。个别一个GPU同时执行的线程数,在1000-10000之间。

接着咱们用一张示意图来看看线程与线程组的构造,如下图:

上半局部代表的是线程组构造,下半局部代表的是单个线程组里的线程构造。因为他们都是由(X,Y,Z)来定义数量的,因而就像一个三维数组,下标都是从0开始。咱们能够把它们看做是表格一样:有Z个一样的表格,每个表格有X列和Y行。例如线程组中的(2,1,0),就是第1个表格的第2行第3列对应的线程组,下半局部的线程也是同理。

搞清楚构造,咱们就能够很好的了解上面这些与单个线程无关的参数含意:

这里须要留神的是,不论是Group还是Thread,它们的程序都是先X再Y最初Z,用表格的了解就是后行(X)再列(Y)而后下一个表(Z),例如咱们tX=5,tY=6那么第1个thread的SV_GroupThreadID=(0,0,0),第2个的SV_GroupThreadID=(1,0,0),第6个的SV_GroupThreadID=(0,1,0),第30个的SV_GroupThreadID=(4,5,0),第31个的SV_GroupThreadID=(0,0,1)。Group同理,搞清程序后,SV_GroupIndex的计算公式就很好了解了。

再举个例子,比方SV_GroupID为(0,0,0)和(1,0,0)的两个Group,它们外部的第1个Thread的SV_GroupThreadID都为(0,0,0)且SV_GroupIndex都为0,然而前者的SV_DispatchThreadID=(0,0,0)而后者的SV_DispatchThreadID=(tX,0,0)。

好好了解下,它们在核函数里十分的重要。
numthreads - Win32 apps

核函数

void CSMain (uint3 id : SV_DispatchThreadID){    Result[id.xy] = float4(id.x & id.y, (id.x & 15)/15.0, (id.y & 15)/15.0, 0.0);}

最初就是咱们申明的核函数了,其中参数SV_DispatchThreadID的含意下面曾经介绍过了,除了这个参数以外,咱们后面提到的几个参数都能够被传入到核函数当中,依据理论需要做取舍即可,残缺如下:

void KernelFunction(uint3 groupId : SV_GroupID,    uint3 groupThreadId : SV_GroupThreadID,    uint3 dispatchThreadId : SV_DispatchThreadID,    uint groupIndex : SV_GroupIndex){}

而函数内执行的代码就是为咱们Texture中下标为id.xy的像素赋值一个色彩,这里也就是最牛的中央。

举个例子,以往咱们想要给一个x*y分辨率的Texture每个像素进行赋值,单线程的状况下,咱们的代码往往如下:

for (int i = 0; i < x; i++)    for (int j = 0; j < y; j++)        Result[uint2(x, y)] = float4(a, b, c, d);

两个循环,像素一个个的缓缓赋值。那么如果咱们要每帧给很多张2048*2048的图片进行操作,可想而知会卡死。

如果应用多线程,为了防止不同的线程对同一个像素进行操作,咱们往往应用分段操作的办法,如下,四个线程进行解决:

void Thread1(){    for (int i = 0; i < x/4; i++)        for (int j = 0; j < y/4; j++)            Result[uint2(x, y)] = float4(a, b, c, d);}void Thread2(){    for (int i = x/4; i < x/2; i++)        for (int j = y/4; j < y/2; j++)            Result[uint2(x, y)] = float4(a, b, c, d);}void Thread3(){    for (int i = x/2; i < x/4*3; i++)        for (int j = x/2; j < y/4*3; j++)            Result[uint2(x, y)] = float4(a, b, c, d);}void Thread4(){    for (int i = x/4*3; i < x; i++)        for (int j = y/4*3; j < y; j++)            Result[uint2(x, y)] = float4(a, b, c, d);}

这么写不是很蠢么,如果有更多的线程,分成更多段,不就一堆反复的代码。然而如果咱们能晓得每个线程的开始和完结下标,不就能够把这些代码对立起来了么,如下:

void Thread(int start, int end){    for (int i = start; i < end; i++)        for (int j = start; j < end; j++)            Result[uint2(x, y)] = float4(a, b, c, d);}

那我要是能够开出很多很多的线程是不是就能够一个线程解决一个像素了?

void Thread(int x, int y){    Result[uint2(x, y)] = float4(a, b, c, d);}

用CPU咱们做不到这样,然而用GPU,用Compute Shader咱们就能够。实际上,后面默认的Compute Shader的代码里,核函数的内容就是这样的。

接下来咱们来看看Compute Shader的妙处,看id.xy的值。id的类型为SV_DispatchThreadID,咱们先来回顾下SV_DispatchThreadID的计算公式:
假如该线程的SV_GroupID=(a, b, c),SV_GroupThreadID=(i, j, k) 那么SV_DispatchThreadID=(atX+i, btY+j, c*tZ+k)

首先后面咱们应用了[numthreads(8,8,1)],即tX=8,tY=8,tZ=1,且i和j的取值范畴为0到7,而k=0。那么咱们线程组(0,0,0)中所有线程的SV_DispatchThreadID.xy也就是id.xy的取值范畴即为(0,0)到(7,7),线程组(1,0,0)中它的取值范畴为(8,0)到(15, 7),...,线程(0,1,0)中它的取值范畴为(0,8)到(7,15),...,线程组(a,b,0)中它的取值范畴为(a8, b8, 0)到(a8+7,b8+7,0)。

咱们用示意图来看下,假如下图每个网格里蕴含了64个像素:

也就是说咱们每个线程组会有64个线程同步解决64个像素,并且不同的线程组里的线程不会反复解决同一个像素,若要解决分辨率为1024*1024的图,咱们只须要dispatch(1024/8, 1024/8, 1)个线程组。

这样就实现了成千盈百个线程同时解决一个像素了,若用CPU的形式这是不可能的。是不是很妙?

而且咱们能够发现numthreads中设置的值是很值得斟酌的,例如咱们有4*4的矩阵要解决,设置numthreads(4,4,1),那么每个线程的SV_GroupThreadID.xy的值不正好能够和矩阵中每项的下标对应上么。

咱们在Unity中怎么调用核函数,又怎么dispatch线程组以及应用的RWTexture又怎么来呢?这里就要回到咱们C#的局部了。

C#局部

以往的vertex&fragment shader,咱们都是给它关联到Material上来应用的,然而Compute Shader不一样,它是由C#来驱动的。

先新建一个monobehaviour脚本,Unity为咱们提供了一个Compute Shader的类型用来援用咱们后面生成的 .compute 文件:

public ComputeShader computeShader;


在Inspector界面关联.compute文件

此外咱们再关联一个Material,因为Compute Shader解决后的纹理,仍旧要通过Fragment Shader采样起初显示。
public Material material;

这个Material咱们应用一个Unlit Shader,并且纹理不必设置,如下:

而后关联到咱们的脚本上,并且轻易建个Cube也关联上这Material。

接着咱们能够将Unity中的RenderTexture赋值到Compute Shader中的RWTexture2D上,然而须要留神因为咱们是多线程解决像素,并且这个处理过程是无序的,因而咱们要将RenderTexture的enableRandomWrite属性设置为true,代码如下:

RenderTexture mRenderTexture = new RenderTexture(256, 256, 16);mRenderTexture.enableRandomWrite = true;mRenderTexture.Create();

咱们创立了一个分辨率为256*256的RenderTexture,首先咱们要把它赋值给咱们的Material,这样咱们的Cube就会显示出它。而后要把它赋值给咱们Compute Shader中的Result变量,代码如下:

material.mainTexture = mRenderTexture;computeShader.SetTexture(kernelIndex, "Result", mRenderTexture);

这里有一个kernelIndex变量,即核函数下标,咱们能够利用FindKernel来找到咱们申明的核函数的下标:

int kernelIndex = computeShader.FindKernel("CSMain");

这样在咱们FragmentShader采样的时候,采样的就是Compute Shader解决过后的纹理:

fixed4 frag (v2f i) : SV_Target{    // _MainTex 就是被解决后的 RenderTexture    fixed4 col = tex2D(_MainTex, i.uv);    return col;}

最初就是开线程组和调用咱们的核函数了,在Compute Shader中,Dispatch办法为咱们一步到位:

computeShader.Dispatch(kernelIndex, 256 / 8, 256 / 8, 1);

为什么是256/8,后面曾经解释过了。来看看成果:

上图就是咱们Unity默认生成的Compute Shader代码所能带来的成果,咱们也可试下用它解决2048*2048的Texture,也是十分快的。


接下来咱们再来看看粒子成果的例子:

首先一个粒子通常领有色彩和地位两个属性,并且咱们必定是要在Compute Shader里去解决这两个属性的,那么咱们就能够在Compute Shader创立一个struct来存储:

struct ParticleData {float3 pos;float4 color;};

接着,这个粒子必定是很多很多的,咱们就须要一个像List一样的货色来存储它们,在ComputeShader中为咱们提供了RWStructuredBuffer类型。

RWStructuredBuffer

它是一个可读写的Buffer,并且咱们能够指定Buffer中的数据类型为咱们自定义的struct类型,不必再局限于int、float这类的根本类型。

因而咱们能够这么定义咱们的粒子数据:

RWStructuredBuffer<ParticleData> ParticleBuffer;

RWStructuredBuffer - Win32 apps

为了有动效,咱们能够再增加一个工夫相干值,咱们能够依据工夫来批改粒子的地位和色彩:

float Time;

接着就是怎么在核函数里批改咱们的粒子信息了,要批改某个粒子,咱们必定要晓得粒子在Buffer中的下标,并且这个下标在不同的线程中不能反复,否则就可能导致多个线程批改同一个粒子了。

依据后面的介绍,咱们晓得一个线程组中SV_GroupIndex是惟一的,然而在不同线程组中并不是。例如每个线程组内有1000个线程,那么SV_GroupID都是0到999。咱们能够依据SV_GroupID把它叠加下来,例如SV_GroupID=(0,0,0)是0-999,SV_GroupID=(1,0,0)是1000-1999等等,为了不便咱们的线程组都能够是(X,1,1)格局。而后咱们就能够依据Time和Index轻易的摆布下粒子,Compute Shader残缺代码:

#pragma kernel UpdateParticlestruct ParticleData {float3 pos;float4 color;};RWStructuredBuffer<ParticleData> ParticleBuffer;float Time;[numthreads(10, 10, 10)]void UpdateParticle(uint3 gid : SV_GroupID, uint index : SV_GroupIndex){int pindex = gid.x * 1000 + index;float x = sin(index);float y = sin(index * 1.2f);float3 forward = float3(x, y, -sqrt(1 - x * x - y * y));ParticleBuffer[pindex].color = float4(forward.x, forward.y, cos(index) * 0.5f + 0.5, 1);if (Time > gid.x)ParticleBuffer[pindex].pos += forward * 0.005f;}

接下来咱们要在C#里给粒子初始化并且传递给Compute Shader。咱们要传递粒子数据,也就是说要给后面的RWStructuredBuffer<ParticleData>赋值,Unity为咱们提供了ComputeBuffer类来与RWStructuredBuffer或StructuredBuffer绝对应。

ComputeBuffer

在Compute Shader中常常须要将咱们一些自定义的Struct数据读写到内存缓冲区,ComputeBuffer就是为这种状况而生的。咱们能够在C#里创立并填充它,而后传递到Compute Shader或者其余Shader中应用。

通常咱们用上面办法来创立它:

ComputeBuffer buffer = new ComputeBuffer(int count, int stride)

其中count代表咱们buffer中元素的数量,而stride指的是每个元素占用的空间(字节),例如咱们传递10个float的类型,那么count=10,stride=4。须要留神的是ComputeBuffer中的stride大小必须和RWStructuredBuffer中每个元素的大小统一

申明实现后咱们能够应用SetData办法来填充,参数为自定义的struct数组:

buffer.SetData(T[]);

最初咱们能够应用Compute Shader类中的SetBuffer办法来把它传递到Compute Shader中:

public void SetBuffer(int kernelIndex, string name, ComputeBuffer buffer)

记得用完后把它Release()掉。
https://docs.unity3d.com/Scri...

在C#中咱们定义一个一样的struct,这样能力保障和Compute Shader中的大小统一:

public struct ParticleData{    public Vector3 pos;//等价于float3    public Color color;//等价于float4}

而后咱们在Start办法中申明咱们的ComputeBuffer,并且找到咱们的核函数:

void Start(){    //struct中一共7个float,size=28    mParticleDataBuffer = new ComputeBuffer(mParticleCount, 28);    ParticleData[] particleDatas = new ParticleData[mParticleCount];    mParticleDataBuffer.SetData(particleDatas);    kernelId = computeShader.FindKernel("UpdateParticle");}

因为咱们想要咱们的粒子是静止的,即每帧要批改粒子的信息。因而咱们在Update办法里去传递Buffer和Dispatch:

void Update(){    computeShader.SetBuffer(kernelId, "ParticleBuffer", mParticleDataBuffer);    computeShader.SetFloat("Time", Time.time);    computeShader.Dispatch(kernelId,mParticleCount/1000,1,1);}

到这里咱们的粒子地位和色彩的操作都曾经实现了,然而这些数据并不能在Unity里显示出粒子,咱们还须要Vertex&FragmentShader的帮忙,咱们新建一个UnlitShader,批改下外面的代码如下:

Shader "Unlit/ParticleShader"{    SubShader    {        Tags { "RenderType"="Opaque" }        LOD 100        Pass        {            CGPROGRAM            #pragma vertex vert            #pragma fragment frag            #include "UnityCG.cginc"            struct v2f            {                float4 col : COLOR0;                float4 vertex : SV_POSITION;            };            struct particleData            {float3 pos;float4 color;            };            StructuredBuffer<particleData> _particleDataBuffer;            v2f vert (uint id : SV_VertexID)            {                v2f o;                o.vertex = UnityObjectToClipPos(float4(_particleDataBuffer[id].pos, 0));                o.col = _particleDataBuffer[id].color;                return o;            }            fixed4 frag (v2f i) : SV_Target            {                return i.col;            }            ENDCG        }    }}

后面咱们说了ComputeBuffer也能够传递到一般的Shader中,因而咱们在Shader中也创立一个构造一样的Struct,而后利用StructuredBuffer<T>来接管。

SV_VertexID:在VertexShader中用它来作为传递进来的参数,代表顶点的下标。咱们有多少个粒子即有多少个顶点。顶点数据应用咱们在Compute Shader中解决过的Buffer。

最初咱们在C#中关联一个带有下面Shader的Material,而后将粒子数据传递过来,最终绘制进去。残缺代码如下:

public class ParticleEffect : MonoBehaviour{    public ComputeShader computeShader;    public Material material;    ComputeBuffer mParticleDataBuffer;    const int mParticleCount = 20000;    int kernelId;    struct ParticleData    {        public Vector3 pos;        public Color color;    }    void Start()    {        //struct中一共7个float,size=28        mParticleDataBuffer = new ComputeBuffer(mParticleCount, 28);        ParticleData[] particleDatas = new ParticleData[mParticleCount];        mParticleDataBuffer.SetData(particleDatas);        kernelId = computeShader.FindKernel("UpdateParticle");    }    void Update()    {        computeShader.SetBuffer(kernelId, "ParticleBuffer", mParticleDataBuffer);        computeShader.SetFloat("Time", Time.time);        computeShader.Dispatch(kernelId,mParticleCount/1000,1,1);        material.SetBuffer("_particleDataBuffer", mParticleDataBuffer);    }    void OnRenderObject()    {        material.SetPass(0);        Graphics.DrawProceduralNow(MeshTopology.Points, mParticleCount);    }    void OnDestroy()    {        mParticleDataBuffer.Release();        mParticleDataBuffer = null;    }}

material.SetBuffer:传递ComputeBuffer到咱们的Shader当中。

OnRenderObject:该办法里咱们能够自定义绘制几何。

DrawProceduralNow:咱们能够用该办法绘制几何,第一个参数是拓扑构造,第二个参数数顶点数。

https://docs.unity3d.com/Scri...

最终失去的成果如下:

Demo链接如下:
https://github.com/luckyWjr/C...

ComputeBufferType

在例子中,咱们new一个ComputeBuffer的时候并没有应用到ComputeBufferType的参数,默认应用了ComputeBufferType.Default。实际上咱们的ComputeBuffer能够有多种不同的类型对应HLSL中不同的Buffer,来在不同的场景下应用,一共有如下几种类型:

举个例子,在做GPU剔除的时候常常会应用到Append的Buffer(例如前面介绍的用Compute Shader实现视椎剔除),C#中的申明如下:

var buffer = new ComputeBuffer(count, sizeof(float), ComputeBufferType.Append);

注:Default,Append,Counter,Structured对应的Buffer每个元素的大小,也就是stride的值应该是4的倍数且小于2048。

上述ComputeBuffer能够对应Compute Shader中的AppendStructuredBuffer,而后咱们能够在Compute Shader里应用Append办法为Buffer增加元素,例如:

AppendStructuredBuffer<float> result;[numthreads(640, 1, 1)]void ViewPortCulling(uint3 id : SV_DispatchThreadID){    if(满足一些自定义条件)        result.Append(value);}

那么咱们的Buffer中到底有多少个元素呢?计数器能够帮忙咱们失去这个后果。

在C#中,咱们能够先应用ComputeBuffer.SetCounterValue办法来初始化计数器的值,例如:

buffer.SetCounterValue(0);//计数器值为0

随着AppendStructuredBuffer.Append办法,咱们计数器的值会主动的++。当Compute Shader解决实现后,咱们能够应用ComputeBuffer.CopyCount办法来获取计数器的值,如下:

public static void CopyCount(ComputeBuffer src, ComputeBuffer dst, int dstOffsetBytes);

Append、Consume或者Counter的Buffer会保护一个计数器来存储Buffer中的元素数量,该办法能够把src中的计数器的值拷贝到dst中,dstOffsetBytes为在dst中的偏移。在DX11平台dst的类型必须为Raw或者IndirectArguments,而在其余平台能够是任意类型。

因而获取buffer中元素数量的代码如下:

uint[] countBufferData = new uint[1] { 0 };var countBuffer = new ComputeBuffer(1, sizeof(uint), ComputeBufferType.IndirectArguments);ComputeBuffer.CopyCount(buffer, countBuffer, 0);countBuffer.GetData(countBufferData);//buffer中的元素数量即为:countBufferData[0]

从下面两个最根底的例子中,咱们能够看出,Compute Shader中的数据都是由C#传递过去的,也就是说数据要从CPU传递到GPU。并且在Compute Shader解决完结后又要从GPU传回CPU。这样可能会有点提早,而且它们之间的传输速率也是一个瓶颈。

然而如果咱们有大量的计算需要,不要犹豫,请应用Compute Shader,对性能能有很大的晋升。

UAV(Unordered Access view)

Unordered是无序的意思,Access即拜访,view代表的是“data in the required format”,应该能够了解为数据所须要的格局吧。

什么意思呢?咱们的Compute Shader是多线程并行的,因而咱们的数据必然须要可能反对被无序的拜访。例如,如果纹理只能被(0,0),(1,0),(2,0),...,Buffer只能被[0],[1],[2],...这样有序拜访,那么想要用多线程来批改它们显著不行,因而提出了一个概念,即UAV,可无序拜访的数据格式

后面咱们提到了RWTexture,RWStructuredBuffer这些类型都属于UAV的数据类型,并且它们反对在读取的同时写入。它们只能在FragmentShader和ComputeShader中被应用(绑定)。

如果咱们的RenderTexture不设置enableRandomWrite,或者咱们传递一个Texture给RWTexture,那么运行时就会报错:
the texture wasn't created with the UAV usage flag set!

不能被读写的数据类型,例如Texure2D,咱们称之为SRV(Shader Resource View)

Direct3D 12 术语表 - Win32 apps

Wrap / WaveFront

后面咱们说了应用numthreads能够定义每个线程组内线程的数量,那么咱们应用numthreads(1,1,1)真的每个线程组只有一个线程嘛?NO!

这个问题要从硬件说起,咱们GPU的模式是SIMT(single-instruction multiple-thread,单指令多线程)。在NVIDIA的显卡中,一个SM(Streaming Multiprocessor)可调度多个wrap,而每个wrap里会有32个线程。咱们能够简略的了解为一个指令起码也会调度32个并行的线程。而在AMD的显卡中这个数量为64,称之为Wavefront。

也就是说如果是NVIDIA的显卡,如果咱们应用numthreads(1,1,1),那么线程组依旧会有32个线程,然而多进去的31个线程齐全就处于没有应用的状态,造成节约。因而咱们在应用numthreads时,最好将线程组的数量定义为64的倍数,这样两种显卡都能够顾及到。

https://www.cvg.ethz.ch/teach...

挪动端反对问题

咱们能够运行时调用SystemInfo.supportsComputeShaders来判断以后的机型是否反对Compute Shader。其中OpenGL ES从3.1版本才开始反对Compute Shader,而应用Vulkan的Android平台以及应用Metal的IOS平台都反对Compute Shader。

然而有些Android手机即便反对Compute Shader,然而对RWStructuredBuffer的反对并不敌对。例如在某些OpenGL ES 3.1的手机上,只反对Fragment Shader内拜访StructuredBuffer。

在一般的Shader中要反对Compute Shader,Shader Model最低要求为4.5,即:

#pragma target 4.5

利用Compute Shader实现视椎剔除
《Unity中应用Compute Shader做视锥剔除(View Frustum Culling)》

利用Compute Shader实现Hi-z遮挡剔除
《Unity中应用Compute Shader实现Hi-z遮挡剔除(Occlusion Culling)》

Shader.PropertyToID

在Compute Shader中定义的变量仍旧能够通过 Shader.PropertyToID("name") 的形式来取得惟一id。这样当咱们要频繁利用ComputeShader.SetBuffer对一些雷同变量进行赋值的时候,就能够把这些id当时缓存起来,防止造成GC。

int grassMatrixBufferId;void Start() {    grassMatrixBufferId = Shader.PropertyToID("grassMatrixBuffer");}void Update() {    compute.SetBuffer(kernel, grassMatrixBufferId, grassMatrixBuffer);    // dont use it    //compute.SetBuffer(kernel, "grassMatrixBuffer", grassMatrixBuffer);}

全局变量或常量?

如果咱们要实现一个需要,在Compute Shader中判断某个顶点是否在一个固定大小的突围盒内,那么依照以往C#的写法,咱们可能如下定义突围盒大小:

#pragma kernel CSMainfloat3 boxSize1 = float3(1.0f, 1.0f, 1.0f); // 办法1const float3 boxSize2 = float3(2.0f, 2.0f, 2.0f); // 办法2static float3 boxSize3 = float3(3.0f, 3.0f, 3.0f); // 办法3[numthreads(8,8,1)]void CSMain (uint3 id : SV_DispatchThreadID){    // 做判断}

通过测试,其中办法1和办法2的定义,在CSMain里读取到的值都为 float3(0.0f,0.0f,0.0f) ,只有办法3才是最开始定义的值。

Shader variants and keywords

ComputeShader同样反对Shader变体,用法和一般的Shader变体根本类似,示例如下:

#pragma kernel CSMain#pragma multi_compile __ COLOR_WHITE COLOR_BLACKRWTexture2D<float4> Result;[numthreads(8,8,1)]void CSMain (uint3 id : SV_DispatchThreadID){#if defined(COLOR_WHITE)Result[id.xy] = float4(1.0, 1.0, 1.0, 1.0);#elif defined(COLOR_BLACK)Result[id.xy] = float4(0.0, 0.0, 0.0, 1.0);#elseResult[id.xy] = float4(id.x & id.y, (id.x & 15) / 15.0, (id.y & 15) / 15.0, 0.0);#endif}

而后咱们就能够在C#端启用或禁用某个变体了:
#pragma multi_compile 申明的全局变体能够应用Shader.EnableKeyword/Shader.DisableKeyword或者ComputeShader.EnableKeyword/ComputeShader.DisableKeyword

#pragma multi_compile_local 申明的部分变体能够应用ComputeShader.EnableKeyword/ComputeShader.DisableKeyword

示例如下:

public class DrawParticle : MonoBehaviour{    public ComputeShader computeShader;    void Start() {        ......        computeShader.EnableKeyword("COLOR_WHITE");    }}

这是侑虎科技第1027篇文章,感激作者王江荣供稿。欢送转发分享,未经作者受权请勿转载。如果您有任何独到的见解或者发现也欢送分割咱们,一起探讨。(QQ群:793972859)

作者主页:https://www.zhihu.com/people/...,再次感激王江荣的分享,如果您有任何独到的见解或者发现也欢送分割咱们,一起探讨。(QQ群:793972859)