Unity中ComputeShader入门
ComputeShaderComputeShader是如今比较流行的一种技术,例如之前的《天刀手游》,还有最近大火的《永劫无间》,在分享技术的时候都有提到它。本着不学习就可能失业的压力,就来学一下,虽然好像已经晚了好几年了=。=。Unity官方对ComputeShader的介绍如下:https://docs.unity3d.com/Manual/class-ComputeShader.htmlCo
·
ComputeShader
ComputeShader是如今比较流行的一种技术,例如之前的《天刀手游》,还有最近大火的《永劫无间》,在分享技术的时候都有提到它。
本着不学习就可能失业的压力,就来学一下,虽然好像已经晚了好几年了=。=。
Unity官方对ComputeShader的介绍如下:
ComputeShader和其他Shader一样是运行在GPU上的,但是它是
独立于渲染管线之外
的。我们可以利用它实现
大量且并行
的GPGPU算法,用来加速我们的游戏。
例如我们可以利用ComputeShader实现GPU Culling,效果如下:
https://zhuanlan.zhihu.com/p/396979267

在Unity中,我们在Project中右键,即可创建出一个ComputeShader文件:

生成的文件属于一种 Asset 文件,并且都是以
.compute 作为文件后缀
的。
我们来看下里面的默认内容:
#
pragma
kernel CSMain
RWTexture2D
<
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
)
;
}
本文的主要目的就是让和我一样的
萌新
能够看懂这区区几行代码的含义,学好了基础才能够看更牛逼的代码嘛。如果看完还看不懂,那就是我写的不好了!
kernel
然后我们来看看第一行:
#
pragma
kernel CSMain
CSMain其实就是一个函数,在代码后面可以看到,而 kernel 是内核的意思,这一行即把一个名为CSMain的函数声明为内核,或者称之为核函数。这个核函数就是最终会在GPU中被执行。
一个ComputeShader中
至少要有一个kernel才能够被唤起
。声明方法即为:
#pragma kernel functionName
我们也可用它在一个ComputeShader里声明多个内核,此外我们还可以再该指令后面定义一些预处理的宏命令,如下:
#pragma kernel KernelOne SOME_DEFINE DEFINE_WITH_VALUE=1337 #pragma kernel KernelTwo OTHER_DEFINE
我们不能把注释写在该命令后面,而应该
换行写注释
,例如下面写法会造成编译的报错:
#pragma kernel functionName // 一些注释
RWTexture2D
接着我们再来看看第二行:
RWTexture2D
<
float4
>
Result
;
看着像是声明了一个和纹理有关的变量,具体来看一下这些关键字的含义。
RWTexture2D中,RW其实是
Read
和
Write
的意思,Texture2D就是二维纹理,因此它的意思就是
一个可以被compute shader读写的二维纹理
。
如果我们只想读不想写,那么可以使用Texture2D的类型。
我们知道纹理是由一个个像素组成的,每个像素都有它的下标,因此我们就可以通过像素的下标来访问它们,例如:Result[uint2(0,0)]。
同样的每个像素会有它的一个对应值,也就是我们要读取或者要写入的值。这个值的类型就被写在了<>当中,通常对应的是一个rgba的值,因此是float4类型。通常情况下,我们会在ComputeShader中处理好纹理,然后在FragmentShader中来对处理后的纹理进行采样。
这样我们就大致理解这行代码的意思了,声明了一个名为Result的可读写二维纹理,其中每个像素的值为float4。
在Compute Shader中可读写的类型除了
RWTexture
以外还有
RWBuffer
和
RWStructuredBuffer
,后面会介绍。
numthreads
然后是下面一句(很重要!):
[
numthreads
(
8
,
8
,
1
)
]
又是num,又是thread的,肯定和线程数量有关。没错,它就是定义
一个线程组(Thread Group)中可以被执行的线程(Thread)总数量
,格式如下:
numthreads(X, Y, Z)
其中 X*Y*Z 的值即线程的总数量,例如 numthreads(4, 4, 1) 和 numthreads(16, 1, 1) 都代表着有16个线程。那么为什么不直接使用 numthreads(num) 这种形式定义,而非要分成X,Y,Z这种三维的形式呢?看到后面自然就懂其中的奥秘了。
每个核函数前面我们都需要定义numthreads
,否则编译会报错。
其中X,Y,Z三个值也并不是也可随便乱填的,比如来一刀 X=99999 暴击一下,这是不行的。它们在不同的版本里有如下的约束:

