opengl的着色器计算方面的应用
OpenGL的计算着色器
(之前曾经整理过opengl的相关学习资料,之后会放过来,这边先直接谈论OpenGL的计算着色器的学习)
背景
由于图形处理器(GPU)每秒能够进行数以亿计次的计算,它已成为一种性能十分惊人的器件。因此为了能够更加方便的使用GPU的计算性能,OpenGL将【着色器】这部分与GPU交互的语言中抽象处一种接口,来进行数据的运算。这个接口的所构造的着色器就称为**“计算着色器”**
概况与基本使用
计算着色器本身可以被认为是一个一级着色器(大概就说是顶点直接输入GPU计算,计算完成后的数据直接返回给电脑),没有固定的数据和输出格式。
计算着色器同样也是通过glCreateShader进行着色器对象的创建,通过glShaderSource将数据传入的GPU,最后通过函数glCompileShader将数据进行编译。使用的时候也是通过过glAttachShader对着色器对象绑定,然后使用glLinkShader将指定的着色器依附在顶点信息中,传入GPU进行数据的运算。
工作组及执行
计算着色器的任务以组为单位进行执行,称为工作组(work group)。拥有邻居的工作组被称为本地工作组(local workgroup), 这些组可以组成更大的组,称为全局工作组(global workgroup),而其通常作为执行命令的一个单位。
计算着色器会被全局工作组中每一个本地工作组中的每一个单元调用一次,工作组的每一个单元称为工作项(work item),每一次调用称为一次执行。执行的单元之间可以通过变量和显存进行通信,且可执行同步操作保持一致性。
图12-1对这种工作方式进行了说明。在这个简化的例子中,全局工作组包含16个本地工作组, 而每个本地工作组又包含16个执行单元,排成4*4的网格。每个执行单元拥有一个2维向量表示的索引值。
本地工作组的大小使用local_size_x,local_size_y,local_size_z来进行说明,默认均为1。例子如下:
1 | layout(local_size_x=8,local_size_y=8)in; |
这段内容的意思就是声明此时本地工作组的大小为8*8*1。
然后通过使用函数glDispatchCompute()把工作组发送到计算管线上:
void glDispatchCompute(GLuint num_groups_x, GLuint num_groups_y, GLuint num_groups_z);
其中num_groups_*表示在这个方向上设置的工作组的数量(并非工作组大小)。然后OpenGL就会创建一个
num_gourps_x*num_gourps_y*num_gourps_z
大小的工作组,存放所有的工作组。
工作组的位置
计算着色器需要从输入的指定位置读取数据,并且把数据输出到输出数组指定的位置上。因此会需要知道当前的数据在本地工作组,或者说全局工作组的位置。OpenGL提供了一下内置变量:
- const uvec3 gl_WorkGroupSize
- in uvec3 gl_NumWorkGroups
- in uvec3 gl_LocalInvocationID
- in uvec3 gl_WorkGroupID
- in uvec3 gl_GlobalInvocationID
- in uvec3 gl_LocalInvocationID
- in uint gl_LocalInvocationIndex
变量的含义:
gl_WorkGroupSize是一个用来存储本地工作组大小的常数。它已在着色器的布局限定符中有local_size_x,local_size_y和local_size_z声明。之所以拷贝这些信息,主要是为了两个目的:首先,它使得工作组的大小可以在着色器中被访问很多次而不需要依赖于预处理;其次,它使得以多维形式表示的工作组大小可以直接按向量处理,而无需显式地构造。
gl_NumWorkGroups是一个向量,它包含传给glDispatchCompute()的参数(num_groups_x,num_groups_y和 num_groups_z)。 这使得着色器知道它所属的全局工作组的大小。除了比手动给uniform显式赋值要方便外,一部分OpenGL硬件对于这些常数的设定也提供了高效的方法。
gl_LocalInvocationID 表示当前执行单元在本地工作组中的位置。它的范围从uvec3(0)到gl_WorkGroupSize – uvec3(1)
gl_WorkGroupID 表示当前本地工作组在更大的全局工作组中的位置。该变量的范围在uvec3(0)和gl_NumWorkGroups – uvec3(1)之间。
gl_GlobalInvocationID 由gl_LocalInvocationID、gl_WorkGroupSize和gl_WorkGroupID派生而来。它的准确值是gl_WorkGroupID * gl_WorkGroupSize +
gl_LocalInvocationID,所以它是当前执行单元在全局工作组中的位置的一种有效的3维索引。
gl_LocalInvocationIndex是gl_LocalInvocationID的一种扁平化形式。其值等于
gl_LocalInvocationID.z*gl_WorkGroupSize.x*gl_WorkGroupSize.y+gl_LocalInvocationID.y * gl_WorkGroupSize.x + gl_LocalInvocationID.x
它可以用1维的索引来代表2维或3维的数据。
通道(同步数据)
由于我们在使用GPU的时候是并行运算,为了实现让并行运行的管道通信,这里提供了关键字shared。当变量被声明了shared之后,这个变量将能够被保存在特定的位置上,从而对所有的着色器都可用。
提到了通道,那么就要扯到同步的问题。为了实现同步的功能,我们有两种处理方法:
- 运行屏障(execution barrier),通过barrier()函数触发。当着色器遇到了这个函数之后,就会运行停止直到所有的着色器都运行到了这个位置。
- 内存屏障(memery barrier),通过调用memoryBarrier()触发。此函数保证着色器写入内存的操作一定是交给内存端而不是cache或者队列调度的方式。
其他定义
管线 – 图形渲染管线(Graphics PipeLine)
在OpenGL中,几乎所有的图案都是绘制在3D的坐标下的,然后OpenGL进行了一定的转换从而将其放到2D的平面上。这个转换的过程是由一个叫做管线的模块。线管可以被分为两个部分:
- 1、将3D的坐标转换为2D的坐标
- 2、将2D坐标转变为实际的有颜色的像素
将坐标传入了管线后,就能够将数据转换成屏幕上的坐标,同时根据相关的顶点数据和段数据,从而对图形本身进行渲染。
着色器(Shader)
管线被分为了不同步骤,每个步骤都被高度的专门化(即是专门来处理这个问题),可以【并行】运行。借助并行的特性,现在的GPU上有许多个小程序在帮助我们利用管线处理我们的数据,这些小程序就叫做【着色器shader】。着色器语言会将嵌入到程序中的程序翻译成ARB Assembly Language。这些程序可以传送到GPU中被编译。
使用流程:
首先要通过将编写好的GLSL程序将其进行编译,从而得到一个编译好的GLSL对象:
当我们需要使用着色器的是时候,通过将需要传入的数据以合适的形式传入,然后将着色器依附在对应的顶点元素上,将其链接,最后传入GPU进行渲染和运算:
参考资料:
http://blog.csdn.net/panda1234lee/article/details/51777980