<think>
我们需要使用OpenGL ES 3.0来实现一个离屏渲染的框架,具体流程如上图所示。主要步骤包括:
1. 创建一个自定义的FBO(离屏渲染)
2. 将渲染目标设置为FBO,并绑定一个RGBA纹理作为颜色附着
3. 使用YUV转换着色器将RGBA纹理转换为Y平面和UV平面(这里实际上是将RGBA图像转换为YUV格式,并分离出Y和UV分量)
4. 对UV平面进行下采样(因为UV通常是Y的一半,即4:2:0)
5. 将Y平面和经过下采样的UV平面组合成NV12格式的数据
6. 将NV12数据保存到文件
注意:在OpenGL ES中,我们通常使用两个纹理来存储Y和UV(或者U和V)数据。NV12格式中,Y是一个平面,而UV是交错存储的另一个平面(即U和V交替出现)。
步骤:
1. 初始化:创建程序、纹理、FBO等。
2. 离屏渲染:将场景渲染到FBO的RGBA纹理。
3. 将RGBA纹理转换为YUV:使用另一个着色器程序(包含YUV转换)将RGBA纹理转换为两个纹理:一个用于Y(亮度),一个用于UV(色度,但这里我们用一个纹理存储UV交错数据)。
4. 下采样UV:由于NV12要求UV分量在水平和垂直方向都是Y分量的一半,所以我们需要对UV纹理进行下采样(可以使用FBO和另一个着色器进行下采样,或者使用glGenerateMipmap然后取下一级,但注意NV12的UV是交错存储的,所以下采样需要特殊处理)。
5. 从纹理中读取数据:将Y纹理和下采样后的UV纹理读取到内存,然后组合成NV12格式。
6. 保存为NV12文件。
但是,由于OpenGL ES不支持直接渲染到NV12格式的纹理(NV12是一种特定格式,通常作为外部纹理),我们这里采用分别渲染到两个纹理(Y纹理和UV纹理),然后分别读取它们的数据,再组合成NV12。
另外,在转换步骤中,我们可能需要两个步骤:
第一步:将RGBA转换为YUV444(全分辨率的YUV),然后分离出Y和UV(此时UV是交错存储?还是分开?注意NV12中UV是交错的,所以我们需要生成一个包含U和V交错数据的纹理)。
第二步:对UV纹理进行下采样(因为NV12是4:2:0,所以UV的分辨率是Y的一半)。
因此,我们需要:
1. 一个FBO,附着RGBA纹理(用于原始渲染)。
2. 两个额外的FBO:一个用于Y平面(全分辨率),一个用于UV平面(全分辨率)。注意:这里我们将UV存储在一个纹理中,格式为RG(R通道存U,G通道存V)?但是NV12要求UV交错,所以我们可以使用一个纹理,其内部格式为GL_RG8,每个纹素包含U和V(即两个分量)。
3. 下采样UV:再使用一个FBO,附着另一个纹理(尺寸为原图的一半),然后使用一个简单的着色器(比如双线性采样)对UV纹理进行下采样。
但是,由于NV12的UV平面要求:每个像素位置存储一个U和一个V(交错),而我们下采样后的UV纹理是RG8格式,每个纹素包含一个U和一个V(分别对应R和G)。那么当我们读取这个下采样的UV纹理时,可以直接将其作为UV平面(每个字节一个U,紧接着一个V,然后下一组),这样就是NV12的UV部分。
具体步骤:
1. 创建FBO1,附着纹理tex1(RGBA8),用于离屏渲染场景。
2. 创建FBO2,附着两个颜色附着:texY(R8,用于存储Y分量)和texUV(RG8,用于存储UV分量,全分辨率)。注意:FBO2需要两个颜色附着,所以我们要使用多目标渲染(MRT)。在OpenGL ES 3.0中,我们可以声明多个out变量,然后使用glDrawBuffers指定多个颜色附着。
3. 使用一个着色器程序(YUV转换程序),将tex1(RGBA)转换为Y和UV(全分辨率),并分别存入texY和texUV。
4. 创建FBO3,附着纹理texUVHalf(RG8,宽度和高度为原图一半),然后将全分辨率的texUV下采样到texUVHalf(使用另一个着色器,简单的双线性过滤)。
5. 读取texY(全分辨率)和texUVHalf(半分辨率)的数据到内存,然后组合成NV12格式(先所有Y,然后UV交错排列)。
注意:由于NV12格式要求Y平面是8位单通道,UV平面是8位双通道(交错)但分辨率减半,所以:
- Y平面大小 = 宽度 * 高度
- UV平面大小 = (宽度 * 高度) / 2 (因为每个UV纹素对应原图的2x2区域,包含2个字节:U和V)
因此,NV12总大小 = 宽度 * 高度 * 3 / 2。
代码结构:
1. 初始化:创建并编译着色器,创建纹理和FBO。
2. 主循环(这里假设我们只渲染一帧):
a. 绑定FBO1,渲染场景(输出RGBA)。
b. 绑定FBO2(带两个颜色附着),使用YUV转换着色器,绘制一个全屏四边形(输入是FBO1的纹理),输出Y和UV(全分辨率)。
c. 绑定FBO3,使用下采样着色器(输入是FBO2的UV纹理),绘制一个全屏四边形(但视口设置为半分辨率),输出下采样后的UV纹理。
d. 从Y纹理(全分辨率)和UV纹理(半分辨率)读取数据。
e. 组合成NV12并保存文件。
由于代码较长,我们将分步骤实现。
注意:在OpenGL ES中,读取纹理数据使用glReadBuffer(指定要读取的附着)和glReadPixels。但是,由于我们有两个纹理(Y和UV),而且它们分别在不同的FBO中,我们需要分别读取。
另外,由于我们使用了多个FBO和多个纹理,需要小心管理状态。
以下是一个简化的实现,我们只关注核心步骤。
</think>
下面我将使用 OpenGL ES 3.0 实现您的离屏渲染框架。这个框架的核心是将渲染结果转换为 NV12 格式并保存到文件。
### 实现方案
```cpp
#include <GLES3/gl3.h>
#include <fstream>
// 纹理尺寸
const int WIDTH = 640;
const int HEIGHT = 480;
// 着色器代码
const char* vsSource = R"(
#version 300 es
layout(location=0) in vec4 position;
void main() {
gl_Position = position;
}
)";
const char* fsSourceRGBA = R"(
#version 300 es
precision mediump float;
out vec4 fragColor;
void main() {
fragColor = vec4(1.0, 0.0, 0.0, 1.0); // 红色测试图案
}
)";
const char* fsSourceYUV = R"(
#version 300 es
precision mediump float;
uniform sampler2D rgbaTex;
out vec2 yuvOut[2]; // 双输出目标
const mat3 rgb2yuv = mat3(
0.299, -0.169, 0.499,
0.587, -0.331, -0.418,
0.114, 0.499, -0.0813
);
void main() {
vec2 uv = gl_FragCoord.xy / vec2(float($W), float($H));
vec3 rgb = texture(rgbaTex, uv).rgb;
vec3 yuv = rgb2yuv * rgb;
// Y 平面输出 (全分辨率)
yuvOut[0] = vec2(yuv.r, 0.0);
// UV 平面输出 (半分辨率)
if (mod(gl_FragCoord.x, 2.0) < 1.0 && mod(gl_FragCoord.y, 2.0) < 1.0) {
yuvOut[1] = vec2(yuv.g, yuv.b);
}
}
)".replace("$W", std::to_string(WIDTH)).replace("$H", std::to_string(HEIGHT));
// 创建着色器程序
GLuint createProgram(const char* vs, const char* fs) {
GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertexShader, 1, &vs, nullptr);
glCompileShader(vertexShader);
GLuint fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fs, nullptr);
glCompileShader(fragmentShader);
GLuint program = glCreateProgram();
glAttachShader(program, vertexShader);
glAttachShader(program, fragmentShader);
glLinkProgram(program);
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
return program;
}
// 主渲染函数
void render() {
// 1. 创建离屏FBO和RGBA纹理
GLuint fbo, rgbaTex;
glGenFramebuffers(1, &fbo);
glGenTextures(1, &rgbaTex);
glBindTexture(GL_TEXTURE_2D, rgbaTex);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, WIDTH, HEIGHT, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, rgbaTex, 0);
// 2. 渲染到离屏FBO
GLuint programRGBA = createProgram(vsSource, fsSourceRGBA);
glUseProgram(programRGBA);
glViewport(0, 0, WIDTH, HEIGHT);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
// 3. 创建YUV转换FBO和纹理
GLuint yuvFBO;
glGenFramebuffers(1, &yuvFBO);
// Y平面纹理 (全分辨率)
GLuint yTex;
glGenTextures(1, &yTex);
glBindTexture(GL_TEXTURE_2D, yTex);
glTexImage2D(GL_TEXTURE_2D, 0, GL_R8, WIDTH, HEIGHT, 0, GL_RED, GL_UNSIGNED_BYTE, nullptr);
// UV平面纹理 (半分辨率)
GLuint uvTex;
glGenTextures(1, &uvTex);
glBindTexture(GL_TEXTURE_2D, uvTex);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RG8, WIDTH/2, HEIGHT/2, 0, GL_RG, GL_UNSIGNED_BYTE, nullptr);
// 绑定到FBO
glBindFramebuffer(GL_FRAMEBUFFER, yuvFBO);
GLenum buffers[] = {GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1};
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, yTex, 0);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT1, GL_TEXTURE_2D, uvTex, 0);
glDrawBuffers(2, buffers);
// 4. YUV转换
GLuint programYUV = createProgram(vsSource, fsSourceYUV);
glUseProgram(programYUV);
glUniform1i(glGetUniformLocation(programYUV, "rgbaTex"), 0);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, rgbaTex);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
// 5. 读取NV12数据
std::vector<uint8_t> nv12Data(WIDTH * HEIGHT * 3 / 2);
// 读取Y平面
glBindFramebuffer(GL_FRAMEBUFFER, yuvFBO);
glReadBuffer(GL_COLOR_ATTACHMENT0);
glReadPixels(0, 0, WIDTH, HEIGHT, GL_RED, GL_UNSIGNED_BYTE, nv12Data.data());
// 读取UV平面
glReadBuffer(GL_COLOR_ATTACHMENT1);
glReadPixels(0, 0, WIDTH/2, HEIGHT/2, GL_RG, GL_UNSIGNED_BYTE, nv12Data.data() + WIDTH * HEIGHT);
// 6. 保存为NV12文件
std::ofstream file("output.nv12", std::ios::binary);
file.write(reinterpret_cast<char*>(nv12Data.data()), nv12Data.size());
// 清理资源
glDeleteTextures(1, &rgbaTex);
glDeleteTextures(1, &yTex);
glDeleteTextures(1, &uvTex);
glDeleteFramebuffers(1, &fbo);
glDeleteFramebuffers(1, &yuvFBO);
glDeleteProgram(programRGBA);
glDeleteProgram(programYUV);
}
```
### 代码解释
#### 1. 离屏渲染 (FBO + RGBA纹理)
- 创建离屏FBO并附加RGBA8格式的颜色纹理
- 使用简单着色器渲染测试图案(红色)
#### 2. YUV转换
- **双输出FBO**:同时输出Y平面和UV平面
- **Y平面**:使用R8纹理格式(单通道),全分辨率
- **UV平面**:使用RG8纹理格式(双通道),半分辨率(宽高各减半)
- **RGB转YUV矩阵**:在片段着色器中进行颜色空间转换
- **下采样**:通过只处理偶像素实现UV分量2x下采样
#### 3. NV12数据生成
- **数据布局**:
- Y平面:`WIDTH * HEIGHT` 字节
- UV平面:`(WIDTH * HEIGHT) / 2` 字节(U/V交替存储)
- **纹理读取**:
- 先读取Y平面到NV12数据起始位置
- 再读取UV平面到Y平面数据之后
#### 4. 关键优化点
- **多渲染目标(MRT)**:同时输出Y和UV平面
- **整数下采样**:在着色器中使用模运算实现2x下采样
- **带宽优化**:UV平面使用半分辨率纹理减少内存占用
#### 5. 文件输出
- 将内存中的NV12数据直接写入二进制文件
- 文件格式符合YUV420sp标准
### 注意事项
1. 需要处理OpenGL ES上下文初始化
2. 顶点数据未展示(需提供全屏四边形)
3. 实际使用时需添加错误检查
4. 颜色转换矩阵可根据标准调整(如BT.601/BT.709)
5. NV12文件可用FFmpeg验证:`ffplay -f rawvideo -pix_fmt nv12 -s 640x480 output.nv12`
这个方案完整实现了从离屏渲染到NV12文件生成的整个流程,充分利用了OpenGL ES 3.0的多渲染目标和纹理特性。