叨叨一句几句
关于Opengl的系列已经有较长的一段时间没有更新了,然而这个系列还远没有到完毕地步,后续至少还有关于Opengl矩阵变换、YUV与RGB互转、Opengl水印贴图、Opengl转场动画等主题文章。
断更的主要原因如果给自己找个借口的话可以说是工作比价忙,如果说的比较现实一点就是自己懒且没啥动力,毕竟写技术博客文章是一件时间成本投入很大,而收益产出极小的一件事情…
进入正题…
了解过Opengl的童鞋们都知道,在Opengl中存在这个各种O,例如VAO、VBO、FBO等,而出现各种各样的O一般都是因为考虑到性能的原因。
今天我们要介绍的主角PBO,它和之前我们介绍VBO很像,了解完PBO之后童鞋们可以对比一下PBO与VBO的差异点。
下面从两个方面介绍PBO,什么是PBO以及如何使用PBO。
更多关于音视频、FFmpeg、Opengl、C++的原创文章请关注微信公众号:思想觉悟
什么是PBO
PBO(Pixel Buffer Object像素缓冲对象)。在了解什么是PBO之前,我们先来了解一下为什么会出现PBO这么一个东西?
所谓存在即合理,用发展的眼光看问题,PBO的出现肯定是为了替代某种东西,或者是为了解决某个问题。
在使用Opengl的时候经常需要在GPU和CPU之间传递数据,例如在使用Opengl将YUV数据转换成RGB数据时就需要先将YUV数据上传到GPU,一般使用函数glTexImage2D
,处理完毕后再将RGB结果数据读取到CPU,
这时使用函数glReadPixels
即可将数据取回。但是这两个函数都是比较缓慢的,特别是在数据量比较大的时候。PBO就是为了解决这个访问慢的问题而产生的。
PBO可以让我们通过一个内存指针,直接访问显存(GPU)的数据,我们将这块内存指针称作缓冲区。我们可以通过函数glMapBuffer
得到它的内存指针,然后就对这块缓冲区的数据可以为所欲为了。
例如使用函数glReadPixels
原本是要传一个内存指针进去的,但是有了缓冲区,它就可以把数据直接复制到缓冲区中去,而不是复制到内存条中去,这样就大大提高了数据的传递效率。
PBO的主要优点是通过直接内存访问的方式与显存进行快速像素数据传输,而无需占用CPU周期。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RtbEyIH4-1669172615917)(https://flyer-blog.oss-cn-shenzhen.aliyuncs.com/PBO%E6%95%B0%E6%8D%AE%E4%BA%A4%E6%8D%A2.png)]
可能看到这张图你没什么感觉,但是对比看看下面这张CPU与GPU直接传递数据的图你就会有所发现了:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KKZ6RDsF-1669172615917)(https://flyer-blog.oss-cn-shenzhen.aliyuncs.com/gpu%E5%B8%B8%E8%A7%84%E6%95%B0%E6%8D%AE%E4%BA%A4%E6%8D%A2.png)]
注意:PBO是OpenGL ES 3.0开始提供的一种方式,主要应用于从内存快速复制纹理到显存,或从显存复制像素数据到内存。
PBO的使用方式
既然PBO这么有效率,那么我们在什么情况下可能会用到PBO呢?有个常见的例子,例如我们在安卓上开发Camera应用录制视频时,如果需要用到x264进行软编码的话可能就会用到PBO,
首先我们将相机纹理图像送到Surface渲染显示,然后将Surface数据使用PBO的方式读取处理送到X264编码器中进行编码,当然在安卓上你也可以使用ImageReader…
下面我们来介绍下PBO的使用方式。
PBO的创建和初始化类似于VBO,但是在使用的时候需要用到GL_PIXEL_UNPACK_BUFFER
和GL_PIXEL_PACK_BUFFER
这两个令牌,其中GL_PIXEL_UNPACK_BUFFER
绑定表示该PBO用于将像素数据从程序(CPU)传送到OpenGL中;绑定为GL_PIXEL_PACK_BUFFER
表示该PBO用于从OpenGL中读回像素数据。
- Pbo创建
先上代码,跟着注释看
void PBOOpengl::initPbo() {
int imgByteSize = imageWidth * imageHeight * 4; // RGBA
glGenBuffers(1, &uploadPboId);
// 绑定pbo
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, uploadPboId);
// 设置pbo内存大小
// 这一步十分重要,第2个参数指定了这个缓冲区的大小,单位是字节,一定要注意
// 然后第3个参数是初始化用的数据,如果你传个内存指针进去,这个函数就会把你的
// 数据复制到缓冲区里,我们这里一开始并不需要什么数据,所以传个nullptr就行了
glBufferData(GL_PIXEL_UNPACK_BUFFER, imgByteSize, nullptr, GL_STREAM_DRAW);
// 解除绑定
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
glGenBuffers(1, &downloadPboId);
glBindBuffer(GL_PIXEL_PACK_BUFFER, downloadPboId);
glBufferData(GL_PIXEL_PACK_BUFFER, imgByteSize, nullptr, GL_STREAM_DRAW);
// 解除绑定
glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
LOGD("uploadPboId:%d---downloadPboId:%d---imgByteSize:%d", uploadPboId, downloadPboId,
imgByteSize);
}
上面的代码创建了两个PBO,其中uploadPboId
用于纹理上传,downloadPboId
用于纹理下载。创建好PBO之后然后使用两个PBO专用的令牌进行绑定,之后就调用glBufferData
给PBO分配缓冲区,当然,你也可以在使用的时候先进行绑定,然后重新调用glBufferData
分配新的缓冲区。
- Pbo上传纹理
所谓上传纹理是值将纹理数据从CPU传递到OpenGL,使用Pbo上传纹理时需要先使用令牌GL_PIXEL_UNPACK_BUFFER
绑定对应的PBO,然后才行使用PBO的缓冲区:
// 单个PBO测试
void PBOOpengl::setPixel(void *data, int width, int height, int length) {
LOGD("texture setPixel");
imageWidth = width;
imageHeight = height;
// Pbo初始化
initPbo();
glGenTextures(1, &imageTextureId);
// 激活纹理,注意以下这个两句是搭配的,glActiveTexture激活的是那个纹理,就设置的sampler2D是那个
// 默认是0,如果不是0的话,需要在onDraw的时候重新激活一下?
// glActiveTexture(GL_TEXTURE0);
// glUniform1i(textureSampler, 0);
// 例如,一样的
glActiveTexture(GL_TEXTURE2);
glUniform1i(textureSampler, 2);
// 本文首发于微信公总号号:思想觉悟
// 更多关于音视频、FFmpeg、Opengl、C++的原创文章请关注微信公众号:思想觉悟
// 绑定纹理
glBindTexture(GL_TEXTURE_2D, imageTextureId);
// 为当前绑定的纹理对象设置环绕、过滤方式
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);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
// pixels参数传递空,后面会通过pbo更新纹理数据
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
// 生成mip贴图
glGenerateMipmap(GL_TEXTURE_2D);
int dataSize = width * height * 4;
// 使用Pbo
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, uploadPboId);
// 将纹理数据拷贝进入缓冲区
GLubyte *bufPtr = (GLubyte *) glMapBufferRange(GL_PIXEL_UNPACK_BUFFER, 0,
dataSize,
GL_MAP_WRITE_BIT);
if (bufPtr) {
memcpy(bufPtr, data, static_cast<size_t>(dataSize));
glUnmapBuffer(GL_PIXEL_UNPACK_BUFFER);
}
// 将pbo缓冲区中的数据拷贝到纹理,调用 glTexSubImage2D 后立即返回,不影响 CPU 时钟周期
// 这个函数会判断 GL_PIXEL_UNPACK_BUFFER 这个地方有没有绑定一个缓冲区
// 如果有,就从这个缓冲区读取数据,而不是data参数指定的那个内存
// 这样glTexSubImage2D就会从我们的缓冲区中读取数据了
// 这里为什么要用glTexSubImage2D呢,因为如果用glTexImage2D,glTexImage2D会销毁纹理内存重新申请,glTexSubImage2D就仅仅只是更新纹理中的数据,这就提高了速度,并且优化了显存的利用率
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
// Pbo解除
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
// 解绑定
glBindTexture(GL_TEXTURE_2D, 0);
}
注释已经很详细了,就不多解析了,还是看不懂的私聊交流呗…
- Pbo下载纹理
所谓上传纹理是值将纹理数据从OpenGL中读取回CPU,与上传纹理一样,下载纹理也是需要先使用令牌绑定PBO才能使用,下载纹理使用的令牌是GL_PIXEL_PACK_BUFFER
。
下面的代码作用是