本篇文章主要描述如何使用OpenGL ES来渲染i420(YUV420P)和nv21(YUV420SP)
首先准备yuv数据文件,使用ffmpeg对图片进行格式转换
原图大小为800x480:
ffmpeg转化为nv21和i420格式的yuv文件
代码语言:javascript复制// convert to nv21
ffmpeg -i test.png -s 800x480 -pix_fmt nv21 test.yuv
// convert to i420
ffmpeg -i test.png -s 800x480 -pix_fmt yuv420p yuv420p.yuv
在OpenGL中,片元着色器最后输出的都是rgba的数据,所以使用OpenGL来渲染YUV数据的关键还是将YUV数据传递给着色器,并在着色器中将YUV转化为RGB
在我们创建一个2D纹理并使用glTexImage2D来填充数据的时候可以指定internalformat
代码语言:javascript复制public static native void glTexImage2D(
int target, // 目标纹理,此处必须为GL_TEXTURE_2D
int level, // 执行细节级别,0是最基本的图像级别,n表示第N级贴图细化级别
int internalformat, // 指定纹理中的颜色组件
int width, // 指定纹理图像的宽度,
int height, // 指定纹理图像的高度,
int border, // 指定边框的宽度。必须为0
int format, // 像素数据的颜色格式
int type, // 指定像素数据的数据类型
java.nio.Buffer pixels // 指定内存中指向图像数据的指针
);
internalformat这个参数指定纹理中的颜色组件,可选的值有GL_RGB,GL_RGBA,GL_LUMINANCE,GL_LUMINANCE_ALPHA 等
通常使用的GL_RGBA这种internalformat,它会单独保存R,G,B,A四个数据,而在渲染YUV数据的时候,我们使用GL_LUMINANCE和GL_LUMINANCE_ALPHA
使用GL_LUMINANCE的时候,可以将Y分量存储到像素的各个通道内,这样在着色器中,我们可以通过R,G,B任意一个分量来获取到Y值。U,V分量同理
使用GL_LUMINANCE_ALPHA的时候,首先存储亮度,然后是alpha值,利用这一点可以将U值存储到像素的A通道,V值存储到R,G,B通道
渲染i420
在使用GL渲染i420格式的YUV数据时,需要使用三个2D纹理,每个纹理的颜色组件采用GL_LUMINANCE
代码语言:javascript复制private fun textureLuminance(imageData: ByteBuffer, width: Int, height: Int, textureId: Int) {
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId)
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR)
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR)
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE)
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE)
GLES20.glTexImage2D(
GLES20.GL_TEXTURE_2D, 0,
GLES20.GL_LUMINANCE, width, height, 0,
GLES20.GL_LUMINANCE,
GLES20.GL_UNSIGNED_BYTE, imageData
)
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0)
}
首先把i420数据从文件中读取出来,然后创建3个2D纹理和buffer,并填充数据到buffer中,关键代码如下
代码语言:javascript复制// read i420 data
imageBytes = Util.read("yuv420p.yuv", context!!)
yBuffer = ByteBuffer.allocateDirect(imageWidth * imageHeight)
.order(ByteOrder.nativeOrder())
yBuffer.put(imageBytes, 0, imageWidth * imageHeight)
yBuffer.position(0)
// y texture
textureLuminance(yBuffer, imageWidth, imageHeight, yTextureId)
// u, v的流程是一样的,只是填充数据的时候要注意offset和纹理的宽高
uBuffer.put(imageBytes, imageWidth * imageHeight, imageWidth * imageHeight / 4)
textureLuminance(uBuffer, imageWidth / 2, imageHeight / 2, uTextureId)
vBuffer.put(imageBytes, imageWidth * imageHeight * 5 / 4, imageWidth * imageHeight / 4)
textureLuminance(vBuffer, imageWidth / 2, imageHeight / 2, vTextureId)
在渲染的时候,激活三个纹理单元并将纹理传递给着色器即可
在片元着色器中是如何从纹理中拿到Y,U,V分量的数据并且转化为R,G,B的呢?
从纹理中提取Y,U,V分量
代码语言:javascript复制// We had put the Y values of each pixel to the R, G, B components by GL_LUMINANCE,
// that's why we're pulling it from the R component, we could also use G or B
y = texture2D(yTexture, vTexCoord).r;
// u, v, same as above
u = texture2D(uTexture, vTexCoord).r;
v = texture2D(vTexture, vTexCoord).r;
YUV与RGB的互转公式
根据公式在片元着色器中进行YUV to RGB的转化
代码语言:javascript复制y = 1.164 * (y - 16.0 / 255.0);
u = u - 128.0 / 255.0;
v = v - 128.0 / 255.0;
r = y 1.596 * v;
g = y - 0.391 * u - 0.813 * v;
b = y 2.018 * u;
gl_FragColor = vec4(r, g, b, 1.0);
渲染nv21
在使用GL渲染nv21格式的YUV数据时,只需要使用两个2D纹理,Y分量纹理的颜色组件采用GL_LUMINANCE,UV分量纹理的颜色组件采用GL_LUMINANCE_ALPHA
代码语言:javascript复制private fun textureLuminanceAlpha(imageData: ByteBuffer, width: Int, height: Int, textureId: Int) {
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId)
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR)
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR)
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE)
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE)
GLES20.glTexImage2D(
GLES20.GL_TEXTURE_2D, 0,
GLES20.GL_LUMINANCE_ALPHA, width, height, 0,
GLES20.GL_LUMINANCE_ALPHA,
GLES20.GL_UNSIGNED_BYTE, imageData
)
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0)
}
从文件中读取nv21数据,创建纹理和buffer,填充数据到buffer的流程和渲染i420的步骤是类似的,此处就不再赘述了
和渲染i420的片元着色器中唯一不同的就是获取U分量是从a通道获取
代码语言:javascript复制// We had put the U and V values of each pixel to the A and R, G, B components of the
// texture respectively using GL_LUMINANCE_ALPHA. Since U, V bytes are interspread
// in the texture
u = texture2D(uvTexture, vTexCoord).a;
v = texture2D(uvTexture, vTexCoord).r;
DEMO
代码传送门
https://github.com/sifutang/opengl.git