本文记录在OpenGL中使用 帧缓冲 (FBO)和像素缓冲(PBO)来上行视频帧数据(YUYV), 并最终渲染显示出来。在之前的文章中介绍了渲染 YUV420 和 YUYV 的流程,不过都是用的默认的帧缓冲,即创建一个和视频幅面一样的 GLFW 窗口, 但是在实际开发中会遇到各种幅面的素材,,此时默认的 GLFW 帧缓冲就不满足要求了, 所以需要借助自定义的 FBO 来完成各种幅面视频帧的上行。
本文相关详细代码地址: https://github.com/pengguoqing/samples_code/tree/master/OpenGL
一、 帧缓冲(FBO)
关于帧缓冲, LearnOpenGL 官方网站已经介绍得比较详细了, 这里就不再赘述。上行视频帧数据主要就是用到帧缓冲的颜色缓冲。首先根据视频帧的幅面为帧缓冲创建一个同幅面的纹理附件,FBO 创建纹理附件和渲染缓冲附件的流程都大体相似,所以就直接封装了一个简单的 CXFBO的 C++ 类,只需要简单的设置纹理附件的宽高参数即可,使用方式如下:
CXFbo yuy2fbo;
yuy2fbo.InitFbo(imgwidth, imgheight);
yuy2fbo.UnBindFbo();
初始化的部分代码如下:
glGenTextures(1, &m_colortex);
glBindTexture(GL_TEXTURE_2D, m_colortex);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_GENERATE_MIPMAP, GL_TRUE);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, m_width, m_height, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_colortex, 0);
二、 像素缓冲(PBO)
关于PBO这篇文章有非常详细的介绍: http://www.songho.ca/opengl/gl_pbo.html, 这里贴出原文中一张能简单清晰的阐述 PBO 原理的示意图:
比如 CPU 解码出一帧 YUV 数据后, 如果使用 PBO上行的话那么CPU 只需要运算将 YUV 数据拷贝至 像素缓冲区这一步, 后续流程都由利用 GPU 强大的并行运行能力将像素缓冲区数据加载到GPU显存中。 与不使用 PBO相比, CPU 只需要进行一次拷贝即可, 否则整个过程都由 CPU 控制,时钟周期这一缺点就显露出来了。这里贴出封装的 PBO初始化流程和调用接口:
public:
bool Init(uint32_t width, uint32_t height, PBOTYPE type, GLenum pixfmt);
void Map(uint8_t** ptr, uint32_t* linesize) const;
void UnMap() const;
void Bind() const;
void UnBind() const;
bool CXPbo::Init(uint32_t width, uint32_t height, PBOTYPE type, GLenum pixfmt)
{
m_width = width;
m_hegit = height;
m_pixfmt = pixfmt;
switch (type)
{
case PBOTYPE::kDynamic:
m_rwtype = GL_PIXEL_UNPACK_BUFFER;
break;
case PBOTYPE::kStage:
m_rwtype = GL_PIXEL_PACK_BUFFER;
break;
default:
return false;
break;
}
glGenBuffers(1, &m_pbo);
if (!m_pbo)
{
return false;
}
glBindBuffer(m_rwtype, m_pbo);
m_pbosize = m_width * GetPixfmtBpp(pixfmt) / 8;
m_pbosize = (m_pbosize + 3) & 0xFFFFFFFC;
m_pbosize *= m_hegit;
glBufferData(m_rwtype, m_pbosize, nullptr, GL_STREAM_DRAW);
glBindBuffer(m_rwtype, 0);
return true;
}
三、 关于YUYV纹理的采样
在之前的文章中 OpenGL渲染YUYV, 因为默认的帧缓冲和实际的素材的幅面相同, 而且又是用的 RGBA 来存储一组完整的 YUYV 数据, 所以 shader 中使用 texture直接采样时会自动进行插值,这就会导致 YUV 不是 严格严格匹配的,正确的做法是需要使用 textuerLod来加载原始纹理,以保证YUV的严格匹配,shader 如下:
vec3 SampleYUYV(vec2 pos)
{
ivec2 size = textureSize(image, 0);
ivec2 actualpos = ivec2(pos.x, pos.y);
vec2 actualuv = (vec2(actualpos.xy)+0.5)/size;
vec4 yuyv = textureLod(image, actualuv, 0);
float leftover = fract(pos.x);
float y = (leftover<0.5f) ? yuyv.x : yuyv.z;
vec3 yuv = vec3(y, yuyv.yw);
return YUV_to_RGB(yuv);
}
四、 测试 Demo
首先准备一张 1280 * 960 幅面的 YUYV 数据,然后用 GLFW 创建一个 960 * 540 (16:9)的窗口用于显示。代码中使用宏来配置是否使用 PBO模式
#define USE_PBO true
渲染管线的 pass 如下:
while (!glfwWindowShouldClose(winhandle))
{
processInput(winhandle);
glViewport(0, 0, imgwidth, imgheight);
yuy2fbo.BindFbo();
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
uploadshader.use();
UploadYUYV(yuy2pbo, imgdata, uploadtex);
glBindVertexArray(uploadvao);
glBindTexture(GL_TEXTURE_2D, uploadtex);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
glBindTexture(GL_TEXTURE_2D, 0);
yuy2fbo.UnBindFbo();
glBindVertexArray(0);
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
glViewport(0, 0, wndwidth, wndheight);
rendershader.use();
glBindVertexArray(rendervao);
yuy2fbo.BindColorTexture();
glGenerateMipmap(GL_TEXTURE_2D);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
yuy2fbo.UnBindColorTexture();
glfwSwapBuffers(winhandle);
glfwPollEvents();
}
渲染结果如下:
使用 PBO后的纹理上行效率如下:
使用 glTexSubImage2D直接上行纹理的效率如下:
可以发现效率差别还是蛮大的。
五、 总结
① 使用 PBO进行纹理上行的效率要由于 glTexSubImage2D,但是测试的时候发现假如把 glMapBuffer的整个时间算上的话,两者其实是相差不多的。所以实际开发中可能需要 多个 PBO来交替上行纹理。
②渲染管线中存在多个 Pass 需要借助 帧缓冲来实现。