关于unity:Unity中Compute-Shader的基础介绍与使用

107次阅读

共计 15774 个字符,预计需要花费 40 分钟才能阅读完成。

前言

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

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

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

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

生成的文件属于一种 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);
}

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

语言

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 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;

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 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 才是最开始定义的值。

Shader variants and keywords

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

#pragma kernel CSMain
#pragma multi_compile __ COLOR_WHITE COLOR_BLACK

RWTexture2D<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);
#else
Result[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)

正文完
 0