Unity3D Shader系列之Compute Shader基础及图像灰度化
目录
- 1.什么是Compute Shader
- 2.Compute Shader语法
- 2.1 #pragma kernel
- 2.2 Compute Shader中的变量
- 2.2.1 标量
- 2.2.2 向量
- 2.2.3 矩阵(matrix)
- 2.2.4 数组
- 2.2.5 StructuredBuffer
- 2.2.6 Texture
- 2.3 numthreads与Dispatch
- 2.3.1 numthreads
- 2.3.2 线程为什么要分组
- 2.3.3 numthreads中的xyz值何时最优
- 2.3.4 Dispatch
- 2.4 SV_DispatchThreadID
- 3. Compute Shader灰度化图像
- 4 参考文章
前两篇我们分别用OpenCV for Unity和片元着色器实现了图像灰度化的功能,今天我们来看看使用Compute Shader实现图像灰度化的功能。
1.什么是Compute Shader
定义
Compute Shader也是Shader中的一种,也运行在GPU上。它和我们平时说的顶点、片元着色器不同地方在于,Compute Shader不经过渲染流水线,而是用于通用计算。什么是通用计算呢?就是把之前在CPU的计算挪到GPU这边来。基于GPU强大的并行计算能力,与通过CPU计算相比能极大地缩短计算时间。使用GPU进行通用计算又叫GPGPU(General Purpose GPU Programming)编程。
适用场景
可以并行计算的地方,如图片的处理,视频编码。
我们这里想使用Compute Shader对图片进行灰度化。原因有二,①是图片上各个像素点灰度计算互不干扰,可以利用GPU的强大并行计算能力;②是通过GPU计算生成的图片可以不用再传回CPU而直接层递到屏幕,这也是个提升效率的地方。
性能瓶颈
如图,CPU和GPU之间的数据传输是瓶颈。
语言
Unity中Compute Shader是用什么语言写的呢?标准的DX11 HLSL。
2.Compute Shader语法
如图,首先创建一个Compute Shader。
然后将下面灰度化纹理的Compute Shader拷贝过去,咱们来看看它的语法。
// 申明内核
#pragma kernel CSMain// 定义变量
Texture2D inputTex;
RWTexture2D<float4> outputTex;// 定义线程组内线程的数量
[numthreads(8,8,1)]
void CSMain (uint3 id : SV_DispatchThreadID)
{float r = inputTex[id.xy].r;float g = inputTex[id.xy].g;float b = inputTex[id.xy].b;float a = inputTex[id.xy].a;float gray = r * 0.299 + g * 0.587 + b * 0.114;outputTex[id.xy] = float4(gray, gray, gray, a);
}
2.1 #pragma kernel
用于定义内核,一个Compute Shader至少要有一个内核。内核其实就是下面定义的方法名,我们可以在CPU端命令GPU执行这个内核(ComputeShader.Dispatch,后面会讲到)。
#pragma kernel CSMain
上面这句代码就表示CSMain这个方法是一个内核。
在c#这边,我们通过使用ComputeShader.FindKernel去持有并记录到对应内核的索引值,以后向GPU传递数据时都需要传递它。
FindKernel的参数只有一个,即Compute Shader中内核名(由#pragma kernal指定的),返回值为该内核的索引。
// 参数为内核名(由#pragma kernal指定的),返回值为该内核的索引。
int kernal = cs.FindKernel("CSMain");
2.2 Compute Shader中的变量
Compute Shader语言是HLSL,所以HLSL支持的变量其都支持。
可去微软官方文档查看。
注:标量到数组部分摘自文章《DirectX11–HLSL语法入门》
2.2.1 标量
类型 | 描述 |
---|---|
bool | 32位整数值用于存放逻辑值true和false |
int | 32位有符号整数 |
uint | 32位无符号整数 |
half | 16位浮点数(仅提供用于向后兼容) |
float | 32位浮点数 |
double | 64位浮点数 |
2.2.2 向量
向量类型可以支持2到4个同类元素。
3种申明向量的方式。
1.使用类似模板的形式来描述
vector<float, 4> vec1; // 向量vec1包含4个float元素
vector<int, 2> vec2; // 向量vec2包含2个int元素
2.直接在基本类型后面加上数字
float4 vec1; // 向量vec1包含4个float元素
int3 vec2; // 向量vec2包含3个int元素
3.使用vector本身则表示为一种包含4个float元素的类型**
vector vec1; // 向量vec1包含4个float元素
向量的初始化。
float2 vec0 = {0.0f, 1.0f};
float3 vec1 = float3(0.0f, 0.1f, 0.2f);
float4 vec2 = float4(vec1, 1.0f);
向量的使用与赋值。
向量的第1到第4个元素既可以用x, y, z, w来表示,也可以用r, g, b, a来表示。除此之外,还可以用索引的方式来访问。下面展示了向量的取值和访问方式:
float4 vec0 = {1.0f, 2.0f, 3.0f, 0.0f};
float f0 = vec0.x; // 1.0f
float f1 = vec0.g; // 2.0f
float f2 = vec0[2]; // 3.0f
vec0.a = 4.0f; // 4.0f
我们还可以使用swizzles的方式来进行赋值,可以一次性提供多个分量进行赋值操作,这些分量的名称可以重复出现:
float4 vec0 = {1.0f, 2.0f, 3.0f, 4.0f};
float3 vec1 = vec0.xyz; // (1.0f, 2.0f, 3.0f)
float2 vec2 = vec0.rg; // (1.0f, 2.0f)
float4 vec3 = vec0.zzxy; // (4.0f, 4.0f, 1.0f, 2.0f)
vec3.wxyz = vec3; // (2.0f, 4.0f, 4.0f, 1.0f)
vec3.yw = ve1.zz; // (2.0f, 3.0f, 4.0f, 3.0f)
2.2.3 矩阵(matrix)
矩阵有如下类型(以float为例):
float1x1 float1x2 float1x3 float1x4
float2x1 float2x2 float2x3 float2x4
float3x1 float3x2 float3x3 float3x4
float4x1 float4x2 float4x3 float4x4
此外,我们也可以使用类似模板的形式来描述:
matrix<float, 2, 2> mat1; // float2x2
而单独的matrix类型的变量实际上可以看做是一个包含了4个vector向量的类型,即包含16个float类型的变量。matrix本身也可以写成float4x4:
matrix mat1; // float4x4
矩阵的初始化方式如下:
float2x2 mat1 = {1.0f, 2.0f, // 第一行3.0f, 4.0f // 第二行
};
float3x3 TBN = float3x3(T, B, N); // T, B, N都是float3
矩阵的取值方式如下:
matrix M;
// ...float f0 = M._m00; // 第一行第一列元素(索引从0开始)
float f1 = M._12; // 第一行第二列元素(索引从1开始)
float f2 = M[0][1]; // 第一行第二列元素(索引从0开始)
float4 f3 = M._11_12; // Swizzles
矩阵的赋值方式如下:
matrix M;
vector v = {1.0f, 2.0f, 3.0f, 4.0f};
// ...M[0] = v; // 矩阵的第一行被赋值为向量v
M._m11 = v[0]; // 等价于M[1][1] = v[0];和M._22 = v[0];
M._12_21 = M._21_12; // 交换M[0][1]和M[1][0]
无论是向量还是矩阵,乘法运算符都是用于对每个分量进行相乘,例如:
float4 vec0 = 2.0f * float4(1.0f, 2.0f, 3.0f, 4.0f); //(2.0f, 4.0f, 6.0f, 8.0f)
float4 vec1 = vec0 * float4(1.0f, 0.2f, 0.1f, 0.0f); //(2.0f, 0.8f, 0.6f, 0.0f)
若要进行向量与矩阵的乘法,则需要使用mul函数。
2.2.4 数组
float M[4][4];
int p[4];
float3 v[12]; // 12个3D向量
除以上类型外,Compute Shader还支持StructuredBuffer和Texture,这两种类型也是我们实际开发中最常使用的。
2.2.5 StructuredBuffer
我们可以自定义结构体。结构体可以存放任意数目的标量,向量和矩阵类型,除此之外,它还可以存放数组或者别的结构体类型。
struct A
{float4 vec;
};struct B
{int scalar;float4 vec;float4x4 mat;float arr[8];A a;
};// 结构体的访问
B b;
b.vec = float4(1.0f, 2.0f, 3.0f, 4.0f);
在Compute Shader这边我们一般用StructuredBuffer和RWStructuredBuffer来存储结构体数组,StructuredBuffer对Compute Shader而言是只读的(只能通过C#设置值),RWStructuredBuffer可读®可写(W)。
它们两者在C#这边对应的类是ComputeBuffer。
StructuredBuffer、RWStructuredBuffer和ComputeBuffer如何使用见下面这个例子。
Compute Shader:
struct Vertex {float3 pos;float2 uv;
}RWStructuredBuffer< Vertex > buffer;
C#:
// 定义与Compute Shader中结构体相同的结构体
struct Vertex
{public Vector3 pos; // c#中的Vector3对应Compute Shader中的float3可对应public Vector2 uv; // c#中的Vector2可对应float2
}private void RunShader()
{// 定义结构体数组Vertex[] vertex = new Vertex[64];Vertex[] result = new Vertex[64];// 初始化vertex数据...// 示例化一个ComuteBuffer,用于和Compute Shader的RWStructuredBuffer交互// ComputeBuffer构造函数需指定“数组的长度”, “一个结构体所占用的字节数”// 我们的结构体中一共5个float,一个float占4个字节,所以该结构体占20个字节ComputeBuffer buffer = new ComputeBuffer(vertex.Length, 20);// vertex的设置给ComputeBufferbuffer.SetData(vertex);// 加载ComputeShaderComputeShader cs = Resources.Load<ComputeShader>("xxx");// FindKernel("内核"),获取到内核Idint kernel = cs.FindKernel("CSMain");// 将buffer中的值赋值给Compute Shader中的buffer,将两者联系起来cs.SetBuffer(kernel, "buffer", buffer);// 执行kernal这个内核cs.Dispatch(kernel, 8, 8, 1);// 从Compute Shader中读取计算后的值,结果存在result中buffer.GetData(result);// 不再使用时释放内存buffer.Dispose();
}
从例子可以看出,c#中的Compute Buffer与Compute Shader中StructuredBuffer\RWStructuredBuffer的交互主要就以下几个方法。
1.Compute Buffer实例化,需指定数组的长度,和数组中单个元素占用的字节数
ComputeBuffer buffer = new ComputeBuffer(vertex.Length, 20);
2.使用ComputeBuffer.SetData,传入一个数组,将该数组中的值拷贝到ComputeBuffer
需要注意的是数组的长度和单个元素占的字节数要和buffer初始化是指定的值相同。
// vertex的设置给ComputeBuffer
buffer.SetData(vertex);
3.使用ComputeShader.SetBuffer将buffer指定给Compute Shader中StructuredBuffer\RWStructuredBuffer,参数分别是内核名、Compute Shader中StructuredBuffer\RWStructuredBuffer的变量名、要传递的ComputeBuffer
// 将buffer中的值赋值给Compute Shader中的buffer,将两者联系起来
cs.SetBuffer(kernel, "buffer", buffer);
4.ComputeShader.Dispatch执行ComputeShader
Dispatch中的参数是什么意思?后面我们会讲。
5.最后从Compute Shader中读取计算后的结果,方法为ComputeBuffer.GetData
// 从Compute Shader中读取计算后的值,结果存在result中
buffer.GetData(result);
2.2.6 Texture
Texture2D< float4 > xx; // 只读
RWTexture2D< float4 > xx; // 读写
RWTexture2D< float2 > xx; // RG_int
Texture2d< float4 >对应c#中的Texture2D。
RWTexture2d< float4 >对应c#中的RenderTexture。
在Unity里读写的只能是RenderTexture并且支持随机读写(RenderTexture.enableRandomWrite = true)
与StructuredBuffer类似,c#中使用ComputeShader.SetTexture(内核Id, “ComputeShader中的Texture或RWTexture2D变量名”, c#中的Texture2D或RenderTexture)
cs.SetTexture(kernal, "inputTex", inputTexture);
2.3 numthreads与Dispatch
Compute Shader利用了GPU的并行处理,这里的并行处理是指Compute Shader中的内核方法会被GPU中的多个线程同时运行。
这里的多个线程到底是多少个,是由我们自己确定的。这些线程需要被拆分为多个线程组,一个线程组中包含一定数量的线程,具体方式是通过numthreads和Dispatch来设置。
比如我们可以通过设置numthreads和Dispatch来让一个线程单独处理图像上的一个像素点。
但要注意这种处理是无序的随机的,并不一定是固定的处理顺序,例如不一定是从左到右挨个处理像素点。
2.3.1 numthreads
我们看到在内核方法的上方,加上了一个[numthreads(8,8,1)]前缀。
[numthreads(8,8,1)]
void CSMain (uint3 id : SV_DispatchThreadID)
{// ...
}
numthreads它的作用是什么呢?它是用来指定线程组(Threads Group)中线程的数量。
什么是线程组呢?可以简单将其理解为是一堆线程的组合。一个线程组,包含有很多线程。
那这么多线程,在线程组中是怎么排列的?类似长方体的排列。
numthreads后面指定了x,y,z三个方向各有多少个线程。画了个图,相信大家一看就明白了。
比如我们指定[numthreads(8,4,2)],其指定了x、y、z轴各8个、4个、2个线程,所以一个线程组共842=64个线程。其线程排列如下,其中一个小立方体代表一个线程(小立方体内的编号我们暂时用不上,但是后面谈到SV_DispatchThreadID的时候我们会用到)。
又比如[numthreads(8,8,1)],其指定x、y、z轴各8个、8个、2个线程,所以一个线程组共881=64个线程。排列规则如下。
2.3.2 线程为什么要分组
问题来了,我们把多个线程合成一个线程组的意义在哪儿,有什么好处?
答案是在同一个组的线程咱们可以共享变量,并能够将它们设置为同步。
2.3.3 numthreads中的xyz值何时最优
GPU一次Dispatch会调用64(AMD成为wavefront)或32(NVIDIA称为warp)个线程(这实际上是一种SIMD技术),所以,numThreads的乘积最好是这个值的整数倍。但是Mali不需要这种优化。此外,Metal可以通过API获取这个值。
numthreads的数值除了应该为32或64的整数倍外,也是有大小限制的,具体如下表所示。
Compute Shader | Maximum Z | Maximum Threads (XYZ) |
---|---|---|
cs_4_x | 1 | 768 |
cs_5_0 | 64 | 1024 |
可以鼠标右键选中一个Compute Shader,然后点击Show compiled code,打开的汇编语言里面有说明是哪个版本,是cs_4_x还是cs_5_0。
鉴于以上两个条件,我一般在工作中最常使用numthreads(8, 8, 1)。
2.3.4 Dispatch
ComputeShader.Dispatch用来指定有多少个线程组。
Dispatch用法如下,第一个参数为内核索引值,后面三个参数分别为x、y、z轴的线程组数量。
cs.Dispatch(kernal, 1024/8, 768/8, 1);
比如我们设定cs.Dispatch(kernal, 4, 4, 2),则线程组的排列规则如下。
如果同时numthreads(8, 8, 1),则numthreads和Dispatch的关系如下。
两者的关系,咱们还可以结合nvida和微软的两张图来理解。
图片来源:https://www.nvidia.com/content/GTC-2010/pdfs/2260_GTC2010.pdf
图片来源:https://docs.microsoft.com/en-us/windows/win32/direct3dhlsl/sm5-attributes-numthreads
两者的关系弄清楚后,问题又来了,Dispatch中的xyz值怎么设置才是最优的?
这里有一个原则:
A Thread Group 运行在一个GPU单元 (A single multiprocesser),如果GPU有16个
multiprocesser,那么程序至少要分成16个 Thread Group使得每个multiprocesser都参与计算。
组之间不分享内存。
当然这个原则不是必须的,只是遵循这样的原则效率最高。
一个常用的经验是Dispatch(kernal, 图片长(像素)/numthreads中的x值,图片宽(像素)/numthreads中的y值, 1),即将图像的尺寸除以numthreads的xy。
同时需要注意的是,这样做要满足上面的原则的话,图片的长宽像素最好均为32的整数倍。
比如我们要处理的图像像素为1024×768,numthreads[8, 8, 1],那么按照经验应该设置Dispatch(kernal, 1024/8, 768/8, 1)。
2.4 SV_DispatchThreadID
[numthreads(8,8,1)]
void CSMain (uint3 id : SV_DispatchThreadID)
{// ...
}
SV_DispatchThreadID用标识当前线程在整个线程组中的位置,是一个三维向量。
由于我们通常通过Dispatch和numthreads来让一个线程处理一个像素点,所以可以通过SV_DispatchThread来获取到当前处理的像素点。
当然除了SV_DispatchThreadID外,我们还可以使用SV_GroupID、SV_GroupThreadID、SV_GroupIndex。
变量名 | 含义 |
---|---|
SV_GroupThreadID | 当前线程在该线程组中的位置 |
SV_GroupID | 当前线程所在线程组的位置 |
SV_DispatchThreadID | 当前线程在整个线程组中的位置 |
SV_GroupIndex | 当前线程在该线程组中的索引值 |
说到这里,Compute Shder的语法就讲得差不多了,最后,咱们去Unity官网看看ComputeShader类的完整API。
3. Compute Shader灰度化图像
Compute Shader:
#pragma kernel CSMainTexture2D<float4> inputTex;
RWTexture2D<float4> outputTex;[numthreads(8,8,1)]
void CSMain (uint3 id : SV_DispatchThreadID)
{float r = inputTex[id.xy].r;float g = inputTex[id.xy].g;float b = inputTex[id.xy].b;float a = inputTex[id.xy].a;float gray = dot(inputTex[id.xy].rgb, float3(0.299, 0.587, 0.114));outputTex[id.xy] = float4(gray, gray, gray, a);
}
C#:
using UnityEngine;
using UnityEngine.UI;public class GrayDemo : MonoBehaviour
{private ComputeShader m_GrayComputeShader;private RawImage m_GrayImg;private void Start(){m_GrayImg = GameObject.Find("Canvas/RawImg-ComputeShader").GetComponent<RawImage>();Texture2D inputTexture = Resources.Load<Texture2D>("flower_art_0EN071");RenderTexture rt = new RenderTexture(inputTexture.width, inputTexture.height, 24);rt.enableRandomWrite = true;rt.Create();m_GrayImg.texture = rt;m_GrayImg.rectTransform.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, rt.width);m_GrayImg.rectTransform.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, rt.height);m_GrayComputeShader = Resources.Load<ComputeShader>("ComputeShader/Gray");int kernal = m_GrayComputeShader.FindKernel("CSMain");m_GrayComputeShader.SetTexture(kernal, "inputTex", inputTexture);m_GrayComputeShader.SetTexture(kernal, "outputTex", rt);m_GrayComputeShader.Dispatch(kernal, 1024/8, 768/8, 1);}
}
效果如下。
项目链接。
链接:https://pan.baidu.com/s/1V6gqXlKN_YREpiaFXd_Wfg
提取码:z77y
博主个人博客本文链接。
4 参考文章
- Shader第二十八讲 Compute Shaders
- Compute Shader次世代优化方案
- Unity中的ComputeShader
- Unity Compute Shader入门初探
- Unity 3D : ComputeShader 全面詳解
- DirectX11–HLSL语法入门
- Unity3d | 浅谈 Compute Shader
- 在Unity开始你的Compute Shader编写
- OpenGL-Compute Shader的输入和输出
- Unity Compute Shader Thread Groups 线程组
- CUDA编程——GPU架构,由sp,sm,thread,block,grid,warp说起
- https://www.nvidia.com/content/GTC-2010/pdfs/2260_GTC2010.pdf
- https://docs.microsoft.com/en-us/windows/win32/direct3dhlsl/sm5-attributes-numthreads
- https://docs.unity3d.com/2020.2/Documentation/ScriptReference/ComputeShader.html
- 微软HLSL官方文档
Unity3D Shader系列之Compute Shader基础及图像灰度化相关推荐
- Unity3D Shader系列之UI Image灰度化
目录 1.灰度化是什么 2.灰度值计算方法 3.灰度化的目的 4.Shader实现 5.参考文章 在<OpenCV for Unity学习笔记(1)--Mat及灰度化图像>我们看到,使用O ...
- Unity3D Shader系列之描边
目录 1 引言 2 顶点沿法线外拓方式 2.1 法线外拓+ZTest Always 2.1.1 代码 2.1.2 问题点 2.2 法线外拓+Cull Front 2.2.1 代码 2.2.2 改进点 ...
- Unity3D Shader系列之全息投影
1 效果展示 2 实现原理 全息投影其实是几个效果的叠加:①半透明效果②上下条纹的扫描效果③边缘光效果④模拟信号传输不稳定的顶点偏移效果. 咱们依次来看看这几个效果背后的原理. ①半透明效果 在Uni ...
- 【Unity3D Shader编程】之十三 单色透明Shader 标准镜面高光Shader
本系列文章由@浅墨_毛星云 出品,转载请注明出处. 文章链接: http://blog.csdn.net/poem_qianmo/article/details/50878538 作者:毛星云(浅 ...
- 【Unity3D Shader编程】之十三 单色透明Shader 标准镜面高光Shader
分享一下我老师大神的人工智能教程!零基础,通俗易懂!http://blog.csdn.net/jiangjunshow 也欢迎大家转载本篇文章.分享知识,造福人民,实现我们中华民族伟大复兴! 本系列文 ...
- Compute Shader次世代优化方案
这是侑虎科技第498篇文章,感谢作者凯奥斯供稿.欢迎转发分享,未经作者授权请勿转载.如果您有任何独到的见解或者发现也欢迎联系我们,一起探讨.(QQ群:793972859) 作者主页:https://z ...
- Directx 计算着色器(compute shader)
原文 :http://www.cnblogs.com/Ninputer/archive/2009/12/11/1622190.html 博者注:计算着色器调试(http://msdn.microsof ...
- Shader Forge 入门学习(一) 基础操作
引言:失踪人口回归,最近几个月刚刚毕业,进入社会,对着未来有着些许迷茫,但起风了,唯有努力生存!近日学习Shader Forge,记录下来,共同进步!内容主要包括ShaderForge的操作设置,并配 ...
- android device monitor命令行窗口在哪里_Vulkan在Android使用Compute shader
oeip 相关功能只能运行在window平台,想移植到android平台,暂时选择vulkan做为图像处理,主要一是里面有单独的计算管线且支持好,二是熟悉下最新的渲染技术思路. 这个 demo(git ...
- OpenGL Compute Shader计算着色器的实例
OpenGL Compute Shader计算着色器 先上图,再解答. 完整主要的源代码 源代码剖析 先上图,再解答. 完整主要的源代码 // #define USE_GL3W #include &l ...
最新文章
- Ajax 的优势和不足
- Apache Solr schema.xml及solrconfig.xml文件中文注解
- 解决Ext JS 4.1版本Tree在刷新时选择第一行的问题
- thinkphp 查找表并返回结果
- 当系统扩展遇到违背OO的里氏原则(LSP)的时候怎么办 ?
- ORACLE+RAC+ASM环境下添加redo日志组
- 「C++」C++ Primer Plus 笔记:第三章 处理数据
- 晟数学院 oracle,Oracle 控制文件存储解析
- react-native flatlist 上拉加载onEndReached方法频繁触发的问题
- MySQL Internals Manual
- Hack.Chat 在浏览器里快速建立简单、随用即丢线上聊天室,无须下载安装软体
- Perl用LWP实现GET/POST数据发送
- 国内最全最详细的hadoop2.2.0集群的HA高可靠的最简单配置
- Solr Wiki文档
- Java中多态的一些简单理解
- 一篇文章带你搞定数学建模中的 Malthus人口模型(含代码)
- 联想数据中心技术总监单奖定:双态IT -数字化转型下的IT建设新思路
- 你在被窝里刷手机岁月静好,一个名叫 Flink 的 ​“神秘引擎” 却在远方和时间赛跑...
- 机械臂技术参数的意义
- ST-LINK/V2:cannot reset target shutting down debug session
热门文章
- 手机端APP防盗链配置问题
- windows10双系统删除linux,双系统删除教程详解:Windows(linux)双系统,教你如何删除其中一个!...
- 新能源与材料如何应用计算机,计算机模拟在光电材料及太阳能工程领域的应用与新进展.doc...
- python井字棋_python之井字棋游戏
- 快速 二进制,八进制,十进制,十二进制转换 .源码,反码,补码,
- excel下拉公式保持一些参数不变
- 单片机和嵌入式设计的区别
- 计算快递费系统(java版)
- 415错误及解决方法
- R-概率统计与模拟(四)拒绝抽样