本系列文章为Learn OpenGL个人学习总结!
OpenGL入门(一)之认识OpenGL和创建Window
OpenGL入门(二)之渲染管线pipeline,VAO、VBO和EBO
OpenGL入门(三)之着色器Shader
OpenGL入门(四)之纹理Texture
OpenGL入门(五)之Matrix矩阵操作和坐标系统
OpenGL进阶(一)之帧缓冲FrameBuffer
OpenGL进阶(二)之像素缓冲PixelBuffer
说起PBO,不得不提一篇文章:
http://www.songho.ca/opengl/gl_pbo.html
本篇文章,更多的是对该文章的翻译和提炼总结!另外文章提到的PBO当时还是OpenGL中的一个拓展,在v2.1版本开始已经是核心功能了!
PBO(Pixel Buffer Object)非常类似VBO,以便于将顶点数据和像素数据存储到缓冲对象中,这种存储像素数据的缓冲区对象称为像素缓冲区对象 (PBO)。
另外,添加了2个额外的“目标”标志。这些标志协助绑定的PBO内存管理器(OpenGL驱动)决定缓冲对象存储的最好的位置:系统内存,共享内存或者显存。
另外,目标标志清楚的指出PBO绑定将用于2种操作:GL_PIXEL_PACK_BUFFER
用于将像素数据传送到PBO,或者GL_PIXEL_UNPACK_BUFFER
将PBO传输到像素数据。
举个例子,glReadPixels()
和glGetTexImage()
是“打包(pack)”像素操作, 而glDrawPixels()
, glTexImage2D()
和glTexSubImage2D()
是“解压(unpack)”操作.当一个PBO绑定到GL_PIXEL_PACK_BUFFER
标志上时,glReadPixels()
从OpenGL的帧缓冲(framebuffer)读取像素数据(pixel data),然后把数据画(打包pack)到PBO中。当一个PBO绑定到GL_PIXEL_UNPACK
标志上时,glDrawPixels()
从PBO读取(unpack)或复制像素数据到OpenGL帧缓冲(framebuffer)中。
PBO的主要优势是通过直接的内存访问(Direct Memory Access,DMA)而不用涉及到CPU周期就可以快速的将像素数据传输到或者传输出显卡。PBO的另一个优势是异步的直接内存访问传输,让我们比较下传统的纹理传输方法和使用像素缓冲对象。下面左图是用传统的方式从图像源(图像文件或者视频流)去加载图片数据.图像源数据首先加载到系统内存中,然后,通过glTexImage2D()
从系统内存里面拷贝到OpenGL纹理对象。这2个传输步骤(加载和拷贝)都是CPU执行的。
相反,在右侧图中,图像源可以直接加载到由 OpenGL 控制的 PBO 中。CPU 仍然涉及将图像源加载到 PBO,但不用于将像素数据从 PBO 传输到纹理对象。相反,GPU(OpenGL 驱动程序)管理将数据从 PBO 复制到纹理对象。这意味着 OpenGL 在不浪费 CPU 周期的情况下执行 DMA 传输操作。此外,OpenGL 可以安排异步 DMA 传输以供以后执行。因此,glTexImage2D() 立即返回,CPU 可以执行其他操作,而无需等待像素传输完成。
有两种主要的 PBO 方法可以提高像素数据传输的性能:流式纹理更新和从帧缓冲区异步回读。
开头提到PBO和VBO非常类似,不同的是多了两个额外标志GL_PIXEL_PACK_BUFFER
和GL_PIXEL_UNPACK_BUFFER
。GL_PIXEL_PACK_BUFFER
是用来将像素数据从OpenGL传输到你的应用的,而GL_PIXEL_UNPACK_BUFFER
是用来将像素数据从应用传输到OpenGL中的。OpenGL参考这些标志决定PBO的最佳内存空间,例如,用于上传(解包unpacking)纹理的视频内存,或用于读取(打包packing)帧缓冲区的系统内存。然后,这些目标标志只是用来提示而已。OpenGL驱动会为你决定合适的位置。
创建 PBO 需要 3 个步骤;
glGenBuffers()
生成一个新的缓冲区对象。glBindBuffer()
绑定缓冲区对象。glBufferData()
将像素数据复制到缓冲区对象。如果在 glBufferData()
中指定指向源数组的 NULL 指针,则 PBO 仅分配具有给定数据大小的内存空间。glBufferData()
的最后一个参数是 PBO 提供如何使用缓冲区对象的另一个性能提示。GL_STREAM_DRAW
用于流式纹理上传,GL_STREAM_READ
用于异步帧缓冲区回读。
PBO 提供了一种内存映射机制,将 OpenGL 控制的缓冲区对象映射到客户端的内存地址空间(GPU—>CPU)。因此,客户端可以使用glMapBuffer()
和glUnmapBuffer()
修改缓冲区对象的一部分或整个缓冲区。
void* glMapBuffer(GLenum target, GLenum access)
GLboolean glUnmapBuffer(GLenum target)
如果成功,glMapBuffer()
返回指向缓冲区对象的指针。否则返回 NULL。目标参数是GL_PIXEL_PACK_BUFFER
或 GL_PIXEL_UNPACK_BUFFER
。第二个参数,access指定如何处理映射的缓冲区;从 PBO 读取数据 (GL_READ_ONLY),将数据写入 PBO (GL_WRITE_ONLY),或两者兼而有之 (GL_READ_WRITE)。
请注意,如果 GPU 仍在使用缓冲区对象,则glMapBuffer()
将不会返回,直到 GPU 完成其与相应缓冲区对象的工作。为了避免这种停顿(等待),请在glMapBuffer()
之前使用 NULL 指针调用 glBufferData()
。然后,OpenGL 会丢弃旧的缓冲区,并为缓冲区对象分配新的内存空间。
使用 PBO 后,必须使用 glUnmapBuffer()
取消映射缓冲区对象。如果成功,glUnmapBuffer()
返回 GL_TRUE。否则,它返回 GL_FALSE。
纹理源在 PBO 模式下的每一帧都直接写入映射的像素缓冲区。然后,使用 glTexSubImage2D() 将这些数据从 PBO 传输到纹理对象。通过使用 PBO,OpenGL 可以在 PBO 和纹理对象之间执行异步 DMA 传输。它显着提高了纹理上传性能。如果支持异步 DMA 传输,glTexSubImage2D() 应该立即返回,CPU 可以处理其他作业而无需等待实际的纹理复制。
为了最大限度地提高流传输性能,您可以使用多个像素缓冲区对象。上图显示同时使用了 2 个 PBO;glTexSubImage2D() 从一个 PBO 复制像素数据,同时将纹理源写入另一个 PBO。
对于第n帧,PBO 1用于 glTexSubImage2D() 并且PBO 2用于获取新的纹理源。对于第n+1帧,2 个像素缓冲区正在切换角色并继续更新纹理。由于异步 DMA 传输,可以同时执行更新和复制过程。CPU 将纹理源更新为 PBO,而 GPU 从另一个 PBO 复制纹理。
下边我们使用前边文章中的木箱纹理,来模拟一下两个PBO更新纹理的流程:
//加载图片
int width, height, nrChannels;
stbi_set_flip_vertically_on_load(true); // OpenGL要求y轴0.0坐标是在图片的底部的,但是图片的y轴0.0坐标通常在顶部。
//这里会拿到图像的宽高和颜色通道个数 3
unsigned char *data = stbi_load("../dependency/stb/container.jpg", &width, &height, &nrChannels, 0);
int data_size = width*height*3;
unsigned int pboIds[2]; // IDs of PBO
// create 2 pixel buffer objects, you need to delete them when program exits.
// glBufferData() with NULL pointer reserves only memory space.
// 第三个参数data传NULL,只保留内存空间
glGenBuffers(2, pboIds);
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pboIds[0]);
glBufferData(GL_PIXEL_UNPACK_BUFFER, data_size, 0, GL_STREAM_DRAW);
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pboIds[1]);
glBufferData(GL_PIXEL_UNPACK_BUFFER, data_size, 0, GL_STREAM_DRAW);
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
//生成纹理
unsigned int texture1;
glGenTextures(1, &texture1);
glBindTexture(GL_TEXTURE_2D, texture1);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
//data传0
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, 0);
//传统的直接上传纹理
// glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
glGenerateMipmap(GL_TEXTURE_2D);
....
//渲染
while (!glfwWindowShouldClose(window))
{
processInput(window);
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
static int index = 0;
int nextIndex = 0; // pbo index used for next frame
// In dual PBO mode, increment current index first then get the next index
index = (index + 1) % 2;
nextIndex = (index + 1) % 2;
// bind the texture and PBO
glBindTexture(GL_TEXTURE_2D, texture1);
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pboIds[index]);
/*________________________________*/
// ----start----
// copy pixels from PBO to texture object
// Use offset instead of ponter.
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width, height, GL_RGB, GL_UNSIGNED_BYTE, 0);
// bind PBO to update pixel values
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pboIds[nextIndex]);
// map the buffer object into client's memory
// Note that glMapBuffer() causes sync issue.
// If GPU is working with this buffer, glMapBuffer() will wait(stall)
// for GPU to finish its job. To avoid waiting (stall), you can call
// first glBufferData() with NULL pointer before glMapBuffer().
// If you do that, the previous data in PBO will be discarded and
// glMapBuffer() returns a new allocated pointer immediately
// even if GPU is still working with the previous data.
glBufferData(GL_PIXEL_UNPACK_BUFFER, data_size, 0, GL_STREAM_DRAW);
unsigned char *ptr = (unsigned char*)glMapBuffer(GL_PIXEL_UNPACK_BUFFER, GL_WRITE_ONLY);
if (ptr)
{
// update data directly on the mapped buffer
memcpy(ptr, data, data_size);
glUnmapBuffer(GL_PIXEL_UNPACK_BUFFER); // release pointer to mapping buffer
}
// ----end----
// it is good idea to release PBOs with ID 0 after use.
// Once bound with 0, all pixel operations behave normal ways.
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
....
}
上边几个地方需要注意:
glBufferData(GL_PIXEL_UNPACK_BUFFER, data_size, 0, GL_STREAM_DRAW);
中第三个参数传0,只保留内存空间glTexImage2D()
生成纹理时,最后一个参数data传0,同样是保留内存空间glTexSubImage2D()
即通知GPU将当前绑定的PBO中数据上传到纹理对象中。因为绑定了PBO,所以最后一个参数data也传0。glTexSubImage2D()
要求宽高和通道数都要与glTexImage2D()
中的相同!glBufferData(GL_PIXEL_UNPACK_BUFFER, data_size, 0, GL_STREAM_DRAW);
第三个参数data也传0。如果不用下边的内存映射,可以直接传图像数据data。传统的 glReadPixels()
会阻塞管道并等待所有像素数据传输完毕。然后,它将控制权返回给应用程序。相反,带有 PBO 的 glReadPixels()
可以安排异步 DMA 传输并立即返回而不会停顿。因此,应用程序(CPU)可以立即执行其他进程,同时通过 OpenGL(GPU)使用 DMA 传输数据。
上图使用 2 个像素缓冲区。在第 n帧,应用程序使用 glReadPixels()
将像素数据从 OpenGL 帧缓冲区读取到PBO 1 ,并处理PBO 2中的像素数据。这些读取和处理可以同时执行,因为对PBO 1的 glReadPixels()
会立即返回,并且 CPU 会立即开始处理PBO 2中的数据。而且,我们在每一帧上 交替使用PBO 1和PBO 2 。
关于FBO的内容,可以参考上一篇文章:OpenGL进阶(一)之帧缓冲FrameBuffer
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
//初始化
unsigned int pboIds[2]; // IDs of PBO
// create 2 pixel buffer objects, you need to delete them when program exits.
// glBufferData() with NULL pointer reserves only memory space.
// 第三个参数data传NULL,只保留内存空间
glGenBuffers(2, pboIds);
glBindBuffer(GL_PIXEL_PACK_BUFFER, pboIds[0]);
glBufferData(GL_PIXEL_PACK_BUFFER, data_size, 0, GL_STREAM_READ);
glBindBuffer(GL_PIXEL_PACK_BUFFER, pboIds[1]);
glBufferData(GL_PIXEL_PACK_BUFFER, data_size, 0, GL_STREAM_READ);
glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
...
首先创建没有什么好说的,跟上边上传纹是一样的,只不过类型修改一下GL_PIXEL_PACK_BUFFER
,GL_STREAM_READ
。
//绑定到第一个PBO
glBindBuffer(GL_PIXEL_PACK_BUFFER, mPboIds[index]);
//调用glReadPixels通知GPU把数据拷贝到第一个pbo里
glReadPixels(0, 0, width, height, GL_RGB, GL_UNSIGNED_BYTE, 0);
//绑定到第二个PBO
glBindBuffer(GL_PIXEL_PACK_BUFFER, mPboIds[mPboIdNewIndex]);
//映射内存,pbo->cpu
void *pixelsPtr = glMapBufferRange(GL_PIXEL_PACK_BUFFER, 0, data_size, GL_MAP_READ_BIT);
if (pixelsPtr) {
memcpy(data, static_cast<unsigned char *>(pixelsPtr), data_size);
glUnmapBuffer(GL_PIXEL_PACK_BUFFER);
}
glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
index = (index + 1) % 2;
nextIndex = (nextIndex + 1) % 2;
首先glReadPixels()
函数:
参数1,2:要从frame buffer中读取的第一个像素的坐标
参数3:指定像素数据的格式
参数4:指定像素数据的数据类型
参数5:具体的像素数据指针。这里使用了FBO,所以传0
然后,glMapBufferRange()
和上边glMapBuffer()
相同,使用内存映射,将PBO2中的数据读取复制出来!
最后交换索引!