Opengl ES之PBO

本文介绍了OpenGL ES中的PBO(Pixel Buffer Object),用于解决GPU和CPU间数据交换的效率问题。PBO允许直接内存访问显存,提高数据传输速度。文章详细阐述了PBO的概念、使用方式,包括双PBO的异步传输优势,并讨论了使用PBO时可能遇到的坑及兼容性问题。同时提供了示例代码展示PBO在纹理上传和下载中的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

叨叨一句几句

关于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交换数据图

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中读回像素数据。

  1. 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分配新的缓冲区。

  1. 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);
}

注释已经很详细了,就不多解析了,还是看不懂的私聊交流呗…

  1. Pbo下载纹理

所谓上传纹理是值将纹理数据从OpenGL中读取回CPU,与上传纹理一样,下载纹理也是需要先使用令牌绑定PBO才能使用,下载纹理使用的令牌是GL_PIXEL_PACK_BUFFER
下面的代码作用是将Opengl的渲染结果使用PBO读取出来:

// 单PBO读取测试
void PBOOpengl::readPixel(uint8_t **data,int *width,int *height) {

<think>我们使用OpenGL ES 3.0进行离屏渲染,将1080p的NV12格式视频帧转换为720p的RGBA,并使用双PBO(Pixel Buffer Object)来高效读取渲染后的图像数据,然后通过QImage显示。 步骤: 1. 初始化OpenGL ES 3.0离屏环境(使用EGL)。 2. 创建着色器程序,用于将NV12转换为RGBA并缩放到720p。 3. 创建纹理对象:一个用于NV12的Y分量,一个用于UV分量(注意NV12中UV是交错存储,且宽高各为Y的一半)。 4. 创建帧缓冲对象(FBO)并绑定一个720p的RGBA纹理作为颜色附件。 5. 创建两个PBO用于异步读取FBO中的像素数据(双缓冲)。 6. 加载.yuv文件(假设是NV12格式,即Y平面大小为1920x1080,UV平面为1920x1080/2,但存储为交错形式,所以UV平面大小为1920x540,每个UV平面像素包含两个字节:U和V)。 7. 渲染循环:将NV12数据上传到纹理,用FBO渲染到720p的纹理,然后通过PBO读取数据。 8. 将PBO中的数据映射到内存,创建QImage并显示。 注意:由于NV12的UV分量是交错存储,我们使用GL_LUMINANCE_ALPHA格式来存储UV分量(每个纹素两个字节:U和V)。 代码结构: - 初始化OpenGL ES 3.0上下文(离屏) - 编译着色器 - 设置顶点和纹理坐标(全屏四边形) - 创建纹理(Y和UV) - 创建FBO - 创建双PBO - 渲染循环:上传纹理,渲染到FBO,用PBO读取,显示 我们将创建一个类来封装这些功能。 由于我们使用离屏渲染,需要EGL创建离屏表面。另外,我们使用双PBO来交替读取,避免等待。 实现细节: 着色器:顶点着色器传递纹理坐标,片段着色器进行YUV到RGB的转换,并注意UV纹理坐标的缩放(因为UV平面是Y平面的一半)。 注意:NV12格式中,Y分量是完整的,UV分量是交错存储(每个UV纹素对应2x2的Y像素),所以UV纹理的宽度和高度都是Y的一半。 转换公式(BT.601): R = Y + 1.402*(V-128) G = Y - 0.34414*(U-128) - 0.71414*(V-128) B = Y + 1.772*(U-128) 但为了效率,我们可以在着色器中归一化到0-1,然后调整公式。 我们假设输入的YUV数据范围是0-255,在着色器中我们将其归一化,然后进行转换。 片段着色器代码(GLSL ES 3.0): precision mediump float; uniform sampler2D texY; uniform sampler2D texUV; in vec2 vTexCoord; out vec4 outColor; void main() { float y = texture(texY, vTexCoord).r; vec2 uv = texture(texUV, vTexCoord * 0.5).ra; // 注意:因为UV纹理尺寸是Y的一半,所以纹理坐标要乘以0.5 // 归一化到0-1,但注意YUV值域是16-235(视频范围)或0-255(全范围)。这里假设全范围0-255。 // 所以归一化:y = (y * 255.0) 但我们的纹理是GL_LUMINANCE,已经就是0-1的灰度值,即y值在0-1对应0-255。 // 同样,uv分量在0-1对应0-255。 // 转换公式(全范围): y = y * 255.0; float u = uv.x * 255.0 - 128.0; float v = uv.y * 255.0 - 128.0; float r = y + 1.402 * v; float g = y - 0.34414 * u - 0.71414 * v; float b = y + 1.772 * u; // 归一化到0-1 outColor = vec4(r/255.0, g/255.0, b/255.0, 1.0); } 但是,为了效率,我们可以避免在着色器中进行归一化,而是使用整数计算。另外,常见的做法是使用矩阵乘法,并且利用GPU的浮点性能。 另一种常见的做法(归一化到0-1): y = texture(texY, vTexCoord).r; // 0-1 对应 0-255 u = texture(texUV, vTexCoord*0.5).r; // 取U分量(在r通道)0-1对应0-255 v = texture(texUV, vTexCoord*0.5).a; // 取V分量(在a通道)?但是注意,我们使用GL_LUMINANCE_ALPHA,所以U在r通道,V在a通道?实际上,GL_LUMINANCE_ALPHA格式的纹理,一个纹素有两个分量:亮度(这里就是U)和Alpha(这里就是V)。所以我们可以这样: vec2 uv = texture(texUV, vTexCoord*0.5).rg; // 不对,GL_LUMINANCE_ALPHA的纹理采样返回的是(lum, lum, lum, alpha),所以我们需要用.r和.a?或者我们设置纹理为GL_RG格式?但OpenGL ES 3.0支持GL_RG格式。 但是,在OpenGL ES 3.0中,我们可以使用GL_R8和GL_RG8格式。不过,为了兼容性,我们使用GL_LUMINANCE_ALPHA,那么: U分量在纹理的R通道(即.r)就是亮度分量,V分量在纹理的A通道(即.a)?不对,GL_LUMINANCE_ALPHA的纹理,采样返回的vec4是 (L, L, L, A),所以: U = texture(texUV, vTexCoord*0.5).r; V = texture(texUV, vTexCoord*0.5).a; 但是,我们也可以使用GL_RG格式,这样每个纹素就是两个分量(R和G),然后我们分别存储U和V。这样更直观。 因此,我们修改UV纹理的格式为GL_RG,内部格式为GL_RG8,数据格式为GL_UNSIGNED_BYTE,每个像素两个字节:第一个字节是U,第二个字节是V。 所以,在创建UV纹理时: glTexImage2D(GL_TEXTURE_2D, 0, GL_RG, srcWidth/2, srcHeight/2, 0, GL_RG, GL_UNSIGNED_BYTE, nullptr); 然后在着色器中,我们可以这样: vec2 uv = texture(texUV, vTexCoord * 0.5).rg; 然后,我们使用以下转换公式(归一化到0-1,但转换公式调整为0-255范围): float y = texture(texY, vTexCoord).r; // 0-1,但实际值在0-1之间,代表0-255 vec2 uv = texture(texUV, vTexCoord*0.5).rg; // 同样,每个分量在0-1,代表0-255 // 转换公式(全范围): float r = y + 1.370705 * (uv.y - 0.5); float g = y - 0.337633 * (uv.x - 0.5) - 0.698001 * (uv.y - 0.5); float b = y + 1.732446 * (uv.x - 0.5); 或者,更常见的做法是减去0.5然后乘以系数,因为UV在0-1范围,0.5对应128。 我们使用以下公式(归一化后,128对应0.5): y = texture(texY, vTexCoord).r; u = texture(texUV, vTexCoord*0.5).r - 0.5; v = texture(texUV, vTexCoord*0.5).g - 0.5; r = y + 1.402 * v; g = y - 0.34414 * u - 0.71414 * v; b = y + 1.772 * u; 然后限制在0-1之间(使用clamp)。 因此,片段着色器可以这样写: precision mediump float; uniform sampler2D texY; uniform sampler2D texUV; in vec2 vTexCoord; out vec4 outColor; void main() { float y = texture(texY, vTexCoord).r; vec2 uv = texture(texUV, vTexCoord * 0.5).rg; float u = uv.r - 0.5; float v = uv.g - 0.5; float r = y + 1.402 * v; float g = y - 0.34414 * u - 0.71414 * v; float b = y + 1.772 * u; outColor = vec4(r, g, b, 1.0); // 由于YUV可能超出RGB范围,需要裁剪 outColor.rgb = clamp(outColor.rgb, 0.0, 1.0); } 但是,这个公式是BT.601的转换公式,适用于标清电视。如果是高清,则使用BT.709。 我们这里使用BT.601。 现在,我们开始写代码。 注意:由于我们使用离屏渲染,需要EGL。我们使用QOpenGLContext可能无法直接用于离屏,所以我们用EGL创建离屏上下文。 但是,如果我们使用Qt,也可以使用QOffscreenSurface,但这里为了直接使用EGL,我们使用EGL API。 由于代码较长,我们分步骤实现。 我们将创建一个类,比如叫做NV12Renderer。 由于需要读取.yuv文件,我们假设.yuv文件是NV12格式,一帧大小为1920*1080*1.5(因为NV12是每4个Y共用一组UV,所以总大小=1920*1080 + 1920*1080/2)。 在渲染循环中,我们读取一帧,然后渲染。 双PBO的使用: 两个PBO,在帧n使用PBO1读取,帧n+1使用PBO2读取,然后交替。 步骤: 1. 初始化EGL,创建上下文和离屏表面(Pbuffer)。 2. 初始化OpenGL ES 3.0函数(使用Qt的QOpenGLFunctions_3_0或者自己加载)。 3. 创建着色器,纹理,FBO,PBO。 4. 进入循环,每次读取一帧NV12数据。 - 将Y数据上传到Y纹理,UV数据上传到UV纹理(注意UV纹理的宽高是Y的一半)。 - 绑定FBO,设置视口为720p。 - 绘制全屏四边形(执行渲染)。 - 绑定PBO(当前PBO索引),然后调用glReadPixels将FBO的颜色附件读取到PBO中。 - 切换到下一个PBO。 - 在上一次读取的PBO(即非当前使用的那个)中,将数据映射到内存,创建QImage并显示。 - 注意:由于双缓冲,我们总是读取前一帧的PBO,而当前帧使用另一个PBO。 但是,为了简单,我们可以这样:在渲染第n帧时,我们使用PBO[n%2]来读取,然后在第n帧渲染结束后,我们映射PBO[(n-1)%2](即上一帧的)来获取数据。这样我们总是处理上一帧的数据,而当前帧使用另一个PBO。 然而,我们也可以这样:在渲染第n帧后,立即读取第n帧到PBO,然后下一帧再处理这个PBO。这样会有延迟,但我们可以使用双PBO来重叠读取和渲染。 我们采用:每次渲染后,立即使用一个PBO读取当前帧,然后交替使用两个PBO。然后,在下一帧渲染开始前,处理上一个PBO的数据(即刚读取完的)。 具体: 初始化:currentPBO = 0; 第1帧: 绑定PBO0进行读取(glReadPixels到PBO0) 然后currentPBO = (0+1)%2 = 1 第1帧结束后,我们处理PBO0(映射,创建QImage) 第2帧: 绑定PBO1进行读取 currentPBO = (1+1)%2 = 0 第2帧结束后,处理PBO1 注意:处理PBO0必须在第2帧渲染开始前完成?不行,因为第2帧渲染时,我们可能会修改FBO,所以我们需要在读取PBO0之前,确保第1帧已经完成渲染(glFinish?)。但是,使用PBO异步读取,我们可以通过glMapBufferRange来映射,如果数据还没读完,会阻塞。为了避免阻塞,我们可以使用双缓冲,并且确保在映射时,这个PBO已经读取完成(即上一帧的PBO已经读取完成)。 因此,我们在第n帧渲染时,同时处理第n-1帧的PBO数据(即currentPBO的另一个)。这样,第n帧渲染和第n-1帧的读取处理是并行的。 但是,为了简单,我们可以在第n帧渲染结束后,立即读取到PBO,然后马上处理上一个PBO(即n-1帧的),因为此时n-1帧的PBO已经读取完成了(因为我们在第n-1帧结束的时候发起了读取,现在第n帧结束,已经读取完了)。这样,处理PBO和渲染第n帧是并行的吗?不是,因为我们在第n帧结束后才发起新的读取,然后处理上一帧的PBO。所以实际上,我们处理的是n-1帧的数据。 步骤: 第0步:初始化两个PBO,currentIndex=0 第1帧: 渲染第1帧 绑定PBO0,发起glReadPixels(异步) currentIndex = (0+1)%2 = 1 然后,立即处理PBO1?不对,此时PBO1还没有数据(第一帧还没读),所以不能处理。 实际上,第一帧我们还没有可处理的PBO(因为之前没有读取过),所以跳过处理。 第2帧: 渲染第2帧 绑定PBO1,发起glReadPixels(异步) currentIndex = (1+1)%2 = 0 然后,处理PBO0(因为第1帧的读取到PBO0已经完成,因为我们在第1帧结束后发起的读取,现在第2帧结束,读取肯定完成了) 第3帧: 渲染第3帧 绑定PBO0,发起glReadPixels currentIndex = (0+1)%2 = 1 然后,处理PBO1(第2帧的数据) 所以,我们总是处理(currentIndex+1)%2的PBO,即上一帧使用的那个。 因此,在每一帧渲染结束后,我们: 1. 绑定下一个PBO(currentIndex)发起新的读取。 2. 然后,处理上一个PBO(即(currentIndex+1)%2)的数据(如果上一帧有数据的话,第一帧除外)。 3. 更新currentIndex = (currentIndex+1)%2 但是注意:在发起新的读取时,我们绑定的是currentIndex对应的PBO,然后读取后,我们更新currentIndex。然后处理的是更新后的currentIndex对应的上一个(即(currentIndex+1)%2)?不对,我们更新后,原来的currentIndex变成了下一帧要处理的,而当前要处理的是更新前的(currentIndex+1)%2?有点绕。 因此,我们改变策略:使用两个PBO,索引0和1。设置一个索引index,表示当前要用于读取的PBO索引。同时,我们记录下一帧要处理的PBO索引(即上一次读取的PBO索引)。 具体: 初始化:pboIndex = 0, lastPBOIndex = -1(表示没有可用的) 第一帧: 绑定PBO[pboIndex]进行读取(glReadPixels) 然后,设置lastPBOIndex = pboIndex(记录这一帧的读取会存到pboIndex对应的PBOpboIndex = (pboIndex+1)%2 // 为下一帧准备 然后,如果lastPBOIndex不为-1,则处理lastPBOIndex对应的PBO(但是第一帧时,lastPBOIndex还是-1,所以不处理) 然后,将lastPBOIndex设置为当前读取的索引(即更新前的pboIndex)?不对。 不如这样:在每一帧,我们首先处理上一次读取的PBO(即lastPBOIndex对应的),然后发起新的读取。 步骤: lastPBOIndex = -1; pboIndex = 0; 第一帧: 如果lastPBOIndex != -1,则处理lastPBOIndex对应的PBO(第一帧跳过) 绑定PBO[pboIndex]发起读取 lastPBOIndex = pboIndex // 记录这一帧的读取会存到这个PBO pboIndex = (pboIndex+1)%2 // 更新为下一个PBO 第二帧: 处理lastPBOIndex(即0)对应的PBO(第一帧的数据) 绑定PBO[pboIndex](即1)发起读取 lastPBOIndex = 1 pboIndex = (1+1)%2=0 第三帧: 处理lastPBOIndex(1)对应的PBO 绑定PBO0发起读取 lastPBOIndex=0 pboIndex=1 这样,我们在第n帧处理的是第n-1帧的数据(即上一帧的数据)。 因此,代码实现: // 渲染当前帧 renderFrame(); // 绑定FBO后渲染完成,现在读取像素 if (lastPBOIndex != -1) { // 处理上一个PBO(即lastPBOIndex)的数据 processPBO(lastPBOIndex); } // 绑定当前PBO进行读取 glBindBuffer(GL_PIXEL_PACK_BUFFER, pbo[pboIndex]); glReadPixels(0, 0, dstWidth, dstHeight, GL_RGBA, GL_UNSIGNED_BYTE, 0); // 更新索引 lastPBOIndex = pboIndex; pboIndex = (pboIndex+1)%2; // 解绑PBO glBindBuffer(GL_PIXEL_PACK_BUFFER, 0); 注意:在processPBO中,我们需要映射PBO,将数据复制出来,然后创建QImage。 但是,在processPBO中,我们需要确保这个PBO的异步读取已经完成。当我们映射PBO时,如果读取未完成,会阻塞直到完成。所以我们可以直接映射。 另外,我们使用QImage::Format_RGBA8888来创建图像。 现在,我们开始写代码。 由于代码较长,我们只写关键部分,并假设已经初始化了EGL。 注意:EGL的初始化步骤: EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY); eglInitialize(display, ...); // 选择配置 EGLConfig config; EGLint configAttribs[] = { EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, ... }; eglChooseConfig(display, configAttribs, &config, 1, ...); // 创建上下文 EGLContext context = eglCreateContext(display, config, EGL_NO_CONTEXT, contextAttribs); // 创建离屏表面(Pbuffer) EGLint pbufferAttribs[] = { EGL_WIDTH, width, EGL_HEIGHT, height, EGL_NONE }; EGLSurface surface = eglCreatePbufferSurface(display, config, pbufferAttribs); eglMakeCurrent(display, surface, surface, context); 然后,我们就可以使用OpenGL ES 3.0 API了。 我们假设源分辨率是1920x1080,目标分辨率是1280x720。 由于代码较长,我们分块实现。 注意:在Qt中显示QImage,我们可以在主线程中更新UI。 但是,离屏渲染可能在单独的线程,所以要注意跨线程的QImage显示(比如通过信号槽传递)。 下面,我们给出完整的代码框架(不包括文件读取和Qt显示部分,只包括OpenGL ES渲染和PBO读取)。 由于我们使用C++,并且需要Qt的QImage,所以包含Qt头文件。 注意:这个类可能运行在非GUI线程,所以不能直接操作Qt的UI组件,但可以创建QImage(QImage不依赖GUI线程)。 我们写一个类: 注意:由于OpenGL上下文是线程相关的,所以整个渲染过程必须在同一个线程中。</think>### OpenGL ES 3.0 + PBO双缓冲离屏渲染解决方案 以下是完整的实现方案,包含NV12转RGBA、1080p到720p缩放、PBO双缓冲读取和QImage显示的代码: ```cpp #include <QOpenGLFunctions_3_0> #include <QImage> #include <QOffscreenSurface> #include <QOpenGLContext> #include <QCoreApplication> #include <fstream> #include <chrono> class NV12Renderer : protected QOpenGLFunctions_3_0 { public: NV12Renderer(int srcW, int srcH, int dstW, int dstH) : srcWidth(srcW), srcHeight(srcH), dstWidth(dstW), dstHeight(dstH) { initialize(); } ~NV12Renderer() { cleanup(); } void renderFrame(const uint8_t* yData, const uint8_t* uvData) { // 1. 上传NV12数据 uploadNV12(yData, uvData); // 2. 绑定FBO并渲染 glBindFramebuffer(GL_FRAMEBUFFER, fbo); glViewport(0, 0, dstWidth, dstHeight); glUseProgram(program); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, texY); glActiveTexture(GL_TEXTURE1); glBindTexture(GL_TEXTURE_2D, texUV); glUniform1i(glGetUniformLocation(program, "texY"), 0); glUniform1i(glGetUniformLocation(program, "texUV"), 1); glBindVertexArray(vao); glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); // 3. 使用双PBO异步读取 readPixelsWithDoublePBO(); // 4. 清理状态 glBindFramebuffer(GL_FRAMEBUFFER, 0); } QImage retrieveImage() { if (lastReadyPBO == -1) return QImage(); // 绑定已就绪的PBO glBindBuffer(GL_PIXEL_PACK_BUFFER, pbo[lastReadyPBO]); // 映射PBO内存 GLubyte* pixels = static_cast<GLubyte*>( glMapBufferRange(GL_PIXEL_PACK_BUFFER, 0, dstWidth * dstHeight * 4, GL_MAP_READ_BIT)); if (pixels) { // 创建QImage(深拷贝) QImage img(pixels, dstWidth, dstHeight, QImage::Format_RGBA8888); QImage result = img.copy(); // 解除映射 glUnmapBuffer(GL_PIXEL_PACK_BUFFER); glBindBuffer(GL_PIXEL_PACK_BUFFER, 0); // 重置状态 lastReadyPBO = -1; return result; } glBindBuffer(GL_PIXEL_PACK_BUFFER, 0); return QImage(); } private: void initialize() { // 创建离屏OpenGL上下文 context = new QOpenGLContext(); context->setFormat(QSurfaceFormat::defaultFormat()); context->create(); surface = new QOffscreenSurface(); surface->setFormat(context->format()); surface->create(); if (!context->makeCurrent(surface)) { qFatal("无法创建OpenGL上下文"); } // 初始化OpenGL函数 initializeOpenGLFunctions(); // 初始化OpenGL资源 initShaders(); initTextures(); initFBO(); initPBOs(); initGeometry(); } void cleanup() { context->makeCurrent(surface); // 释放资源 glDeleteTextures(1, &texY); glDeleteTextures(1, &texUV); glDeleteTextures(1, &fboTexture); glDeleteFramebuffers(1, &fbo); glDeleteBuffers(2, pbo); glDeleteProgram(program); glDeleteVertexArrays(1, &vao); glDeleteBuffers(1, &vbo); context->doneCurrent(); delete surface; delete context; } void initShaders() { // 顶点着色器 const char* vsrc = R"( #version 300 es layout(location=0) in vec2 position; layout(location=1) in vec2 texCoord; out vec2 vTexCoord; void main() { gl_Position = vec4(position, 0.0, 1.0); vTexCoord = texCoord; } )"; // 片段着色器 (NV12转RGBA + BT.601转换) const char* fsrc = R"( #version 300 es precision mediump float; uniform sampler2D texY; uniform sampler2D texUV; in vec2 vTexCoord; out vec4 outColor; void main() { float y = texture(texY, vTexCoord).r; vec2 uv = texture(texUV, vTexCoord * 0.5).rg; // UV平面尺寸减半 // 去归一化 (0-255范围) y = y * 255.0; float u = uv.r * 255.0 - 128.0; float v = uv.g * 255.0 - 128.0; // BT.601转换矩阵 float r = clamp((y + 1.402 * v) / 255.0, 0.0, 1.0); float g = clamp((y - 0.34414 * u - 0.71414 * v) / 255.0, 0.0, 1.0); float b = clamp((y + 1.772 * u) / 255.0, 0.0, 1.0); outColor = vec4(r, g, b, 1.0); } )"; // 编译链接着色器 GLuint vshader = glCreateShader(GL_VERTEX_SHADER); glShaderSource(vshader, 1, &vsrc, nullptr); glCompileShader(vshader); GLuint fshader = glCreateShader(GL_FRAGMENT_SHADER); glShaderSource(fshader, 1, &fsrc, nullptr); glCompileShader(fshader); // 检查编译错误 GLint success; glGetShaderiv(vshader, GL_COMPILE_STATUS, &success); if (!success) { char log[512]; glGetShaderInfoLog(vshader, 512, nullptr, log); qFatal("顶点着色器编译失败: %s", log); } glGetShaderiv(fshader, GL_COMPILE_STATUS, &success); if (!success) { char log[512]; glGetShaderInfoLog(fshader, 512, nullptr, log); qFatal("片段着色器编译失败: %s", log); } program = glCreateProgram(); glAttachShader(program, vshader); glAttachShader(program, fshader); glLinkProgram(program); glGetProgramiv(program, GL_LINK_STATUS, &success); if (!success) { char log[512]; glGetProgramInfoLog(program, 512, nullptr, log); qFatal("着色器链接失败: %s", log); } glDeleteShader(vshader); glDeleteShader(fshader); } void initTextures() { // Y平面纹理 (全分辨率) glGenTextures(1, &texY); glBindTexture(GL_TEXTURE_2D, texY); 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_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, srcWidth, srcHeight, 0, GL_LUMINANCE, GL_UNSIGNED_BYTE, nullptr); // UV平面纹理 (半分辨率) glGenTextures(1, &texUV); glBindTexture(GL_TEXTURE_2D, texUV); 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_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glTexImage2D(GL_TEXTURE_2D, 0, GL_RG, srcWidth/2, srcHeight/2, 0, GL_RG, GL_UNSIGNED_BYTE, nullptr); } void initFBO() { // 创建FBO和颜色附件 glGenFramebuffers(1, &fbo); glBindFramebuffer(GL_FRAMEBUFFER, fbo); glGenTextures(1, &fboTexture); glBindTexture(GL_TEXTURE_2D, fboTexture); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, dstWidth, dstHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, fboTexture, 0); if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { qFatal("帧缓冲不完整!"); } glBindFramebuffer(GL_FRAMEBUFFER, 0); } void initPBOs() { // 双PBO实现异步读取 glGenBuffers(2, pbo); for (int i = 0; i < 2; ++i) { glBindBuffer(GL_PIXEL_PACK_BUFFER, pbo[i]); glBufferData(GL_PIXEL_PACK_BUFFER, dstWidth * dstHeight * 4, nullptr, GL_STREAM_READ); } glBindBuffer(GL_PIXEL_PACK_BUFFER, 0); currentPBO = 0; lastReadyPBO = -1; } void initGeometry() { // 全屏四边形顶点数据 float vertices[] = { // 位置 纹理坐标 -1, -1, 0, 1, // 左下 1, -1, 1, 1, // 右下 -1, 1, 0, 0, // 左上 1, 1, 1, 0 // 右上 }; glGenVertexArrays(1, &vao); glGenBuffers(1, &vbo); glBindVertexArray(vao); glBindBuffer(GL_ARRAY_BUFFER, vbo); glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); // 位置属性 glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)0); glEnableVertexAttribArray(0); // 纹理坐标属性 glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)(2 * sizeof(float))); glEnableVertexAttribArray(1); glBindVertexArray(0); } void uploadNV12(const uint8_t* yData, const uint8_t* uvData) { // 上传Y平面 glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, texY); glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, srcWidth, srcHeight, GL_LUMINANCE, GL_UNSIGNED_BYTE, yData); // 上传UV平面 (NV12格式,UV交错存储) glActiveTexture(GL_TEXTURE1); glBindTexture(GL_TEXTURE_2D, texUV); glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, srcWidth/2, srcHeight/2, GL_RG, GL_UNSIGNED_BYTE, uvData); } void readPixelsWithDoublePBO() { // 绑定当前PBO进行读取 glBindBuffer(GL_PIXEL_PACK_BUFFER, pbo[currentPBO]); glReadPixels(0, 0, dstWidth, dstHeight, GL_RGBA, GL_UNSIGNED_BYTE, 0); // 标记上一个PBO已就绪 lastReadyPBO = currentPBO; // 切换到下一个PBO currentPBO = (currentPBO + 1) % 2; // 解绑PBO glBindBuffer(GL_PIXEL_PACK_BUFFER, 0); } // 成员变量 int srcWidth, srcHeight; // 1080p (1920x1080) int dstWidth, dstHeight; // 720p (1280x720) GLuint texY, texUV; // NV12纹理 GLuint fbo, fboTexture; // 帧缓冲对象 GLuint pbo[2]; // 像素缓冲对象 (双缓冲) int currentPBO = 0; // 当前使用的PBO索引 int lastReadyPBO = -1; // 上次就绪的PBO索引 GLuint program; // 着色器程序 GLuint vao, vbo; // 几何数据 // OpenGL上下文 QOpenGLContext* context = nullptr; QOffscreenSurface* surface = nullptr; }; // 使用示例 int main(int argc, char *argv[]) { QCoreApplication app(argc, argv); // 初始化参数 (1080p -> 720p) const int srcW = 1920, srcH = 1080; const int dstW = 1280, dstH = 720; const size_t ySize = srcW * srcH; const size_t uvSize = srcW * srcH / 2; // UV平面是Y平面的一半 // 创建渲染器 NV12Renderer renderer(srcW, srcH, dstW, dstH); // 打开YUV文件 std::ifstream yuvFile("input.yuv", std::ios::binary); if (!yuvFile) { qFatal("无法打开YUV文件"); return -1; } // 分配缓冲区 std::vector<uint8_t> yData(ySize); std::vector<uint8_t> uvData(uvSize); while (!yuvFile.eof()) { // 读取Y分量 yuvFile.read(reinterpret_cast<char*>(yData.data()), ySize); if (yuvFile.gcount() != static_cast<int>(ySize)) break; // 读取UV分量 (NV12格式) yuvFile.read(reinterpret_cast<char*>(uvData.data()), uvSize); if (yuvFile.gcount() != static_cast<int>(uvSize)) break; // 执行渲染 auto start = std::chrono::high_resolution_clock::now(); renderer.renderFrame(yData.data(), uvData.data()); // 获取渲染结果 QImage result = renderer.retrieveImage(); if (!result.isNull()) { // 保存或显示图像 result.save(QString("frame_%1.png").arg(frameCount)); qDebug() << "Frame processed:" << frameCount; } auto end = std::chrono::high_resolution_clock::now(); auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start); qDebug() << "Frame processing time:" << duration.count() << "ms"; } return 0; } ``` ### 代码解释 #### 1. **OpenGL ES初始化** - 使用`QOffscreenSurface`和`QOpenGLContext`创建离屏渲染环境 - 初始化OpenGL ES 3.0函数 - 关键函数:`initializeOpenGLFunctions()`加载OpenGL API #### 2. **资源初始化** - **纹理**: - Y分量:`GL_LUMINANCE`格式,1920&times;1080分辨率 - UV分量:`GL_RG`格式,960&times;540分辨率(NV12 UV平面是Y的一半) - **帧缓冲(FBO)**: - 创建1280&times;720的RGBA纹理作为颜色附件 - 使用`glFramebufferTexture2D`绑定纹理到FBO - **PBO双缓冲**: - 两个PBO交替使用,实现异步读取 - `currentPBO`:当前用于读取的PBO索引 - `lastReadyPBO`:上次读取完成的PBO索引 #### 3. **渲染流程** 1. **数据上传**: ```cpp glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, srcWidth, srcHeight, GL_LUMINANCE, GL_UNSIGNED_BYTE, yData); glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, srcWidth/2, srcHeight/2, GL_RG, GL_UNSIGNED_BYTE, uvData); ``` 2. **离屏渲染**: - 绑定FBO,设置720p视口 - 执行绘制命令,触发着色器处理 3. **PBO读取**: ```cpp glBindBuffer(GL_PIXEL_PACK_BUFFER, pbo[currentPBO]); glReadPixels(0, 0, dstWidth, dstHeight, GL_RGBA, GL_UNSIGNED_BYTE, 0); lastReadyPBO = currentPBO; currentPBO = (currentPBO + 1) % 2; ``` 4. **图像获取**: ```cpp glBindBuffer(GL_PIXEL_PACK_BUFFER, pbo[lastReadyPBO]); GLubyte* pixels = glMapBufferRange(..., GL_MAP_READ_BIT); QImage img(pixels, dstWidth, dstHeight, QImage::Format_RGBA8888); QImage result = img.copy(); // 深拷贝 ``` #### 4. **着色器处理** - **顶点着色器**:传递位置和纹理坐标 - **片段着色器**: - 采样Y和UV纹理(UV纹理坐标缩放0.5倍) - BT.601标准YUV→RGB转换: ```glsl y = y * 255.0; u = uv.r * 255.0 - 128.0; v = uv.g * 255.0 - 128.0; r = (y + 1.402 * v) / 255.0; g = (y - 0.34414 * u - 0.71414 * v) / 255.0; b = (y + 1.772 * u) / 255.0; ``` - 使用`clamp()`确保颜色值在[0,1]范围内 #### 5. **性能优化** 1. **双PBO异步传输**: - PBO0和PBO1交替使用 - 渲染当前帧时读取上一帧结果 2. **纹理复用**:避免每帧重新分配纹理 3. **批量操作**:减少OpenGL状态切换 4. **离屏渲染**:不依赖窗口系统 ### 关键点说明 1. **NV12格式**: - Y平面:全分辨率 (1920&times;1080) - UV平面:半分辨率 (960&times;540),UV分量交错存储 - 内存布局:YYYYYYYY UVUVUVUV 2. **颜色空间转换**: - 使用BT.601标准(标清视频) - 如需BT.709(高清视频),调整转换系数: ```glsl // BT.709 系数 r = y + 1.5748 * v; g = y - 0.1873 * u - 0.4681 * v; b = y + 1.8556 * u; ``` 3. **PBO双缓冲**: - 避免CPU等待GPU,提升吞吐量 - 典型工作流程: ``` 帧1: 渲染 → 读取到PBO0 帧2: 处理PBO0 → 渲染 → 读取到PBO1 帧3: 处理PBO1 → 渲染 → 读取到PBO0 ``` 4. **QImage处理**: - 使用`QImage::Format_RGBA8888`匹配OpenGL格式 - **必须深拷贝**:`img.copy()`确保数据持久化 - 支持直接显示或保存为文件 ### 性能优化建议 1. **多线程渲染**:将渲染放在独立线程 2. **批量处理**:一次处理多帧数据 3. **纹理压缩**:使用ASTC等压缩格式 4. **异步映射**:使用`glMapBufferRange`的异步标志 5. **零拷贝**:直接使用映射内存,避免额外复制
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值