接下来准备实现光照贴图的打包和预计算了。因为想实现HDR,光照贴图准备存储为 RGBE或浮点格式。为了渲染浮点格式的光照贴图,就需要解决两个问题,一是如何让OpenGL能够真正地处理浮点格式的纹理,而不是把他们截断到 [0,1]区间内;二是如何将场景渲染到浮点格式的纹理中,以便对这个纹理进行Tone mapping 和Bloom等操作。
今天花了一晚上的时间在网上搜索资料,学习了Frame Buffer Object的用法。FBO是目前实现RTT和GPGPU算法最好的解决方案,因为它的接口设计相对合理,避免了显存到内存之间的数据交换。
FBO相当于各类帧缓冲,是一个渲染对象。在传统的渲染管线中,渲染后的数据输出到各种帧缓冲中,如颜色缓冲,深度缓冲等。有了FBO之后,就可以把数据渲染到已粘附到FBO的纹理中。稍后将会给出具体的代码,结合代码和分析你将很容易掌握FBO的用法。
现在先讨论一下OpenGL的浮点纹理。我们先来看一下OpenGL的glTexImage2D函数原型。
void glTexImage2D(
GLenum target,
GLint level,
GLint internalformat,
GLsizei width,
GLsizei height,
GLint border,
GLenum format,
GLenum type,
const GLvoid *pixels
);
以前,我们向纹理对象提供数据时是这样做的:
glTexImage2D(GL_TEXTURE_2D,0,GL_RGBA,width,height,0,GL_RGBA,GL_UNSIGNED_BYTE,pixels);
我们为Type参数提供的值是GL_UNSIGNED_BYTE表示我们的数据在内存中的存储格式为无符号字节型整数。现在我们的纹理格式为浮点型,我们会很自然地想到,能不能把上述函数调用改为:
glTexImage2D(GL_TEXTURE_2D,0,GL_RGBA,width,height,0,GL_RGBA,GL_FLOAT,pixels);
来让OpenGL接收浮点纹理呢?答案是不可以。使用GL_FLOAT调用该函数后,glTexImage确实能够接收浮点数据,但type参数表示的仅仅是象素数据在主内存中的存储格式,一个象素在显存里如何储存仅由 internalformat参数决定。这里internalformat如果设置为GL_RGBA,则表示象素存储为8位整数的RGBA格式。这样一来,当函数在接收数据时,会自动把浮点类型的象素数据截断到[0,255]的整数区间内,然后再放到显存中去,这就丢失了浮点数据。
GL_ARB_texture_float扩展解决了这个问题。这个扩展为我们提供了一些新的内部数据类型,包括GL_RGB16F_ARB, GL_RGBA16F_ARB, GL_RGB32F_ARB, GL_RGBA32F_ARB等。其中16F表示半精度浮点数。半精度浮点数占用两个字节,包括1位的符号位,5位的指数位(Exponent)和10位的尾数位(Mantissa)。要注意一些老的显卡可能不支持对GL_RGBA32F_ARB和GL_RGB32F_ARB等32位浮点纹理进行双线性纹理过滤(如我以前使用的GeForce 6200)。同时考虑到空间因素,目前主要还是使用16F的纹理。当我们把纹理内部数据类型指定为这些浮点类型时,OpenGL就不会再把我们提供的纹理数据截断到[0.0,1.0]区间上了。如果我们指定的是16F格式的纹理,OpenGL会自动将我们提供的32bit浮点数据转换为16Bit的数据格式。因此我们唯一要做的事情就是把函数调用改成这样以支持Floating Point纹理:
glTexImage2D(GL_TEXTURE_2D,0,GL_RGBA16F_ARB,width,height,0,GL_RGBA,GL_FLOAT,pixels);
好了,现在通过一段代码解释FBO的用法。下面的这段代码是我探索FBO时写的。这里贴出来解释一下。前面的初始化代码就省略了。
这个代码所作的事情就是,创建一个浮点纹理,然后使用这个浮点纹理向FBO绘制一个矩形,
再把FBO中的数据读回内存。
glEnable(GL_TEXTURE_2D);
// 首先我们创建一个纹理,然后用这个纹理渲染一个QUAD。
glGenTextures(1,&texid);
glBindTexture(GL_TEXTURE_2D,texid);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP);
随便创建一个浮点型的数组,代表我们的浮点纹理。
float *tex = new float[1024*4];
for (int i=0;i<32;i++)
for (int j=0; j<32;j++)
{
tex[i*32*4+j*4] = 1000.3f;
tex[i*32*4+j*4+1] = 1900.3f;
tex[i*32*4+j*4+2] = 1099.3f;
tex[i*32*4+j*4+3] = 1.0f;
}
//把纹理格式设置为半精度浮点,然后传入一个事先生成的浮点型数组。
glTexImage2D(GL_TEXTURE_2D,0,GL_RGBA16F_ARB,32,32,0,GL_RGBA,GL_FLOAT,tex);
if (glGetError()!=GL_NO_ERROR )
{
ui->Console->PrintString(HString("Error Occured."));
}
// 注意,HFBO是我自己封装的OpenGL函数库,其中fbo.xxx相当于glxxxEXT函数。
// 如fbo.GenFramebuffers就相当于glGenFramebuffersEXT。
HFBO fbo;
首先,创建一个帧缓冲对象:
GLuint fboid;
fbo.GenFramebuffers(1,&fboid);
然后绑定这个对象:
fbo.BindFramebuffer(GL_FRAMEBUFFER_EXT,fboid);
接着创建一个纹理,然后把这个纹理粘附到FBO中,这样渲染后的颜色缓冲就会被写入这个纹理:
GLuint texColorBuf;
glViewport(0,0,32,32);
glGenTextures(1,&texColorBuf);
glBindTexture(GL_TEXTURE_2D,texColorBuf);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP);
现在创建的是32Bit浮点纹理,指定相应的格式:
glTexImage2D(GL_TEXTURE_2D,0,GL_RGBA32F_ARB,32,32,0,GL_RGBA,GL_FLOAT,0);
接下来是重点。glFramebufferTexture2DEXT函数将一个纹理对象粘附到当前绑定的FBO中。
注意第二参数指定了帮定点。一个FBO可以有多个帮定点,这样数据可以同时写入到多个纹理对象中。
因为我们目前只有一个纹理对象,所以将绑定点设置为GL_COLOR_ATTACHMENT0_EXT。
fbo.FramebufferTexture2D(GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT0_EXT,
GL_TEXTURE_2D, texColorBuf, 0);
接下来glCheckFramebufferStatus函数检查当前的帧缓冲对象是否准备就绪。如果就绪,
函数返回GL_FRAMEBUFFER_COMPLETE_EXT。
if (fbo.CheckFramebufferStatus(GL_FRAMEBUFFER_EXT)!=GL_FRAMEBUFFER_COMPLETE_EXT)
PrintString("Frame buffer not ready.");
现在我们渲染场景。渲染后的颜色数据将写入到当前绑定的FBO对象中。如果不需要向FBO中写入数据,
而直接绘制到屏幕缓冲区,则调用glBindFramebuffer(GL_FRAMEBUFFER_EXT,0);以取消帧缓冲对象。
BeginUIDrawing();
glBindTexture(GL_TEXTURE_2D,texid);
glColor4ub(255,255,255,255);
glBegin(GL_QUADS);
glTexCoord2i(0,0);
glVertex2i(0,0);
glTexCoord2i(0,1);
glVertex2i(0,32);
glTexCoord2i(1,1);
glVertex2i(32,32);
glTexCoord2i(1,0);
glVertex2i(32,0);
glEnd();
EndUIDrawing();
现在我们创建一个浮点数组,然后试着从刚才已粘附到FBO中的纹理对象读取数据。
看看这些数据是否和我们的预期相符。
float *datar = new float[32*32*4];
glBindTexture(GL_TEXTURE_2D,texColorBuf);
glReadPixels(0,0,32,32,GL_RGBA,GL_FLOAT,datar);
glGetTexImage(GL_TEXTURE_2D,0,GL_RGBA,GL_FLOAT,datar);
如果你愿意,可以在此设置一个断点,观察一下datar中的值。
程序到此结束,执行清理。
glDeleteTextures(1,&texColorBuf);
glDeleteTextures(1,&texid);
fbo.DeleteFramebuffers(1,&fboid);
delete [] datar;
delete [] tex;
如果你还想了解更多关于FBO以及GPGPU的知识,可以参考这篇文章:GPU深度发掘, 作者:Rob 'phantom' Jones 译者:华文广。