SSAO By Computer Shader(一)
开启一个专题,SSAO By Computer Shader。使用Computer Shader实现SSAO效果。第一篇Computer Shader 入门 。第二篇SSAO理论知识。第三篇SSAO By Computer Shader,使用Computer Shader实现SSAO效果。
文章目录
前言
开启一个专题,SSAO By Computer Shader。使用Computer Shader实现SSAO效果。第一篇Computer Shader 入门 。
提示:以下是本篇文章正文内容,下面案例可供参考
一、Compueter Shader是什么?
1.基本概念
首先我们需要认识Compueter Shader 。顾名思义,就是计算着色器。我们先来看一下百度百科上的概念。
Compute Shader是一种技术,是微软DirectX 11 API新加入的特性,在Compute Shader的帮助下,程序员可直接将GPU作为并行处理器加以利用,GPU将不仅具有3D渲染能力,也具有其他的运算能力,也就是我们说的GPGPU的概念和物理加速运算。多线程处理技术使游戏更好地利用系统的多个核心。
我们这里可以得出几个结论。Compuetrshader是在GPU上运行的,并且将GPU作为并行处理器,采用多线程处理,得以快速运行。
我们来看一下Unity官方API对Compueter Shader的解释。
我们可以看到Computer Shader是运行在GPU上,并且脱离常规的渲染管线。它们可用于大规模并行GPGPU算法,加速部分游戏渲染。以下是他的适用平台。
刚才我们讲到了他脱离常规渲染管线,首先我们来看看他渲染管线中的位置。
以下是我们的渲染管线流程图。
这张图很经典。涵盖了整个渲染管线流程。我们先来看一下左下角的备注。蓝色方块表示来自管线输入输出的各种Buffers。
绿色方块表示管线的固定函数不可编辑阶段
黄色方块表示可编程阶段
T表示贴图绑定
B表示Buffer绑定。
管线从左上角开始顶点数据buffer开始,经过中间这一块流程完成渲染主流程,分别经过顶点拉取进入顶点着色器然后经过曲面中色器,几何着色器,图元装配,最后到片段着色器,帧缓冲。整个渲染主流程完毕。渲染的主流程详细可以访问我的其他博客,感兴趣可以上去看看。这里不展开了。其次我们看到右上角就是我们ComputerShader的模块了。我们可以看到他是独立我们我们的管线之外的,所以很重要的一点他不会触发draw call。我们知道触发一次draw call就是cpu向gpu通信,cpu需要设置状态,准备数据。整个流程是很消耗性能。所以这也是computer shader效率高的原因之一。我们可以看到Computer由Disapth模块触发。进入Computer Shader模块。ComputerShader相比较常规Shader 而言,有一点比较特殊,就是其没有任何固定的输入或输出。但是我们可以通过Texture,Buffer作为输入输出。就是我们的蓝色方块。包含Shader storge buffers,Image Load Store,Uniform Block。
对以上做一个总结。
Computer Shader与渲染管线是分开的,他是一个在GPU上运行的渲染命令,这个命令没有用户定义的输入输出。但是可以通过Texuter和buffer作为传入数据载体,以及传出数据载体,不会触发draw Call。
我们继续来看API,触发Computer Shader
根据API解释,我们可以通过 ComputeShader.Dispatch()直接触发,这对应这一步操作。
我们继续来看API接口。
触发 ComputeShader.Dispatch()需要四个参数,分别是内核入口索引,threadGroupsX,threadGroupsY,threadGroupsZ。
此函数“运行”该计算着色器,从而启动 X、Y 和 Z 尺寸中指示数量的 计算着色器线程组。在每个工作组中均进行了一定数量的着色器调用(“线程”)。该工作组 大小是在计算着色器本身中指定的。因此计算着色器 调用的总数是组数乘以线程组大小。 可使用 GetKernelThreadGroupSizes 函数查询工作组的大小。我们前面有讲到Compuetrshader是在GPU上运行的,并且将GPU作为并行处理器,采用多线程处理。这里需要引入新的概念—线程分配。
2.线程分配
这张图也是超级经典,简单明了的解释了Computer Shader的线程分配。线程组分配建立在一个三维空间
我们通过ComputeShader.Dispatch()知道该函数触发需要四个参数。后三个参数X,Y,Z分别对应三个维度的坐标。我们称之为工作组,每个工作组下又是一个三维空间,存放线程。我们看图说话。
ComputeShader.Dispatch()我们将5,3,2代入。我们将建立一个三维线程组空间。x坐标5个格子,y坐标3个格子,z坐标2个格子。每个格子代表一个线程组。我们可以看到532=30,30个线程组被定义。每个线程组我们定义了(10,8,3)的三维线程空间。((10,8,3)我们在ComputerShader中定义)。这个每个线程组有10 * 8 * 3 = 240个线程被定义。以上就是我们的线程空间了。可以看出这是一个三维坐标空间,每个坐标又嵌套一个三维坐标空间。一个挺复杂的线程空间。这时候我们就需要定位线程,我们需要知道每个像素对应哪个线程。
为了方便定位线程,ComputerShader提供了四个数据。
SV_GroudThreadID(Int3)当前线程在所在线程组内的ID
SV_GroupID:(Int3)当前线程组ID
SV_DispatchThreadID (Int3)当前线程在所有线程组中所有线程里的ID
SV_GroupIndex int 当前线程在所在线程组内的下标
举个例子。ComputeShader.Dispatch()我们将5,3,2代入。我们将建立一个三维线程组空间。x坐标5个格子,y坐标3个格子,z坐标2个格子。每个格子代表一个线程组。在线程组坐标(2,1,0)下,我们展开其线程空间。线程空间我们定义(10,8,3),x坐标10个格子,y坐标3个格子,z坐标3个格子。线程坐标(7,5,0)对应四个数据的值
SV_GroudThreadID = (7,5,0)
SV_GroupID =(2,1,0)
SV_DispatchThreadID = [2, 1, 0] * (10, 8, 3) + (7, 5, 0) = (27, 13, 0)
SV_GroupIndex = 0 * 10 * 8 + 5 * 10 * 7 = 57
再举个例子。线程组的概念如上图所示。一般情况下我们会将屏幕如上图分割线程组,每块线程组都是三维的,当然你定义成2维也没问题。每一块我们继续分割,就是我们的线程空间。以上就是我们的线程组分配。
我们继续来看API。变体和关键字。他说你可以使用Keyword在computer shader中去生成变体,用法跟常规shader一样。
2.keywords和关键字
我们看到Computer Shader是同样支持keywords和关键字的。
对以上做一个总结。ComputerShader可视为一种特殊的,单一阶段的管线。每个ComputerShader执行一个称为工作组的工作单位,每个工作组细分多个线程并行执行。无固定输入输出,通过Texture,Buffer交互数据。
二、使用步骤
1.demo需求
接下来我们开始实际操作以下。
举个例子,当前我们这样一个需求,在一个面片的显示区域内划分一个矩形并填充颜色,使之与面片颜色区分开。
2.操作步骤
我们选择增加一个shader文件,选中Computer Shader 类型。点击创建。
生成一个带cs标签的文件,每个一个Compuetr都需要的一个cs文件来控制,为此我们再创建一个cs脚本。再创建一个常规shader 文件用于面片的实际渲染。
我们开始编辑cs脚本
代码如下(示例):
public ComputeShader shader;
public int texResolution = 1024;
Renderer rend;
RenderTexture outputTexture;
int kernelHandle;
// Use this for initialization
void Start()
{
outputTexture = new RenderTexture(texResolution, texResolution, 0);
outputTexture.enableRandomWrite = true;
outputTexture.Create();
rend = GetComponent<Renderer>();
rend.enabled = true;
InitShader();
}
我们定义公共变量包含 ComputerShader, 以及texResolution,就是我们贴图的大小一个1024的图。Rend 用于获取对象材质球修改shader属性。RenderTexture outputTexture(前面我们有讲到,computer没有专门的输入输出),所以我们定义 outputTexture将作为Computer Shader的输出。kernelHandle作为我们Computer Shader的入口索引。在Star()函数中我们初始化 outputTexture并将其设置维可以随机写入,因为我们需要在Compueter中写入数据。并调用了InitShader()函数初始化 shader。
代码如下(示例):
private void InitShader()
{
kernelHandle = shader.FindKernel("Square");
int halfRes = texResolution >> 1;
int quarterRes = texResolution >> 2;
Vector4 rect = new Vector4( quarterRes, quarterRes, halfRes, halfRes );
shader.SetVector( "rect", rect );
shader.SetTexture(kernelHandle, "Result", outputTexture);
rend.material.SetTexture("_MainTex", outputTexture);
DispatchShader(texResolution / 8, texResolution / 8);
}
private void DispatchShader(int x, int y)
{
shader.Dispatch(kernelHandle, x, y, 1);
}
kernelHandle = shader.FindKernel(“Square”);
我们在这里获取 Computer Shader入口索引。来看看API的解释
//
// 摘要:
// Find ComputeShader kernel index.
//
// 参数:
// name:
// Name of kernel function.
//
// 返回结果:
// The Kernel index, or logs a "FindKernel failed" error message if the kernel is
// not found.
[NativeMethod(Name = "ComputeShaderScripting::FindKernel", HasExplicitThis = true, IsFreeFunction = true, ThrowsException = true)]
[RequiredByNativeCode]
public int FindKernel(string name);
函数的作用是查找Computer Shader 的入口索引。参数是 入口函数的名字,返回结果是入口索引,如果找不到的话就会有 FindKernel failed 的错误报警。
我们将texResolution 右移一位,右移两位作为我们的矩形范围传入shader,以及我们的 outputTexture,并将 outputTexture作为面片shader的MainTex属性。调用DispatchShader(),激活Computer Shader。参数就是我们线程组分配。这里填写 texResolution/8,texResolution/8,1,一般情况下我们都会这样填。8是我们在Computer Shader中定义线程个数。这样总线程数就是我们实际texResolution 定义的分辨率像素个数。
以上就是CS代码的内容。
我们来看一下ComputerShader的内容
// Each #kernel tells which function to compile; you can have many kernels
#pragma kernel Square
// Create a RenderTexture with enableRandomWrite flag and set it
// with cs.SetTexture
RWTexture2D<float4> Result;
float4 rect;
float inSquare( float2 pt, float4 rect ){
float horz = step( rect.x, pt.x ) - step( rect.x + rect.z, pt.x );
float vert = step( rect.y, pt.y ) - step( rect.y + rect.w, pt.y );
return horz * vert;
}
[numthreads(8,8,1)]
void Square (uint3 id : SV_DispatchThreadID)
{
float res = inSquare( (float2)id.xy, rect );
Result[id.xy] = float4(0.0, 0.0, res, 1.0);
}
这里定义我们入口名称,我们可以定义很多个入口。变量我们定义了 RWTexture2D Result;一个支持读写的Textrure。以及矩形范围rect,这些变量实际值都由cs代码传入。至此数据从CPU传递到GPU。我们来看入口函数,Square(),标签[numthreads(8,8,1)],这里定义我们单个线程组的线程个数。参数填(8,8,1)。这样分辨率1024的图的像素一共用 1024 * 1024 = 1,048,576个像素,而我们的线程总数就是
1024 * 1024 = 1,048,576
(1024/8 * 1024/8 * 1) *( 8 * 8 * 1) = 1,048,576
可以看出像素个数跟相乘总数是一致的。每个像素都有一个线程处理。
函数Square()我们定义了三维参数 id :来自于SV_DispatchThreadID。我们前面有讲到,SV_DispatchThreadID 就是当前线程在所有线程组中所有线程里的ID,在这里我们用为定位线程,可以看到我们第三参数一直都是填1,这里是为了简化维度,将三维空间转化为二维空间,在这里SV_DispatchThreadID可以简单理解为像素坐标。
接下来我们将参数id,以及从cs代码传进来的矩形范围参数rect 放入函数inSquare()计算检测,范围内范围1,范围外返回0,写入Result,就是我们在cs定义的RenderTexture。从函数inSquare()中的逻辑,我们可以看到Computer Shader一直在强调我是用于计算的。ComputerShader没有返回值,所有这里没有Return 语法。
最后我们来看渲染面片的Shader。
fixed4 frag (v2f i) : SV_Target
{
// sample the texture
fixed4 col = tex2D(_MainTex, i.uv);
// apply fog
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
这里就很简单了,直接采样贴图数据返回。得到以下效果
接下来我们来看一下 Frame Debug
我们可以看到在未调正渲染顺序时,Computer Shader总是第一个被执行,右边面板中显示了Computer Shader的名字,入口名字,线程组分配。以及被写入的Texture以及用到的矩形范围参数。
左边的面板中我们可以看到在Status的面板中, Batches是0,即他没有触发draw call。我们可以看到以上几种特性。
这样我们就能得出使用Compuetr Shader的一套组合拳。
CS代码分配,传输变量,并激活Computer Shader,
Computer Shader 完成计算逻辑并将计算结果写入 Texture或者 Buffer
渲染Shader 引用Computer Shader的计算结果完成相应显示效果。
以上就是一个及其简单的入门例子,计算量也很小。还有很多复杂计算的例子,比如生成草地,计算重力等等。
–
总结
以上就是今天要讲的内容,先对Computer Shader有个大概了解,下一节,我们开始讲SSAO理论知识。