前言
游戏开发工作1年了,还是得抽时间学习图形学!前面学习了OpenGL基础、几何图形、着色器、纹理,也快忘得差不多了,慢慢拾起吧。缓冲区对象:存储尽在掌握。很强!使得应用程序更加快速灵活地进行渲染和移动数据。缓冲区对象是一个强大的概念,允许应用程序迅速方便地将数据从一个渲染管线移动到另一个渲染管线,以及从一个对象绑定到另一个对象。不仅可以把数据移动到格式的位置,还可以在无需CPU介入的情况下完成工作。缓冲区对象使我们获得了对象素的真正控制。
一、缓冲区
缓冲区有很多不同的用途,他们能够保存定点数据、像素数据、纹理数据、着色器处理的输入,或者不同着色器阶段的输出。缓冲区保存在GPU内存中
1、创建缓冲区
GLuint pixBuffObjs[1];
glGenBuffers(1, pixBuffObjs);
有了缓冲区之后,就可以将其名称进行绑定使用缓冲区了
glBindBuffer(GL_PIXEL_PACK_BUFFER, pixBuffObjs[0]);
参数1为绑定点,参数2为缓冲区名称。
用完了缓冲区,这个缓冲区需要被清除,就像所有其他OpenGL对象一样。
glDeleteBuffers(1, pixBuffObjs);
2、填充缓冲区
需要将合法的数据传递到缓冲区来使用它:
glGenBuffers(1, pixBuffObjs);
glBindBuffer(GL_PIXEL_PACK_BUFFER, pixBuffObjs[0]);
glBufferData(GL_PIXEL_PACK_BUFFER, pixelDataSize, pixelData, GL_DYNAMIC_COPY);
glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
在调用glBufferData之前,必须将使用的缓冲区进行绑定。参数1为绑定点类型;参数2为要上传的数据大小,以字节为单位;参数3是要上传的数据,也可以传NULL,只分配特定大小的缓冲区;参数4用来指定如何使用缓冲区。
在不确定缓冲区的用途时,使用GL_DYNAMIC_DRAW是比较安全的。当再次调用glBufferData会对缓冲区重新填充数据,也就是原来的数据会被清除。可以使用glBufferSubData对已经存在的缓冲区的一部分进行更新,而不会导致缓冲区其他部分无效。
3、像素缓冲区对象
在存储像素/纹理单元方面,像素缓冲区对象和纹理缓冲区对象非常相似。我们可以访问和填充像素缓冲区对象(缩写PBO)。只有在绑定到一个PBO缓冲区绑定点时,一个缓冲区才真正成为一个像素缓冲区对象。
1)GL_PIXEL_PACK_BUFFER
当一个像素缓冲区对象被绑定到这个目标上,任何读取像素的OpenGL操作都会从像素缓冲区对象中获得他们的数据。这些操作包括glReadPixels、glGetImage和glGetCompressedTexImage。这些操作会从一个帧缓冲区或纹理中抽取数据,并将他们读回到客户端内存中。当一个像素缓冲区对象被绑定到包装缓冲区是,像素数据在GPU内存中的像素缓冲区对象的任务就结束了,并不会下载到客户端。
2)GL_PIXEL_UNPACK_BUFFER
当一个像素缓冲区对象被绑定到这个目标上,任何绘制像素的OpenGL操作都会向一个绑定的像素缓冲区对象放入他们的数据。
glTexImage*D、glTexSubImage*D、glComPressedImage*D和glCompressedTexImage*D就是这样的操作。这些操作将数据和纹理从本地CPU内存中读取到帧缓冲区中。但是,当一个像素缓冲区对象作为解包缓冲区对被绑定时,会使读取操作GPU内存,而不是CPU内中的PBO。
意味着GPU不需要完成其他工作,只需对数据进行初始化,等待复制完成,然后继续运行就可以了。
- 流纹理更新——在每一帧中对一个纹理进行更新。像素缓冲区对象允许应用程序改变纹理数据,而不必先下载再重新上传。
- 渲染顶点数据——缓冲区对象是通用的数据存储,应用程序可以很容易地使用同一个缓冲区来达到不同的目的。
- 异步调用glReadPixel——应用程序经常需要从屏幕上剥离像素,执行一些操作,然后或者他们,或者使用它们重新绘制
像素缓冲区对象是一个很好的容器,可以暂时存储GPU本地像素数据,但是,在使用之前一定先为它们分配存储空间。
从缓冲区中读取像素数据
// Grab the screen pixels and copy into local memory
glReadPixels(0, 0, screenWidth, screenHeight, GL_RGB, GL_UNSIGNED_BYTE, pixelData);
这个函数从当前启用的读取缓冲区的特定位置获取像素,然后将他们复制到CPU内存中。当我们向客户端内存进行写入时,整个管线经常需要被清空,以保证所有会影响我们将要进行读取的绘制工作能够完成。这对性能是很大的冲击。所以,可以使用缓冲区对象来克服这种性能问题:
// First bind the PBO as the pack buffer, then read the pixels directly to the PBO
glBindBuffer(GL_PIXEL_PACK_BUFFER, pixBuffObjs[0]);
glReadPixels(0, 0, screenWidth, screenHeight, GL_RGB, GL_UNSIGNED_BYTE, NULL);
先将一个缓冲区对象绑定到GL_PIXEL_PACK_BUFFER上,再进行读取。这样就能够将像素重定向到GPU中的一个缓冲区中,避免了复制到客户端内存上可能带来的性能问题。
4、源码分析pix_buffs.cpp
每一帧循环利用像素缓冲区对象,实现运动模糊。
1)void SetupRC(void)
// 创建模糊纹理
glGenTextures(6, blurTextures);
//分配1个像素缓冲区对纹理和PBO进行初始化
pixelDataSize = screenWidth*screenHeight*3*sizeof(unsigned int); // XXX This should be unsigned byte
void* data = (void*)malloc(pixelDataSize);
memset(data, 0x00, pixelDataSize);
//将创建的模糊纹理绑定6个纹理单元,并初始化纹理数据
for (int i=0; i<6;i++)
{
glActiveTexture(GL_TEXTURE1+i);
glBindTexture(GL_TEXTURE_2D, blurTextures[i]);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, screenWidth, screenHeight, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
}
//创建、绑定像素缓冲区,并填充缓冲区
glGenBuffers(1, pixBuffObjs);
glBindBuffer(GL_PIXEL_PACK_BUFFER, pixBuffObjs[0]);
glBufferData(GL_PIXEL_PACK_BUFFER, pixelDataSize, pixelData, GL_DYNAMIC_COPY);
glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
2)void RenderScene(void) //刷新渲染场景
if(bUsePBOPath)
{
// 先将一个缓冲区对象绑定到GL_PIXEL_PACK_BUFFER上,再进行读取,避免了复制到客户端内存上可能带来的性能问题。
glBindBuffer(GL_PIXEL_PACK_BUFFER, pixBuffObjs[0]);
glReadPixels(0, 0, screenWidth, screenHeight, GL_RGB, GL_UNSIGNED_BYTE, NULL);
glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
// 将缓冲区对象绑定为解包缓冲区类型,将数据从数据缓冲区中读出到纹理中
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pixBuffObjs[0]);
// 对模糊图形设置为纹理单元的数据,每一帧都会刷新,循环使用上一帧的像素缓冲区中的纹理单元数据
glActiveTexture(GL_TEXTURE0+GetBlurTarget0() );
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB8, screenWidth, screenHeight, 0, GL_RGB, GL_UNSIGNED_BYTE, NULL);
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
}
else
{
// 获取屏幕的像素并复制到客户端内存
glReadPixels(0, 0, screenWidth, screenHeight, GL_RGB, GL_UNSIGNED_BYTE, pixelData);
// 将像素数据从客户端内存(CPU)读取到纹理中,同样每帧刷新
glActiveTexture(GL_TEXTURE0+GetBlurTarget0() );
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB8, screenWidth, screenHeight, 0, GL_RGB, GL_UNSIGNED_BYTE, pixelData);
}
// 使用模糊着色器和模糊纹理进行全屏绘制圆环
projectionMatrix.PushMatrix();
projectionMatrix.LoadIdentity();
projectionMatrix.LoadMatrix(orthoMatrix);
modelViewMatrix.PushMatrix();
modelViewMatrix.LoadIdentity();
glDisable(GL_DEPTH_TEST);
SetupBlurProg();
screenQuad.Draw();
glEnable(GL_DEPTH_TEST);
modelViewMatrix.PopMatrix();
projectionMatrix.PopMatrix();
// 移动到下一个纹理对象
AdvanceBlurTaget();
结论:使用PBO复制像素比从CPU内存复制像素数据,运行速度能够快6倍。
其中的模糊操作,是用片段着色器从所有6个纹理中进行采用并对结果进行平均。着色器用于对屏幕对其的四边形进行渲染,这些四边形是通过建立一个基于窗口宽度和高度的正模型视图投影矩阵进行设置的。正交矩阵创建一个变换,将坐标直接映射到屏幕空间。
5、缓冲区对象
缓冲区对象在文理上直接使用。一个纹理包含两个个主要组成部分:纹理采样状态和包含纹理值的数据缓冲区。纹理缓冲区也称为texBO或TBO。
首先,纹理缓冲区能够直接填充来自其他渲染结果的数据。他能够用来提供对片段着色器和顶点着色器的顶点数据访问。纹理缓冲区是作为普通的缓冲区来创建的,当他被绑定带一个纹理作者GL_TEXTURE_BUFFER绑定点时会成为真正的纹理缓冲区。
glBindBuffer(GL_TEXTURE_BUFFER, texBO[0]);
glBufferData(GL_TEXTURE_BUFFER, sizeof(float) *count, fileData , GL_STATIC_DRAW);
但是texBO必须绑定到一个纹理单元上才能真正变得有用。要将一个texBO绑定到一个纹理上,可以调用glTexBuffer,但是首先要确保要使用的纹理已经进行了绑定。
glActiveTexture(GL_TEXTURE1);
glBindBuffer(GL_TEXTURE_BUFFER, texBoTexture);
glTexBuffer(GL_TEXTURE_BUFFER, GL_R32F, texBO[0]);
虽然纹理缓冲区对象看起来和操作起来很想普通纹理,但是纹理缓冲区不能在着色器中用普通的采样器,即sampler1D和sampler2D进行访问。必须使用一个新的采样器,叫做samplerBuffer,使用texelFetch从纹理缓冲区进行读取。
下一篇继续学习帧缓冲区对象!