Compute Shader 版本Z的最大取值最大线程数量(X*Y*Z)
cs_4_x1768cs_5_0641024
在Direct11中,可以通过
ID3D11DeviceContext::Dispatch(X,Y,Z)
方法创建X*Y*Z个线程组,一个线程组里又会包含多个线程(数量即numthreads定义)。
注意顺序,先numthreads定义好每个核函数对应线程组里线程的数量,再用Dispatch定义用多少线程组来处理这个核函数
。其中
每个线程组内的线程都是并行的
,不同线程组的线程可能同时执行,也可能不同时执行。一般一个GPU同时执行的线程数,在1000-10000之间。
接着我们用一张示意图来看看线程与线程组的结构,如下图:

上半部分代表的是线程组结构,下半部分代表的是单个线程组里的线程结构。因为他们都是由(X,Y,Z)来定义数量的,因此就像一个三维数组,下标都是从0开始。我们可以把它们看做是表格一样,有Z个一样的表格,每个表格有X列和Y行。例如线程组中的(2,1,0),就是第1个表格的第2行第3列对应的线程组,(2,1,0)也就是他的SV_GroupID,下半部分的线程也是同理。
搞清楚结构,我们就可以很好的理解下面这些与单个线程有关的参数含义:
-
SV_GroupID :线程组的ID,其实就是一个int3的值,如果我们线程组定义为(X,Y,Z),那么SV_GroupID的取值范围即为(0,0,0)到(X-1,Y-1,Z-1)。
-
SV_GroupThreadID :线程组内的某个线程的ID,同样是一个int3的值。它不考虑与线程组的关系,例如不同线程组里的第一个线程的SV_GroupThreadID都是(0,0,0)。
-
SV_DispatchThreadID :所有线程组中的某个线程ID,也是一个int3的值。它和SV_GroupThreadID就不一样了,需要考虑线程组,例如我一个线程组有(X,Y,Z)个线程,那么SV_GroupID=(a,b,c)的线程组里的SV_GroupThreadID=(i,j,k)的线程的SV_DispatchThreadID为:(a*X+i, b*Y+j, c*Z+k)。
-
SV_GroupIndex :线程组内的某个线程的下标,是一个int值。例如我一个线程组有(X,Y,Z)个线程,其中第一个线程(0,0,0)的下标为0,下标增长的顺序是从左往右(x),然后从上往下(y),最后从前往后(z),例如:(1,0,0)=1,(1,0,0)=2,...,(0,1,0)=X,...,(0,0,1)=X*Y,... 因此可以得到如下公式:SV_GroupIndex=SV_GroupThreadID.z*X*Y+SV_GroupThreadID.y*X+SV_GroupThreadID.x
好好理解下,它们在核函数里非常的重要。
核函数
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,用ComputeShader我们就可以,实际上,前面默认的ComputeShader的代码里,核函数的内容就是这样的。
接下来我们来看看compute shader的妙处,
看 id.xy 的值
。id 的类型为SV_DispatchThreadID,我们先来回忆下SV_DispatchThreadID的计算公式:
我们设某个线程的SV_GroupID=(gx, gy, ?),SV_GroupThreadID=(i, j, ?),每个线程组的线程数量为(x, y, ?),那么 SV_DispatchThreadID.xy = (gx*x+i, gy*y+j)
注:因为不考虑 z,所以忽略了它的取值。
首先前面我们使用了[numthreads(8,8,1)],即x=8,y=8 且 i 和 j 的取值范围为0到7。那么我们线程组(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),...,线程组(m,n,0)中它的取值范围为(m*8, n*8, 0)到(m*8+7, n*8+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为我们提供了一个
ComputeShader
的类型用来引用我们前面生成的 .compute 文件:
public
ComputeShader
computeShader
;

此外我们再关联一个Material,因为ComputeShader处理后的纹理,依旧要经过FragmentShader采样后来显示。
public
Material
material
;
这个material我们使用一个unlit shader,并且纹理不用设置,如下:

然后关联到我们的脚本上,并且随便建个Cube也关联上这material。
接着我们可以将Unity中的
RenderTexture
赋值到ComputeShader中的RWTexture2D上,但是需要注意因为我们是多线程处理像素,并且这个处理过程是
无序
的,因此我们要将RenderTexture的
enableRandomWrite
属性设置为true,代码如下:
RenderTexture
mRenderTexture
=
new
RenderTexture
(
256
,
256
,
16
)
;
mRenderTexture
.
enableRandomWrite
=
true
;
mRenderTexture
.
Create
(
)
;
我们创建了一个分辨率为256*256的RenderTexture,首先我们要把它赋值给我们的material,这样我们的Cube就会显示出它。然后要把它赋值给我们computeshader中的Result变量,代码如下:
material
.
mainTexture
=
mRenderTexture
;
computeShader
.
SetTexture
(
kernelIndex
,
"Result"
,
mRenderTexture
)
;
这里有一个kernelIndex变量,即核函数下标,我们可以利用FindKernel来找到我们声明的核函数的下标:
int
kernelIndex
=
computeShader
.
FindKernel
(
"CSMain"
)
;
这样在我们fragment shader采样的时候,采样的就是compute shader处理过后的纹理:
fixed4
frag
(
v2f
i
)
:
SV_Target
{
// _MainTex 就是被处理后的 RenderTexture
fixed4
col
=
tex2D
(
_MainTex
,
i
.
uv
)
;
return
col
;
}
最后就是开线程组和调用我们的核函数了,在ComputeShader中,Dispatch方法为我们一步到位:
computeShader
.
Dispatch
(
kernelIndex
,
256
/
8
,
256
/
8
,
1
)
;
为什么是256/8,前面已经解释过了。来看看效果:

上图就是我们Unity默认生成的ComputeShader代码所能带来的效果,我们也可试下用它处理2048*2048的Texture,也是非常快的。
粒子效果
接下来我们再来看看粒子效果的例子:
首先一个粒子通常拥有颜色和位置两个属性,并且我们肯定是要在Compute Shader里去处理这两个属性的,那么我们就可以在Compute Shader创建一个struct来存储:
struct
ParticleData
{
float3
pos
;
float4
color
;
}
;
接着,这个粒子肯定是很多很多的,我们就需要一个像List一样的东西来存储它们,在Compute Shader中为我们提供了RWStructuredBuffer类型。
RWStructuredBuffer
它是一个可读写的buffer,并且我们可以指定buffer中的数据类型为我们
自定义
的struct类型,不用再局限于int,float这类的基本类型。
因此我们可以这么定义我们的粒子数据:
RWStructuredBuffer
<
ParticleData
>
ParticleBufferRWStructuredBuffer
-
Win32 apps
docs
.
microsoft
.
com
为了有动效,我们可以再添加一个时间相关值,我们可以根据时间来修改粒子的位置和颜色:
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 UpdateParticle
struct
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
;
ParticleBuffer
[
pindex
]
.
pos
=
float3
(
pindex
/
1000.0
+
sin
(
Time
)
,
5
*
cos
(
pindex
)
,
5
*
sin
(
pindex
)
)
;
ParticleBuffer
[
pindex
]
.
color
=
float4
(
(
sin
(
Time
)
+
1
)
*
0.5
,
(
cos
(
Time
)
+
1
)
*
0.5
,
abs
(
cos
(
Time
)
+
sin
(
Time
)
)
,
1
)
;
}
接下来我们要在C#里给粒子初始化并且传递给Compute Shader。我们要传递粒子数据,也就是说要给前面的RWStructuredBuffer<ParticleData>赋值,Unity为我们提供了
ComputeBuffer类来与RWStructuredBuffer或StructuredBuffer相对应
。
ComputeBuffer
在ComputeShader中经常需要将我们一些自定义的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
[
]
)
;
最后我们可以使用ComputeShader类中的SetBuffer方法来把它传递到Compute Shader中:
public
void
SetBuffer
(
int
kernelIndex
,
string
name
,
ComputeBuffer
buffer
)
记得用完后把它Release()掉。
在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
:我们可以用该方法绘制几何,第一个参数是拓扑结构,第二个参数数顶点数。
最终得到的效果如下(虽然丑了点):

