使用GPU进行通用计算和常规的使用CPU进行计算在观念上具有非常大的区别,很多资料都会进行对比(比如经典的《GPU Gem 2》),但是通常用语都比较专业化,初学者可能很难想明白。这里按照我目前的理解,先总结一下:
首先需要明确的是,GPGPU中所有计算的数据,都保存在纹理中。比如一个长度为16的一维数组,在GPGPU中就需要建立一个2*2的纹理,这样这张纹理就有4个像素,而每个像素包含R、G、B、A四个颜色分量,所以总共是16个数据。
具体的数据操作,则需要一个由内存(这里指的是计算机内存)向显存(这里指显卡内存)传输的过程——每一组数据都需要在内存中创建,然后将数据传送到一个纹理中,这些数据在纹理中的表示,则是纹理中每一个像素的颜色值(这个颜色值可以是一个分量,也可以是多个分量,看具体的设计)。
FBO的概念在GPGPU中非常重要!
在将FBO创建好,并设定好我们之后的渲染指向这个FBO后,还需要明确一个观念——“渲染到纹理”(Render to Texture),这里的RTT和三维建模中的烘焙(Baker)不是一个概念。由于我们需要一个计算的结果数据,而这个数据在显存中是保存在一个纹理中的,所以GPGPU的最终渲染成果,是一张包含计算结果的纹理,因此这里的RTT是指我们创建一个结果纹理,并和FBO绑定,让GPU“渲染”(或者,这在里也可以理解为“计算”)到这张纹理上,于是这张结果纹理中每一个像素的颜色值组合起来就是我们需要的计算结果数组了。
那么这个相加的计算怎么做呢?这里就是GPGPU最核心、最动人的部分了——使用Shader(着色器)。
我们都知道Shader有两种,Vertex Shader和Fragment Shader。说句题外话,过去的显卡硬件对这两种着色器的实现是不同的物理路径,分别叫顶点处理单元和片段处理单元,而现在的显卡则将两者合二为一,统称为“流处理单元”,用以实现更高效的GPU着色操作。回到我们的话题。我们的GPU计算,就是通过Shader来实现,也就是我们自己编写着色器来改变默认的显卡渲染管线。
那么,总的计算流程是什么呢,这就是本文的核心内容了:
1.初始化环境,比如glut、glew等,然后创建一个合适的渲染视口,通常使用gluOrtho2D()创建一个二维的,大小和纹理大小相同,这样能够很好的一一对应,后面的渲染工作也会很简便。
2.生成并绑定一个FBO,然渲染工作(也就是GPU计算工作)在后台运行,不输出显示图形:
GLuint fb;
glGenFramebuffersEXT(1,&fb);
glBindFramebufferEXT(GL_FRAMEBUFFER_EXT,fb);
3.创建输入数据(就是源数据)纹理,分配显存,赋初值,并设置:
GLuint texture_source;
glGenTextures(1,&texture_source);
glBindTexture(GL_TEXTURE_RECTANGLE_ARB, texture_source);
//初始化纹理
glTexParameteri(GL_TEXTURE_RECTANGLE_ARB,GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_RECTANGLE_ARB,GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_RECTANGLE_ARB,GL_TEXTURE_WRAP_S, GL_CLAMP);
glTexParameteri(GL_TEXTURE_RECTANGLE_ARB,GL_TEXTURE_WRAP_T, GL_CLAMP);
//分配显存空间,并将纹理中的数据初始化为0(由最后一个参数确定)
glTexImage2D(GL_TEXTURE_RECTANGLE_ARB,0,GL_RGBA32F_ARB,texSize,texSize,0,GL_RGBA,GL_FLOAT,0);
4.将源数据传输到texture_source中,具体方案是(这里介绍一种方法),先将这个纹理与FBO绑定,然后使用glDrawPixels()函数逐个绘制像素,于是源数据就“绘制”到这张源纹理中了:
//绑定FBO和纹理 glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT,GL_COLOR_ATTACHMENT0_EXT,GL_TEXTURE_RECTANGLE_ARB,texture_source,0);
//确定渲染目标位FBO中的第0个(由上一个函数确定)
glDrawBuffer(GL_COLOR_ATTACHMENT0_EXT);
//确定绘制原点
glRasterPos2i(0,0);
//逐个绘制像素,data是在计算机内存中存储的数组
glDrawPixels(texSize,texSize,GL_RGBA,GL_FLOAT,data);
至此,源数据便传送到了这张纹理中。如果有多个数据源和多个纹理,按照同样的方法传递数据。
5.创建输出数据纹理texture_result,同上
6.将输出纹理和FBO绑定,使渲染结果保存在这个输出纹理中,这里可以绑定多个纹理,用“GL_COLOR_ATTACHMENT0_EXT”中的那个数字进行区分,之后(第6步)则可以通过这个参数选择不同的纹理对象:
glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT,GL_COLOR_ATTACHMENT0_EXT,GL_TEXTURE_RECTANGLE_ARB,texture_result,0);
7.使用glDrawBuffer(GL_COLOR_ATTACHMENT0_EXT);确定渲染目标缓冲为FBO中的第0个(由上一个函数指定)
9.创建并编译Shader,这个具体就不在本文讨论范畴了。
10.使用glGetUniformLocationARB()获取Shader参数位置(或者理解为参数接口)
11.使用glUseProgram(programObject);激活Shader
12.向Shader传递参数:
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_RECTANGLE_ARB,texture_source);
glUniform1i(xParam, 0);
13.确定FBO的渲染纹理对象:glDrawBuffer(GL_COLOR_ATTACHMENT0_EXT);
14.绘制一个四边形,使整个渲染管线开始运作,从而实现计算,并将计算结果保存到输出数据纹理texture_result中。
15.下一步便可以从texture_result中读取数据,并以数组形式保存到计算机内存中:
glReadBuffer(GL_COLOR_ATTACHMENT0_EXT);
glReadPixels(0, 0, texSize, texSize,GL_RGBA,GL_FLOAT,result);
至此,一个基本的GPGPU流程就走完了。我们经历了将输入数据读入源纹理、使用Shader对源纹理进行操作、将结果渲染到与FBO绑定的一个输出纹理、将输出纹理中的数据读取并保存到传统数组中。