<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对应的PBO)
pboIndex = (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×1080分辨率
- UV分量:`GL_RG`格式,960×540分辨率(NV12 UV平面是Y的一半)
- **帧缓冲(FBO)**:
- 创建1280×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×1080)
- UV平面:半分辨率 (960×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. **零拷贝**:直接使用映射内存,避免额外复制