<think>我们使用OpenGL来将NV12格式转换为RGBA格式。NV12是一种YUV格式,其中Y分量是亮度,UV分量是色度,并且UV分量在水平方向和垂直方向上都进行了2:1的下采样(即每4个Y分量共享一组UV分量)。NV12的内存布局是:首先是所有Y分量,然后是交错的U和V分量(每一行先是U分量,然后是V分量,但注意UV是交错存储的,即U0,V0,U1,V1,...)。
在OpenGL中,我们可以使用着色器(Shader)来完成这种转换。由于NV12有两个平面(plane),我们可以使用两个纹理对象分别存储Y平面和UV平面(在OpenGL中,我们可以将UV平面视为一个包含两个分量的纹理,即R通道存U,G通道存V,但注意NV12的UV是交错存储的,所以实际上我们需要将UV平面视为一个包含两个分量的纹理,每个纹素包含U和V两个值,但注意在NV12中,UV平面是半宽的,即宽度是原图像的一半,高度也是原图像的一半?实际上,对于NV12,UV平面高度是原图像的一半,但宽度和原图像相同?不对,让我们澄清一下:
实际上,NV12的存储方式:
- 第一块:连续存储所有Y值,长度为width*height。
- 第二块:存储UV分量,交错存储,每两个字节存储一对UV(U在前,V在后)。这个平面的宽度为width,高度为height/2(因为每两个Y行共享一行UV行,所以UV平面有height/2行,每行有width个像素,但每个像素包含一对UV值,即一个U和一个V)。
因此,我们可以将Y平面视为一个宽度为width,高度为height的单一通道(亮度)纹理(GL_LUMINANCE格式)。
将UV平面视为一个宽度为width/2(?)不对,注意:在UV平面中,每个像素包含两个分量(U和V),并且这个平面的宽度实际上应该是width(因为每行有width个像素,每个像素包含一对UV?)但是注意,在UV平面中,每两个Y列共享一个UV对,所以实际上UV平面的宽度是width/2?不对,我们来看:
实际上,在NV12中,UV平面的存储方式是:每一行有width个字节(注意是字节),其中每两个字节代表一个UV对(U和V),而这一行UV数据对应原图像中两行Y(即上面一行和下面一行)以及同一行中的两个Y像素。因此,UV平面的宽度是width(以字节计),高度是height/2。但是,在纹理中,我们通常以纹素为单位。一个纹素可以包含多个分量。
因此,我们可以将UV平面视为一个宽度为width/2,高度为height/2的纹理,每个纹素包含两个字节(U和V)?但是注意,在OpenGL中,我们可以创建一个纹理,其内部格式为GL_RG8(每个纹素两个字节,分别对应R和G通道),然后将UV平面的数据上传,其中U值放在R通道,V值放在G通道。但是,UV平面的宽度是原图像宽度的一半,因为每两个Y像素共享一个UV对。所以,UV平面的纹理宽度=width/2,高度=height/2。
然而,注意NV12的UV平面存储是交错的,并且每一行有width个字节(即每行有width/2个UV对)。所以,我们可以将UV平面数据上传到一个格式为GL_RG,类型为GL_UNSIGNED_BYTE,宽度为width/2,高度为height/2的纹理。
但是,还有一种常见的做法是:将整个NV12数据放在一个纹理中(使用GL_LUMINANCE_ALPHA格式?),但这里我们有两个平面,所以用两个纹理更清晰。
步骤:
1. 创建两个纹理对象,一个用于Y平面,一个用于UV平面。
2. 将Y平面数据上传到第一个纹理,格式为GL_LUMINANCE(在OpenGL ES中,如果使用ES3.0,则可以使用GL_R8,然后红色通道代表Y值,在着色器中取r通道即可)。但为了兼容性,我们也可以使用GL_LUMINANCE格式(在OpenGL ES 2.0中可用)。
3. 将UV平面数据上传到第二个纹理,格式为GL_LUMINANCE_ALPHA(这样每个纹素包含两个分量,分别对应U和V)?但是注意,在NV12中,UV是交错存储的,所以我们可以将UV平面视为一个包含两个分量的纹理,但注意:在UV平面中,每两个字节是一个UV对(U0,V0,U1,V1,...),所以我们可以将纹理格式设置为GL_LUMINANCE_ALPHA,宽度为width/2,高度为height/2。这样,每个纹素对应一个UV对,其中Luminance分量对应U,Alpha分量对应V?但注意,GL_LUMINANCE_ALPHA在ES2.0中可用,它表示两个分量,第一个分量(亮度)对应R、G、B都是这个值,第二个分量(alpha)对应A。但在我们的着色器中,我们只需要分别取出U和V,所以我们可以用GL_LUMINANCE_ALPHA格式,然后在着色器中取纹理的r和a分量?不对,因为GL_LUMINANCE_ALPHA在着色器中会被当作一个双分量的纹理,我们可以用.g和.a?不对,实际上在着色器中,我们采样这个纹理,得到的是vec4,其中.r是亮度值(也就是U值),.a是alpha值(也就是V值)?但这样很奇怪,因为通常我们使用rgba。
在OpenGL ES 3.0中,我们可以使用更精确的格式:将Y纹理设置为GL_R8,然后使用一个通道;将UV纹理设置为GL_RG8,然后使用两个通道。这样在着色器中,我们可以分别采样Y纹理和UV纹理,然后通过公式将YUV转换为RGB。
考虑到兼容性(特别是OpenGL ES 2.0),我们使用GL_LUMINANCE和GL_LUMINANCE_ALPHA。
具体步骤:
1. 创建两个纹理对象:
- 纹理Y:格式为GL_LUMINANCE,宽=原图像宽,高=原图像高。
- 纹理UV:格式为GL_LUMINANCE_ALPHA,宽=原图像宽/2,高=原图像高/2。
2. 上传数据:
- 纹理Y:使用glTexImage2D,格式为GL_LUMINANCE,数据类型为GL_UNSIGNED_BYTE,数据指针指向NV12数据的开始位置(Y平面)。
- 纹理UV:同样使用glTexImage2D,格式为GL_LUMINANCE_ALPHA,数据类型为GL_UNSIGNED_BYTE,数据指针指向NV12数据中UV平面的开始位置(即Y平面之后)。
3. 编写着色器:
顶点着色器:将纹理坐标传递给片元着色器。
片元着色器:同时采样Y纹理和UV纹理,然后使用YUV到RGB的转换公式。
4. 注意:在片元着色器中,我们需要将采样到的Y、U、V值转换为RGB。同时,由于UV纹理的尺寸是Y纹理的一半,所以纹理坐标需要调整。在UV纹理采样时,纹理坐标应该是Y纹理坐标的一半(因为UV平面是下采样的)。但是,我们通常将全尺寸的纹理坐标传递给顶点着色器,然后在片元着色器中,对于UV纹理的采样,我们需要将纹理坐标乘以0.5(因为UV纹理的尺寸是Y纹理的一半)?不对,因为UV纹理的尺寸是Y纹理宽高的一半,所以纹理坐标在UV纹理上需要缩放。但是,注意:在同一个四边形上,我们使用相同的纹理坐标范围(0到1)。当我们使用相同的纹理坐标去采样UV纹理时,由于UV纹理的尺寸小一半,所以我们需要将纹理坐标乘以2?不对,实际上OpenGL纹理采样使用归一化坐标(0到1),所以同一个归一化坐标在Y纹理上对应一个纹素,在UV纹理上也对应一个纹素(但UV纹理的纹素密度是Y纹理的一半,所以同一个归一化坐标在UV纹理上会覆盖四个纹素?)。因此,我们需要调整:在UV纹理采样时,纹理坐标应该是v_texCoord * vec2(1.0, 0.5)?不对,因为UV纹理的高度只有Y纹理的一半,所以垂直方向需要缩放。而且,在UV纹理中,每一行对应原图像的两行。所以,正确的做法是:在片元着色器中,我们使用相同的纹理坐标采样Y纹理,然后对于UV纹理,我们使用v_texCoord * vec2(1.0, 0.5)?但这样垂直方向只取了一半,而且水平方向不变?不对,水平方向也要注意:UV纹理的宽度也是Y纹理的一半,所以应该是v_texCoord * vec2(0.5, 0.5)?这样也不对,因为归一化坐标是0~1,而UV纹理的尺寸是(width/2, height/2),所以当我们使用归一化坐标(s,t)去采样UV纹理时,实际上会采样到UV纹理的(s*(width/2), t*(height/2))位置,这正好对应原图像中(2*s*(width/2), 2*t*(height/2))的像素块(四个像素)?所以,我们只需要将Y纹理的坐标直接用于UV纹理的采样即可?不对,因为UV纹理的尺寸是Y纹理的一半,所以归一化坐标相同的情况下,UV纹理采样到的位置对应原图像中2x2的像素块。因此,我们不需要调整坐标,直接用相同的归一化坐标采样UV纹理,就会得到当前片元位置对应的UV值(注意,一个UV值覆盖2x2的Y像素)。所以,实际上,在同一个片元着色器中,我们使用相同的纹理坐标分别采样Y纹理和UV纹理,就会得到对应位置的Y值和UV值(因为UV纹理的尺寸小,所以会自动进行插值?但我们不希望插值,因为YUV转换需要精确的采样。所以,我们需要使用texelFetch?或者设置纹理过滤为GL_NEAREST)。
为了避免插值,我们可以:
- 将两个纹理的过滤方式设置为GL_NEAREST,这样在采样时就会取最近的纹素。
- 同时,在片元着色器中,我们使用texture2D函数(ES2.0)进行采样,但注意归一化坐标。
然而,在NV12中,每个UV对覆盖2x2的Y像素,所以对于原图像中任意一个像素,其对应的UV坐标应该是:
u = floor(x / 2) (在UV纹理中的坐标,以像素计)
v = floor(y / 2)
但在归一化坐标中,如果当前片元的纹理坐标为(s,t)(对应原图像中的位置),那么它在UV纹理中的归一化坐标应该是:
s_uv = s * (width) / (width/2) ? 不对,归一化坐标应该是:s_uv = s * (width) / (width/2) 这样会大于1,不对。
正确的计算:归一化坐标是相同的,因为UV纹理的尺寸是原图像的一半,所以同一个位置(s,t)在UV纹理中的归一化坐标是(s,t)(因为归一化坐标是0~1,而UV纹理的尺寸是原图像的一半,所以同一个物理位置在UV纹理中的归一化坐标是(s*width/(width/2))? 不对,归一化坐标的定义是:纹理的宽度方向从0到1,所以不管纹理实际尺寸如何,同一个归一化坐标s在Y纹理中对应s*width位置,在UV纹理中对应s*(width/2)位置。因此,如果我们希望UV纹理采样到和当前片元(在Y纹理中位置为(s,t))对应的UV值,那么我们需要将归一化坐标乘以2?不对,因为当前片元的位置(s,t)在Y纹理中对应一个像素,而该像素在UV纹理中对应的位置是(s/2, t/2)(因为UV纹理的尺寸是Y纹理的一半,所以同一个物理位置在UV纹理中需要除以2)。
所以,在片元着色器中,对于UV纹理,我们使用归一化坐标(s/2, t/2)?不对,因为归一化坐标s在UV纹理中会映射到s*(width/2)的位置,而当前片元在Y纹理中的位置是(s*width, t*height),我们希望它在UV纹理中对应的位置是(s*width/2, t*height/2)(因为一个UV对覆盖2x2的Y像素),所以归一化坐标应该是(s, t)(因为(s,t)在UV纹理中就会映射到(s*(width/2), t*(height/2)),而s*width/2正好是原图像中x方向位置s*width的一半,即对应2x2块的中心?不对,我们希望每个2x2块内的四个像素采样到同一个UV值。因此,我们只需要将当前纹理坐标直接用于UV纹理采样,但注意,在同一个2x2块内的四个像素,它们的纹理坐标(s,t)在UV纹理中都会映射到同一个纹素(因为UV纹理的纹素间距是2个像素)。所以,我们不需要调整坐标,直接用(s,t)去采样UV纹理,然后取出的UV值就是这个2x2块共享的UV。
但是,由于纹理过滤模式,如果我们设置为GL_NEAREST,那么对于每个片元,我们都会取到最近的UV纹素(即它所在的2x2块对应的UV纹素)。所以,这样是正确的。
因此,步骤:
- 设置两个纹理,Y纹理的宽高为(width, height),UV纹理的宽高为(width/2, height/2)。
- 设置两个纹理的过滤方式为GL_NEAREST,防止插值。
- 在片元着色器中,使用相同的纹理坐标分别采样Y纹理和UV纹理。
片元着色器代码示例(OpenGL ES 2.0):
```glsl
varying highp vec2 v_texCoord;
uniform sampler2D s_textureY; // Y纹理
uniform sampler2D s_textureUV; // UV纹理(格式为GL_LUMINANCE_ALPHA)
void main() {
// 采样Y分量
highp float y = texture2D(s_textureY, v_texCoord).r; // 如果是GL_LUMINANCE,则取r分量就是Y值(注意:GL_LUMINANCE纹理采样后,rgb三个分量都是相同的亮度值,所以取r即可)
// 采样UV纹理,得到U和V:注意,在GL_LUMINANCE_ALPHA格式中,纹理采样的结果:r分量是亮度(即U值),a分量是alpha(即V值)?不对,GL_LUMINANCE_ALPHA纹理采样后,返回的vec4的r=g=b=亮度值(即U值),a=alpha值(即V值)。但是,我们也可以这样理解:实际上我们上传的UV纹理,每个纹素包含两个字节,第一个字节是U,第二个字节是V。当我们用GL_LUMINANCE_ALPHA格式采样时,纹理的r通道对应U,a通道对应V。所以:
highp float u = texture2D(s_textureUV, v_texCoord).r - 0.5; // 因为U和V在0~1之间,我们需要转换到-0.5~0.5
highp float v = texture2D(s_textureUV, v_texCoord).a - 0.5;
// 注意:由于我们采样的是归一化坐标,并且UV纹理的尺寸是Y的一半,但这里我们直接使用相同的归一化坐标,因为过滤方式为NEAREST,所以会取到正确的UV值。
// YUV转RGB公式(这里使用BT.601标准)
highp float r = y + 1.402 * v;
highp float g = y - 0.344 * u - 0.714 * v;
highp float b = y + 1.772 * u;
gl_FragColor = vec4(r, g, b, 1.0);
}
```
但是,上面的公式中,我们假设YUV的取值范围是:Y在0~1,U和V在-0.5~0.5。实际上,从纹理中采样得到的Y、U、V值是在0~1范围内的(因为纹理是8位无符号整数,被归一化到0~1)。而标准的YUV(BT.601)的Y范围是16~235,UV范围是16~240(但是我们在转换时通常先归一化,然后减去偏移,再乘以系数)。另一种常见的做法是,将YUV值转换到0~255,然后使用整数公式。
在着色器中,我们可以使用如下公式(假设输入是8位,我们将其归一化到0~1,然后转换):
Y = (Y - 16/255.0) * (255.0/219.0); // 将Y映射到0~1
U = U - 128/255.0;
V = V - 128/255.0;
但是,为了效率,我们可以在着色器中直接使用归一化后的值,然后调整公式。
一个常用的公式(来自引用[3]的代码):
r = y + 1.402 * v
g = y - 0.344 * u - 0.714 * v
b = y + 1.772 * u
但是,这个公式假设Y、U、V已经是0~1范围,并且U和V已经减去了0.5(即128/255)?实际上,上面的公式中,v和u已经是减去0.5之后的值(即-0.5~0.5)。所以,我们在着色器里先减去0.5。
但是,注意:从纹理中采样得到的U和V值是在0~1之间,所以我们减去0.5,得到-0.5~0.5。
另外,Y值不需要偏移吗?在引用[3]的代码中,Y值减去了16(然后如果小于0就置0),然后乘以1192。在着色器中,我们可以这样处理Y:
y = (y - 16.0/255.0) * (255.0/219.0); // 将16~235映射到0~1
但很多情况下,为了简单,我们也可以不调整Y,而使用下面的公式(假设Y已经归一化到0~1,并且U和V也归一化到0~1,然后减去0.5):
r = y + 1.370705 * v; // 1.402 * 255/219 ≈ 1.370705? 不对,我们直接使用1.402
g = y - 0.337633 * u - 0.698001 * v;
b = y + 1.732446 * u;
但是,为了简单,我们可以使用标准公式(BT.601):
r = y + 1.13983 * (v - 0.5); // 注意,这里v是0~1,减去0.5变成-0.5~0.5
g = y - 0.39465 * (u - 0.5) - 0.58060 * (v - 0.5);
b = y + 2.03211 * (u - 0.5);
或者,我们可以在着色器外部对YUV进行归一化处理,但通常我们在着色器内部完成。
这里我们采用引用[3]中的公式(整数公式的浮点版本):
r = y + 1.402 * (v - 0.5);
g = y - 0.344 * (u - 0.5) - 0.714 * (v - 0.5);
b = y + 1.772 * (u - 0.5);
但是,注意:这个公式和引用[3]中的整数公式有差异。引用[3]中的整数公式是:
r = (1192 * y + 1634 * v) >> 10;
g = (1192 * y - 833 * v - 400 * u) >> 10;
b = (1192 * y + 2066 * u) >> 10;
我们将整数公式转换为浮点(除以255,并考虑偏移):
首先,y, u, v 是0~255的整数,但是注意在代码中,y = y - 16, u = u - 128, v = v - 128。
所以,归一化到0~1的浮点数,并减去偏移后:
y = (y - 16/255.0) * (255.0/219.0); // 这样y的范围变为0~1
u = (u - 128/255.0) * (255.0/224.0); // 这样u的范围是-0.5~0.5(大约)
v = (v - 128/255.0) * (255.0/224.0);
但是,在整数公式中,并没有除以224,而是直接使用系数。所以,我们可以将整数公式转换为:
r = y * 1.164 + v * 1.596
g = y * 1.164 - u * 0.392 - v * 0.813
b = y * 1.164 + u * 2.017
这个系数是标准的BT.601系数(用于从YUV到RGB的转换,其中Y在16~235,UV在16~240)。
因此,更准确的公式是:
y = (y - 16.0/255.0) * (255.0/219.0); // 这一步可以省略,我们可以将系数整合
u = u - 128.0/255.0;
v = v - 128.0/255.0;
r = y * 1.164 + v * 1.596;
g = y * 1.164 - u * 0.392 - v * 0.813;
b = y * 1.164 + u * 2.017;
但是,为了简化计算,我们可以在着色器中这样写:
float y = texture2D(s_textureY, v_texCoord).r;
float u = texture2D(s_textureUV, v_texCoord).r - 0.5; // 因为GL_LUMINANCE_ALPHA纹理的r分量是U(即第一个字节),a分量是V(第二个字节)
float v = texture2D(s_textureUV, v_texCoord).a - 0.5;
// 调整Y的范围
y = (y - 16.0/255.0) * 1.164; // 1.164 = 255/219
r = y + 1.596 * v;
g = y - 0.392 * u - 0.813 * v;
b = y + 2.017 * u;
但是,注意:我们这里对U和V只减去了0.5(即128/255),但并没有缩放(即没有乘以255/224),所以严格来说,我们还需要对U和V进行缩放(乘以255/224)?或者我们可以将系数进行调整,将255/224乘到系数里。
为了避免复杂的系数调整,我们可以使用整数公式的浮点版本(即直接使用整数公式中的系数,但用浮点计算)。整数公式中的系数是已经考虑了归一化因子的。
因此,我们采用如下公式(来自引用[3]的整数公式,转换为浮点):
y = texture2D(s_textureY, v_texCoord).r * 255.0; // 将归一化的Y值转换到0~255
u = texture2D(s_textureUV, v_texCoord).r * 255.0; // 将归一化的U值转换到0~255
v = texture2D(s_textureUV, v_texCoord).a * 255.0; // 将归一化的V值转换到0~255
// 然后进行整数公式的计算
y = y - 16.0;
u = u - 128.0;
v = v - 128.0;
float r = (1192.0 * y + 1634.0 * v) / 1024.0; // 除以1024相当于右移10位
float g = (1192.0 * y - 833.0 * v - 400.0 * u) / 1024.0;
float b = (1192.0 * y + 2066.0 * u) / 1024.0;
// 然后将r,g,b转换到0~1
r = clamp(r, 0.0, 255.0) / 255.0;
g = clamp(g, 0.0, 255.0) / 255.0;
b = clamp(b, 0.0, 255.0) / 255.0;
但是,在着色器中进行这样的浮点运算效率较低,而且需要将0~1的值乘以255再计算,最后再除以255,这没有必要。
因此,我们采用第一种方法,即使用浮点系数:
y = texture2D(s_textureY, v_texCoord).r;
u = texture2D(s_textureUV, v_texCoord).r;
v = texture2D(s_textureUV, v_texCoord).a;
// 转换到YUV范围(16-235, 16-240)到0~1,并减去偏移
y = 1.164 * (y - 16.0/255.0);
u = u - 128.0/255.0;
v = v - 128.0/255.0;
float r = y + 1.596 * v;
float g = y - 0.392 * u - 0.813 * v;
float b = y + 2.017 * u;
// 由于计算后可能超出0~1,需要clamp
r = clamp(r, 0.0, 1.0);
g = clamp(g, 0.0, 1.0);
b = clamp(b, 0.0, 1.0);
这个公式中,系数1.164, 1.596等是标准系数。
但是,注意:在OpenGL ES中,我们可能希望避免过多的计算。我们可以预先计算好系数。
另外,我们也可以使用矩阵乘法,但这里我们直接写出。
最后,注意:我们也可以使用YUV到RGB的转换矩阵。
综上所述,我们选择第二种方法:使用浮点系数,并在着色器中进行计算。
但是,注意:在OpenGL ES 2.0中,精度很重要,我们应该使用中等精度(mediump)或高精度(highp)。这里我们使用highp。
因此,片元着色器代码(使用浮点系数):
```glsl
varying highp vec2 v_texCoord;
uniform sampler2D s_textureY;
uniform sampler2D s_textureUV;
void main() {
highp float y = texture2D(s_textureY, v_texCoord).r;
highp float u = texture2D(s_textureUV, v_texCoord).r;
highp float v = texture2D(s_textureUV, v_texCoord).a;
// 转换到YUV范围(16-235, 16-240)到0~1,并减去偏移
y = 1.164 * (y - 16.0/255.0);
u = u - 128.0/255.0;
v = v - 128.0/255.0;
highp float r = y + 1.596 * v;
highp float g = y - 0.392 * u - 0.813 * v;
highp float b = y + 2.017 * u;
gl_FragColor = vec4(clamp(r,0.0,1.0), clamp(g,0.0,1.0), clamp(b,0.0,1.0), 1.0);
}
```
但是,注意:上面的公式中,系数0.392, 0.813, 2.017等是近似值,更精确的系数是:
r = y + 1.596 * v;
g = y - 0.391 * u - 0.813 * v;
b = y + 2.018 * u;
或者,我们可以使用精确的系数(来自BT.601):
r = y + 1.402 * (v - 0.5); // 这里我们使用v-0.5,所以v已经减了0.5,所以1.402*v
// 但是上面我们并没有将u和v归一化到-0.5~0.5,而是减去了128/255,所以我们需要用1.402*(v-0.5)的等价形式?不对,我们上面已经将u和v减去了128/255(即0.5),所以u和v现在在-0.5~0.5之间(但实际范围是-0.5~0.5吗?因为u和v原本在0~1,减去0.5后变成-0.5~0.5,但注意,YUV的U和V分量范围是16~240,即0~255的中间部分,所以实际范围是-0.5~0.5的某个子集,但没关系,我们使用整个范围)。
因此,我们可以使用更简单的公式(即前面提到的第一种浮点公式):
r = y + 1.402 * v;
g = y - 0.34414 * u - 0.71414 * v;
b = y + 1.772 * u;
其中,y = (y - 16/255.0) * 1.164;
所以,最终片元着色器:
```glsl
varying highp vec2 v_texCoord;
uniform sampler2D s_textureY;
uniform sampler2D s_textureUV;
void main() {
highp float y = texture2D(s_textureY, v_texCoord).r;
highp float u = texture2D(s_textureUV, v_texCoord).r;
highp float v = texture2D(s_textureUV, v_texCoord).a;
// 调整Y
y = 1.164 * (y - 16.0/255.0);
u = u - 128.0/255.0;
v = v - 128.0/255.0;
highp float r = y + 1.596 * v;
highp float g = y - 0.391 * u - 0.813 * v;
highp float b = y + 2.018 * u;
gl_FragColor = vec4(clamp(r,0.0,1.0), clamp(g,0.0,1.0), clamp(b,0.0,1.0), 1.0);
}
```
或者,使用更常见的系数(1.403, 0.344, 0.714, 1.770):
r = y + 1.403 * v;
g = y - 0.344 * u - 0.714 * v;
b = y + 1.770 * u;
这个系数也是常见的,所以我们可以这样写:
r = y + 1.403 * v;
g = y - 0.344 * u - 0.714 * v;
b = y + 1.770 * u;
我们选择这个系数。
因此,最终片元着色器:
```glsl
varying highp vec2 v_texCoord;
uniform sampler2D s_textureY;
uniform sampler2D s_textureUV;
void main() {
highp float y = texture2D(s_textureY, v_texCoord).r;
highp float u = texture2D(s_textureUV, v_texCoord).r;
highp float v = texture2D(s_textureUV, v_texCoord).a;
// 调整Y
y = 1.164 * (y - 16.0/255.0);
u = u - 128.0/255.0;
v = v - 128.0/255.0;
highp float r = y + 1.403 * v;
highp float g = y - 0.344 * u - 0.714 * v;
highp float b = y + 1.770 * u;
gl_FragColor = vec4(r, g, b, 1.0);
}
```
注意:这里我们没有进行clamp,因为YUV范围通常不会导致RGB超出0~1,但为了安全,可以加上clamp。
另外,顶点着色器很简单,就是传递纹理坐标。
顶点着色器示例:
```glsl
attribute vec4 a_position;
attribute vec2 a_texCoord;
varying vec2 v_texCoord;
void main() {
gl_Position = a_position;
v_texCoord = a_texCoord;
}
```
在C代码中,我们需要:
- 创建两个纹理对象
- 设置纹理参数(过滤方式为GL_NEAREST,环绕方式为GL_CLAMP_TO_EDGE)
- 上传Y平面和UV平面的数据
- 创建着色器程序,编译链接着色器
- 绘制一个四边形,并传递纹理坐标
由于代码较长,这里只给出关键步骤。
创建纹理对象:
```c
GLuint textureY, textureUV;
glGenTextures(1, &textureY);
glGenTextures(1, &textureUV);
// 绑定并设置Y纹理
glBindTexture(GL_TEXTURE_2D, textureY);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
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, width, height, 0, GL_LUMINANCE, GL_UNSIGNED_BYTE, y_data);
// 绑定并设置UV纹理
glBindTexture(GL_TEXTURE_2D, textureUV);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
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_ALPHA, width/2, height/2, 0, GL_LUMINANCE_ALPHA, GL_UNSIGNED_BYTE, uv_data);
```
注意:y_data指向NV12数据的Y平面,uv_data指向NV12数据的UV平面(即y_data + width*height)。
在绘制时,我们需要激活两个纹理单元,并分别绑定Y纹理和UV纹理,然后设置uniform采样器。
由于OpenGL ES 2.0支持多个纹理单元,我们可以这样做:
```c
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, textureY);
glUniform1i(glGetUniformLocation(program, "s_textureY"), 0);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, textureUV);
glUniform1i(glGetUniformLocation(program, "s_textureUV"), 1);
```
然后绘制四边形。
注意:顶点坐标和纹理坐标需要正确设置。
以上就是在C语言中使用OpenGL将NV12转换为RGBA的方法。
注意:这种方法在OpenGL ES 2.0中有效,但需要保证GL_LUMINANCE和GL_LUMINANCE_ALPHA格式可用。在OpenGL ES 3.0中,这些格式可能被废弃,可以使用GL_R8和GL_RG8格式,并在着色器中用单分量和双分量采样。
另外,如果性能要求高,可以考虑使用片段着色器中的查找表或者使用GPU的YUV转换内置支持(如果可用),但这里我们使用通用方法。
最后,由于问题要求使用C语言和OpenGL,这里没有提供完整的代码,但关键步骤已经给出。
引用说明:
[^1]: 提到了使用Shader实现RGBA转NV21,但我们是NV12转RGBA,原理类似。
[^2]: 提到了使用OpenGL ES 3.0的texelFetch,但这里我们使用ES2.0的texture2D。
[^3]: 提供了YUV转RGB的整数公式,我们在着色器中采用了浮点版本的类似公式。
如果有任何问题,请参考引用[1][2][3]中的更多细节。
相关问题:
1. 如何用OpenGL将NV21转换为RGBA?
2. OpenGL ES 3.0中如何转换YUV格式?
3. 如何优化OpenGL中的YUV转RGB性能?</think>在C语言中使用OpenGL将NV12格式转换为RGBA格式,需要通过着色器程序实现YUV到RGB的色彩空间转换。以下是完整实现步骤和代码示例:
### 关键步骤
1. **创建纹理对象**:
- 两个纹理:Y平面(亮度)和UV平面(色度)
- Y纹理尺寸:原图像宽高
- UV纹理尺寸:宽=原宽/2,高=原高/2(因NV12色度下采样)
2. **设置纹理参数**:
```c
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);
```
3. **上传数据**:
```c
// Y平面 (单通道)
glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, width, height, 0,
GL_LUMINANCE, GL_UNSIGNED_BYTE, nv12_data);
// UV平面 (双通道)
glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE_ALPHA, width/2, height/2, 0,
GL_LUMINANCE_ALPHA, GL_UNSIGNED_BYTE, nv12_data + width*height);
```
4. **着色器程序**:
```glsl
// 顶点着色器
attribute vec4 aPosition;
attribute vec2 aTexCoord;
varying vec2 vTexCoord;
void main() {
gl_Position = aPosition;
vTexCoord = aTexCoord;
}
```
```glsl
// 片段着色器 (BT.601标准转换)
varying vec2 vTexCoord;
uniform sampler2D uTextureY;
uniform sampler2D uTextureUV;
void main() {
float y = texture2D(uTextureY, vTexCoord).r;
vec2 uv = texture2D(uTextureUV, vTexCoord).ra;
// YUV偏移处理
y = 1.164 * (y - 0.0625); // 16/255=0.0625
float u = uv.x - 0.5; // 128/255=0.5
float v = uv.y - 0.5;
// YUV转RGB
vec3 rgb;
rgb.r = y + 1.596 * v;
rgb.g = y - 0.813 * v - 0.391 * u;
rgb.b = y + 2.018 * u;
gl_FragColor = vec4(clamp(rgb, 0.0, 1.0), 1.0);
}
```
5. **绘制流程**:
```c
// 绑定纹理
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, textureY);
glUniform1i(glGetUniformLocation(program, "uTextureY"), 0);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, textureUV);
glUniform1i(glGetUniformLocation(program, "uTextureUV"), 1);
// 绘制全屏四边形
GLfloat vertices[] = { /* 顶点坐标 */ };
GLfloat texCoords[] = { /* 纹理坐标 */ };
glVertexAttribPointer(/* 设置属性指针 */);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
```
### 关键说明
1. **色彩空间转换**:
- 使用BT.601标准系数(电视标准)
- 公式推导:
$$ R = Y + 1.403V' $$
$$ G = Y - 0.344U' - 0.714V' $$
$$ B = Y + 1.770U' $$
- 需先进行值域调整:$Y' = 1.164(Y-16)$,$UV' = UV-128$[^3]
2. **性能优化**:
- 使用GL_LINEAR滤波可减少色度采样伪影
- 避免CPU端转换(比GPU慢5-8倍)
- 使用PBO异步传输数据
3. **兼容性**:
- OpenGL ES 2.0+ 支持
- 桌面OpenGL需移除ES限定符
- 替代方案:`GL_EXT_YUV_target`扩展(需硬件支持)
### 完整流程图
```mermaid
graph TD
A[NV12数据] --> B[创建Y纹理]
A --> C[创建UV纹理]
B --> D[着色器初始化]
C --> D
D --> E[绘制四边形]
E --> F[片段着色器转换]
F --> G[输出RGBA]
```
> **注意**:NV12的UV平面是交错的U/V分量,每4个Y像素共享1组UV值,因此UV纹理尺寸需减半[^1][^2]。
---
### 相关问题
1. 如何用OpenGL将NV21格式转为RGBA?
2. OpenGL ES 3.0中`texelFetch()`相比`texture2D()`有什么优势?
3. YUV420和NV12在内存布局上有何区别?
4. 如何实现RGBA到NV12的逆向转换?
5. OpenGL中多平面YUV格式(I420/YV12)如何处理?
[^1]: OpenGL 使用 Shader 实现 RGBA 转 NV21(附带源码)
[^2]: OpenGL ES 渲染 NV21、NV12、I420 等格式
[^3]: YUV格式转RGB格式的数学原理