先来了解 YUV NV12 组成,再来实现 OpenGL ES 送显 YUV NV12。
一、YUV NV12YUV 是编译 true-color 颜色空间(color space)的种类,Y’UV、YUV、 YCbCr、YPbPr 等专有名词都可以称为 YUV,彼此有重叠。Y 表示明亮度(Luminance 或 Luma),也就是灰阶值,U 和 V 表示的则是色度(Chrominance 或 Chroma),作用是描述影像色彩及饱和度,用于指定像素的颜色。
YUV 格式分成两种:
紧缩格式(packedformats):将 Y、U、V 值存储 成 MacroPixels 数组,和 RGB 的存放方式类似;
平面格式(planarformats):将 Y、U、V 的三个分量分别存放在不同的矩阵中。
紧缩格式中的 YUV 是混合在一起的,对于 YUV 常见格式有 AYUV 格式(4:4:4 采样、打包格式);YUY2、UYVY(采样、打包格式),有UYVY、YUYV等。平面格式(planarformats)是指每 Y 分量,U 分量和 V 分量都是以独立的平面组织的,也就是说所有的 U 分量必须在 Y 分量后面,而 V 分量在所有的 U 分量后面,此一格式适用于采样(subsample)。平面格式(planarformat)有I420(4:2:0)、YV12、IYUV等。
现在重点来看 YV12 和 NV12。
YV12 中 Y、U、V 三个平面是分开的。而 NV12 中 Y 是一个平面,UV 共同组成另一个平面。
不难看出 NV12 中一组 UV 共用四个 Y,也就是上图中以相同颜色标识部分。
OpenGL ES 送显图像到 RGB 屏幕,需要将 NV12 中代表的每个像素转换为 RGB。用到了以下 YUV 到 RGB 的转换公式:
yuv.r = texture2D(yTexture, vTexCoord).r;
yuv.g = texture2D(uvTexture, vTexCoord).r - 0.5;
yuv.b = texture2D(uvTexture, vTexCoord).a - 0.5;
rgb = mat3(1.0, 1.0, 1.0,
0.0, 0.39465, 2.03211,
1.13983, -0.5806, 0.0) * yuv;
yuv.r 就是从纹理中拿到的 Y 分量,yuv.g 相应的是从纹理中拿到的 U 分量,yuv.b 就是 V 分量。
mat3 3x3 矩阵中的系数就是 YUV 转 RGB 公式内的转换系数。
OpenGL ES 送显 NV12 完整流程如下:
- 加载 vertex/fragment shaders;
- 创建 program object;
- 附着 shader object 到 program object;
- 绑定 vPosition 到 attribute 0;
- 链接 program object;
- 检查程序链接状态;
- 释放不在使用的 shader 资源;
- 存储 program object;
- 生成纹理 Y 和 UV;
- 激活渲染程序;
- 获取 shader 中的顶点变量;
- 传递顶点;
- 设置纹理层;
- 激活纹理;
- 绑定纹理;
- 设置纹理格式和大小;
- 三维绘制;
- 窗口显示。
具体代码如下:
#ifndef DISPLAYHANDLER_H #define DISPLAYHANDLER_H #include #include#include #include #include "logger.h" #define TEXTURE_NUM 2 class DisplayHandler { public: DisplayHandler(); bool initEGL(ANativeWindow *nwin); void deinitEGL(); GLint createProgram(); void setVideoWH(int width, int height); int getVideoWidth() const; int getVideoHeight() const; void update(unsigned char *yuvBuf); private: GLint initShader(const char *code, GLint type); GLuint loadProgram(const char *vShaderStr, const char *fShaderStr); EGLDisplay eglDisplay; EGLSurface eglSurface; EGLContext eglContext; int videoWidth; int videoHeight; GLuint mTextureID[TEXTURE_NUM]; GLuint glProgram; }; #endif //DISPLAYHANDLER_H
以下是具体 DisplayHandler 类实现。update 函数循环调用就可不断的刷新画面了,此处要注意的是 opengl es 使用的函数,需要都安排在一个线程内,也就是说加载 shader、创建 program 等这些步骤都在同一个线程内调用完成。关于 egl 送显部分可以参考《OpenGL ES 与原生窗口之间的接口——EGL》。
#include "DisplayHandler.h"
//加入三维顶点数据 两个三角形组成正方形
const float vers[] = {
1.0f, -1.0f, 0.0f,
-1.0f, -1.0f, 0.0f,
1.0f, 1.0f, 0.0f,
-1.0f, 1.0f, 0.0f,
};
//加入材质坐标数据
const float txts[] = {
1.0f, 0.0f,//右下
0.0f, 0.0f,
1.0f, 1.0f,
0.0f, 1.0f
};
#define GET_STR(x) #x
//顶点着色器 glsl
static const char *vertexShader = GET_STR(
attribute vec4 aPosition;//顶点坐标
attribute vec2 aTexCoord;//材质顶点坐标
varying vec2 vTexCoord;//输出的材质坐标 输出给片元着色器
void main() {
vTexCoord = vec2(aTexCoord.x, 1.0 - aTexCoord.y);
gl_Position = aPosition;//显示顶点
}
);
//片元着色器 NV12
//
// glsl
static const char *fragYUV = GET_STR(
precision mediump float;//精度
varying vec2 vTexCoord;//顶点着色器传递的坐标
uniform sampler2D yTexture;//输入材质参数(不透明灰度,单像素)
uniform sampler2D uvTexture;//输入材质参数
void main() {
vec3 yuv;
vec3 rgb;
yuv.r = texture2D(yTexture, vTexCoord).r;
yuv.g = texture2D(uvTexture, vTexCoord).r - 0.5;
yuv.b = texture2D(uvTexture, vTexCoord).a - 0.5;
rgb = mat3(1.0, 1.0, 1.0,
0.0, 0.39465, 2.03211,
1.13983, -0.5806, 0.0) * yuv;
//输出像素颜色
gl_FragColor = vec4(rgb, 1.0);
}
);
DisplayHandler::DisplayHandler() {
videoWidth = 1920;
videoHeight = 1080;
glProgram = 0;
eglSurface = nullptr;
eglContext = nullptr;
eglDisplay = nullptr;
}
bool DisplayHandler::initEGL(ANativeWindow *nwin) {
//EGL
//1 eglDisplay 显示
eglDisplay = eglGetDisplay(EGL_DEFAULT_DISPLAY);
if (eglDisplay == EGL_NO_DISPLAY) {
LOGE("get eglDisplay failed!");
return false;
}
//初始化 后面两个参数是版本号
if (EGL_TRUE != eglInitialize(eglDisplay, 0, 0)) {
LOGE("eglInitialize failed!");
return false;
}
//2 surface (关联原始窗口)
//surface 配置
//输出配置
EGLConfig config;
EGLint configNum;
//输入配置
EGLint configSpec[] = {
EGL_RED_SIZE, 8,
EGL_GREEN_SIZE, 8,
EGL_BLUE_SIZE, 8,
EGL_SURFACE_TYPE,
EGL_WINDOW_BIT,
EGL_NONE
};
if (EGL_TRUE != eglChooseConfig(eglDisplay, configSpec, &config, 1, &configNum)) {
LOGE("eglChooseConfig failed!");
return false;
}
//创建surface (关联原始窗口)
eglSurface = eglCreateWindowSurface(eglDisplay, config, nwin, 0);
if (eglSurface == EGL_NO_SURFACE) {
LOGE("eglCreateWindowSurface failed!");
return false;
}
//3 context 创建关联上下文
const EGLint ctxAttr[] = {
EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE
};
eglContext = eglCreateContext(eglDisplay, config, EGL_NO_CONTEXT, ctxAttr);
if (eglContext == EGL_NO_CONTEXT) {
LOGE("eglCreateContext failed!");
return false;
}
//egl 关联 openGL
if (EGL_TRUE != eglMakeCurrent(eglDisplay, eglSurface, eglSurface, eglContext)) {
LOGE("eglMakeCurrent failed!");
return false;
}
LOGI("EGL init success");
return true;
}
void DisplayHandler::deinitEGL() {
EGLBoolean success;
if (eglDisplay != nullptr && eglSurface != nullptr) {
success = eglDestroySurface(eglDisplay, eglSurface);
if (!success) {
LOGE("eglDestroySurface failure.");
}
eglSurface = nullptr;
}
if (eglDisplay != nullptr && eglContext != nullptr) {
success = eglDestroyContext(eglDisplay, eglContext);
if (!success) {
LOGE("eglDestroyContext failure.");
}
eglContext = nullptr;
success = eglTerminate(eglDisplay);
if (!success) {
LOGE("eglTerminate failure.");
}
eglDisplay = nullptr;
}
if (glProgram != 0) {
glDeleteProgram(glProgram);
glProgram = 0;
}
}
//初始化着色器
GLint DisplayHandler::initShader(const char *code, GLint type) {
GLuint shader;
GLint compiled;
// Create an empty shader object, which maintain the source code strings that define a shader
shader = glCreateShader(type);
if (shader == 0) {
return 0;
}
// Replaces the source code in a shader object
glShaderSource(shader, 1, &code, nullptr);
// Compile the shader object
glCompileShader(shader);
// Check the shader object compile status
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
if (!compiled) {
GLint infoLen = 0;
glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &infoLen);
if (infoLen > 1) {
GLchar *infoLog = (GLchar *) malloc(sizeof(GLchar) * infoLen);
glGetShaderInfoLog(shader, infoLen, nullptr, infoLog);
LOGE("Error compiling shader:n%sn", infoLog);
free(infoLog);
}
glDeleteShader(shader);
return 0;
}
return shader;
}
GLuint DisplayHandler::loadProgram(const char *vShaderStr, const char *fShaderStr) {
GLuint vertexShader;
GLuint fragmentShader;
GLuint programObject;
GLint linked;
// Load the vertex/fragment shaders
vertexShader = initShader(vShaderStr, GL_VERTEX_SHADER);
fragmentShader = initShader(fShaderStr, GL_FRAGMENT_SHADER);
// Create the program object
programObject = glCreateProgram();
if (programObject == 0) {
return 0;
}
// Attaches a shader object to a program object
glAttachShader(programObject, vertexShader);
glAttachShader(programObject, fragmentShader);
// Bind vPosition to attribute 0
glBindAttribLocation(programObject, 0, "vPosition");
// link the program object
gllinkProgram(programObject);
// Check the link status
glGetProgramiv(programObject, GL_link_STATUS, &linked);
if (!linked) {
GLint infoLen = 0;
glGetProgramiv(programObject, GL_INFO_LOG_LENGTH, &infoLen);
if (infoLen > 1) {
GLchar *infoLog = (GLchar *) malloc(sizeof(GLchar) * infoLen);
glGetProgramInfoLog(programObject, infoLen, NULL, infoLog);
LOGE("Error linking program:n%sn", infoLog);
free(infoLog);
}
glDeleteProgram(programObject);
return GL_FALSE;
}
// Free no longer needed shader resources
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
return programObject;
}
GLint DisplayHandler::createProgram() {
GLuint programObject;
// Load the shaders and get a linked program object
programObject = loadProgram((const char *) vertexShader,
(const char *) fragYUV);
if (programObject == 0) {
return GL_FALSE;
}
// Store the program object
glProgram = programObject;
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glGenTextures(TEXTURE_NUM, mTextureID);
for (int i = 0; i < TEXTURE_NUM; i++) {
glBindTexture(GL_TEXTURE_2D, mTextureID[i]);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_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);
}
//激活渲染程序
glUseProgram(glProgram);
//获取shader中的顶点变量
GLuint apos = (GLuint) glGetAttribLocation(glProgram, "aPosition");
glEnableVertexAttribArray(apos);
//传递顶点
glVertexAttribPointer(apos, 3, GL_FLOAT, GL_FALSE, 12, vers);
GLuint atex = (GLuint) glGetAttribLocation(glProgram, "aTexCoord");
glEnableVertexAttribArray(atex);
glVertexAttribPointer(atex, 2, GL_FLOAT, GL_FLOAT, 8, txts);
//设置纹理层
glUniform1i(glGetUniformLocation(glProgram, "yTexture"), 0);//对于纹理第1层
glUniform1i(glGetUniformLocation(glProgram, "uvTexture"), 1);//对于纹理第2层
return 0;
}
void DisplayHandler::setVideoWH(int width, int height) {
videoWidth = width;
videoHeight = height;
}
int DisplayHandler::getVideoWidth() const {
return videoWidth;
}
int DisplayHandler::getVideoHeight() const {
return videoHeight;
}
void DisplayHandler::update(unsigned char *yuvBuf) {
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, mTextureID[0]);//绑定纹理,下面的属性针对这个纹理设置
//设置纹理的格式和大小
glTexImage2D(GL_TEXTURE_2D,
0,//默认
GL_LUMINANCE,
videoWidth, videoHeight, //尺寸要是2的次方 拉伸到全屏
0,
GL_LUMINANCE,//数据的像素格式,要与上面一致
GL_UNSIGNED_BYTE,// 像素的数据类型
yuvBuf
);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, mTextureID[1]);
glTexImage2D(GL_TEXTURE_2D,
0,//默认
GL_LUMINANCE_ALPHA,
videoWidth / 2, videoHeight / 2, //尺寸要是2的次方 拉伸到全屏
0,
GL_LUMINANCE_ALPHA,//数据的像素格式,要与上面一致
GL_UNSIGNED_BYTE,// 像素的数据类型
yuvBuf + (videoWidth * videoHeight)
);
//三维绘制
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);//从0顶点开始 一共4个顶点
//窗口显示
eglSwapBuffers(eglDisplay, eglSurface);//交换buf
}