其他知识点
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的数据类型,并且它们
支持在读取的同时写入
。它们只能在Fragment Shader和Compute Shader中被使用(绑定)。
如果我们的RenderTexture不设置enableRandomWrite,或者我们传递一个Texture给RWTexture,那么运行时就会报错:
the texture wasn't created with the UAV usage flag set!
不能被读写的数据类型,例如Texure2D,我们称之为
SRV(Shader resource view)
。
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的倍数,这样两种显卡都可以顾及到。
移动端支持问题
我们可以运行时调用
SystemInfo.supportsComputeShaders
来判断当前的机型是否支持ComputeShader。其中OpenGL ES 从 3.1 版本才开始支持ComputeShader,而使用Vulkan的Android平台以及使用Metal的IOS平台都支持ComputeShader。
然而有些Android手机即使支持ComputeShader,但是对RWStructuredBuffer的支持并不友好。例如在某些OpenGL ES 3.1的手机上,只支持Fragment Shader内访问StructuredBuffer。
在普通的shader中要支持ComputeShader,shader model最低要求为4.5,即:
#
pragma
target 4.5
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);
}
全局变量或常量?
假如我们要实现一个需求,在ComputeShader中判断某个顶点是否在一个固定大小的包围盒内,那么按照以往C#的写法,我们可能如下定义包围盒大小:
#
pragma
kernel CSMain
float3
boxSize1
=
float3
(
1.0f
,
1.0f
,
1.0f
)
;
// 方法1
const
float3
boxSize2
=
float3
(
2.0f
,
2.0f
,
2.0f
)
;
// 方法2
static
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才是最开始定义的值。
更多推荐
所有评论(0)