视频学习笔记:Android OpenGL渲染YUV420P图像

Posted vonchenchen1

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了视频学习笔记:Android OpenGL渲染YUV420P图像相关的知识,希望对你有一定的参考价值。

##背景
android 开发中,当得到一张yuv图需要显示时,之前的做法是利用ffmpeg自带的方法将其转换为RGB565或者RGBA,然后将RGB数据拷贝到aNativeWindow的图像缓冲区,达到显示的目的。这样做比较耗CPU, 最近在阅读ijkplayer源码时,整理了一下OpenGL直接渲染YUV420P相关流程,参考网上一些代码,总结了一个最简单的小例子。

例子下载地址
http://download.csdn.net/detail/lidec/9880093

新增一个参考工程,目前在一些高通手机上多次创建会出现egl具柄无法释放的问题,需要进一步考证
https://gitee.com/vonchenchen/android_native_renderer

##流程
这里首先提出一个问题,对比之前将RGB数据拷贝到显示缓冲区,OpenGL最终也应该有一个存放最终RGB数据的缓冲区,那么我们如何拿到这个缓冲区中的内容并且将其拷贝到显示缓冲区呢?

这里需要用到EGL的相关接口。EGL是OpenGL ES和平台系统视窗之间的接口,我们可以通过它的Api建立OpenGL ES和显示窗口见的关系。OpenGL渲染管线中存储了我们设定的参数,调用渲染方法时会根据这些参数将内容渲染到指定的窗口,Android系统在java层提供了GLSurfaceView类,可以直接在其onDrawFrame方法中设置OpenGL相关命令并对其进行绘制。这样屏蔽了很多细节操作,但是也会是我们产生一些困惑,不知道RGB内存是如何被拷贝到显示缓冲区中的。同时也难以和直接将YUV转RGB并拷贝的那种流程所兼容。

下面总结一下相关流程。
1.初始化EGL相关变量,包括display surface 和 contex
2.初始化OpenGL相关
3.设置 OpenGL绘制相关
4.调用eglSwapBuffers 将后台的显示缓存显示到屏幕上

上述方法都在native层完成,java层只需要传入要绘制的surface对象即可。

###初始化EGL
这里首先介绍一下EGL中三个比较重要的三个数据结构。
####EGLContext
Opengl ES的状态的上下文,用于存储opengl状态,执行Opengl ES指令都会作用这个Context上,所以必须初始化完毕这个变量后才可以调用Opengl相关指令。
####EGLDisplay
用来作为本地显示视窗的引用,是EGL接口抽象出来的一个与平台无关的结构。

EGLDisplay eglGetDisplay (NativeDisplayType display);

用来初始化EGLDisplay,display为显示id,一般取默认值。
####EGLSurface
可以看作是帧缓存的引用,缓存gl绘制的画面,最终将这里的信息swap到EGLDisplay中完成显示。

eglCreateWindowSurface

这个方法会建立Android本地窗口与EGLSurface之间的关系,具体使用见下文代码。
####EGL初始化代码实现
这里首先从java层接收一个 Surface对象,将surface对象转换为Android的native window,之后将这个native window与EGL关联,初始化EGL相关。eglMakeCurrent成功之后,就可以执行gl相关指令了。

//指定EGLDisplay属性 这里是使用rgb888显示
static const EGLint configAttribs[] = 
		EGL_RENDERABLE_TYPE,    EGL_OPENGL_ES2_BIT,
		EGL_SURFACE_TYPE,       EGL_WINDOW_BIT,
		EGL_BLUE_SIZE,          8,
		EGL_GREEN_SIZE,         8,
		EGL_RED_SIZE,           8,
		EGL_NONE
;

//指定EGLContext属性,这里使用opengles2
static const EGLint contextAttribs[] = 
		EGL_CONTEXT_CLIENT_VERSION, 2,      //指定context为opengles2
		EGL_NONE
;

JNIEXPORT
void
Java_com_cm_opengles_CmOpenGLES_setSurface(JNIEnv *env, jobject obj, jobject jsurface, jbyteArray yuvDatas, jint size)

	//在native层获取surface的引用
	window = ANativeWindow_fromSurface(env, jsurface);

	EGLint numConfigs;
	EGLConfig config;

	EGLint format;
	EGLint width;
	EGLint height;

	//egl存储opengl管线状态 必须先初始化context,之后再创建和操作gl相关
	EGLContext context;
	//egl对本地显示窗口的抽象
	EGLDisplay display;
	//egl对显示buffer的抽象
	EGLSurface surface;

	//获取一个EGLDisplay对象
	if((display = eglGetDisplay(EGL_DEFAULT_DISPLAY)) == EGL_NO_DISPLAY)
		LOGI_EU("eglGetDisplay() returned error %d", eglGetError());
		return ;
	
	//初始化EGLDisplay display, 后面两个参数是指定支持的版本
	if(!eglInitialize(display, 0, 0))
		LOGI_EU("eglInitialize() returned error %d", eglGetError());
		return ;
	
	//为display指定显示buffer的格式,具体内容在configAttribs中
	if(!eglChooseConfig(display, configAttribs, &config, 1, &numConfigs))
		LOGI_EU("eglChooseConfig() returned error %d", eglGetError());
		return ;
	

	//从config获取显示格式
	if(!eglGetConfigAttrib(display, config, EGL_NATIVE_VISUAL_ID, &format))
		LOGI_EU("eglGetConfigAttrib() returned error %d", eglGetError());
		return;
	
	//使用上面的格式,根据Android窗口,对显示进行拉伸
 	uint32_t window_width  = ANativeWindow_getWidth(window);
	uint32_t window_height = ANativeWindow_getWidth(window);
	int ret = ANativeWindow_setBuffersGeometry(window, window_width, window_height, format);
	if(ret)
		LOGI_EU("ANativeWindow_setBuffersGeometry(format) returned error %d", ret);
		return;
	

	//用上面构造的display获取一个surface,这个surface可以认为是当前的帧缓存
	if(!(surface = eglCreateWindowSurface(display, config, window, NULL)))
		LOGI_EU("eglCreateWindowSurface() returned error %d", eglGetError());
		return;
	

	//创建一个EGLContext,用来保存gl状态机中相关信息
	if(!(context = eglCreateContext(display, config, EGL_NO_CONTEXT, contextAttribs)))
		LOGI_EU("eglCreateContext() returned error %d", eglGetError());
		return;
	

	//将display,surface与context绑定,后面就可以进行opengl相关操作
	if(EGL_FALSE == eglMakeCurrent(display, surface, surface, context))
		LOGI_EU("eglMakeCurrent() returned error %d", eglGetError());
		return;
	else
		LOGI_EU("EGL_INIT_OK");
	

	if(!eglQuerySurface(display, surface, EGL_WIDTH, &width) ||
			!eglQuerySurface(display, surface, EGL_HEIGHT, &height))
		LOGI_EU("eglQuerySurface() returned error %d", eglGetError());
		return;
	

	eglInstance.surface = surface;
	eglInstance.display = display;
	eglInstance.context = context;

	glClearColor(0.0f, 0.0f, 0.5f, 1.0f);
	glEnable(GL_CULL_FACE);
	glCullFace(GL_BACK);
	glDisable(GL_DEPTH_TEST);

###OpenGL ES相关处理
####YUV与RGB的转换
YUV格式的图像没办法直接显示在显示屏上,所以必须将其转换为RGB格式,如果在CPU中直接操作,会有大量的计算,这里可以将转换过程放到OpenGL的渲染管线中,让GPU在渲染之前先完成转换操作,CPU的工作就是设定好shader,传入YUV数据,之后就可以撒手干其他事情了,RGBbuffer计算完毕后,调用egl的sawp操作,图像就会显示到画布了。
YUV与RGB的转换公式见下图

下面就是如何在shader中实现这个算法了。片元着色器会对每个像素点进行着色计算,所以这个操作可以在片元着色器中进行。与普通纹理贴图不同的是,这里要同时使用Y,U,V三个分量的数据,也就是需要绑定3个纹理贴图,分别存入Y,U,V三个分量的buffer。
####shader实现
顶点着色器

const char * codeVertexShader = GET_STR(
	attribute vec3 aPosition;
	uniform mat4 uMVPMatrix;
	attribute vec2 aTexCoor;
	varying vec2 vTexCoor;
	void main()
	
		gl_Position = uMVPMatrix * vec4(aPosition, 1);
		vTexCoor = aTexCoor;
	
);

片元着色器

const char * codeFragShader = GET_STR(
		precision mediump float;
		uniform sampler2D yTexture;
		uniform sampler2D uTexture;
		uniform sampler2D vTexture;
		varying vec2 vTexCoor;
		void main()
		
			float y = texture2D(yTexture, vTexCoor).r;
			float u = texture2D(uTexture, vTexCoor).r - 0.5;
			float v = texture2D(vTexture, vTexCoor).r - 0.5;
			vec3 yuv = vec3(y, u, v);
			vec3 rgb;
			rgb = mat3( 1,       1,         1,
						0,       -0.39465,  2.03211,
						1.13983, -0.58060,  0) * yuv;
			gl_FragColor = vec4(rgb, 1);
		
);

片元着色器中拿到三个贴图,从而分别获取该点处Y,U,V三个分量的数据。这里texture2D(vTexture, vTexCoor).r和.g和.b的效果是一样的,不清楚为什么。

最终将生成的RGB数据补上最后一个通道的数据传递给 gl_FragColor。

这里有个小技巧,在编写shader时经常将作为字符串,如果直接加引号拼接会显得非常混乱,这里参考了ijk的方法,定义一个宏,将宏里的代码都认为是字符串,写法如下

#define GET_STR(x) #x

这样GET_STR里的内容就会被直接当作字符串了。

####OpenGL ES初始化
这里主要分以下几个步骤:
1.首先需要编译和连接shader,生成program
2.获取顶点坐标,纹理坐标,采样器的索引,用于后面绘制时给这些量传值
3.生成3个纹理的索引,绘制时用这些索引作为纹理id,进行纹理的绑定

代码实现

JNIEXPORT
void
Java_com_cm_opengles_CmOpenGLES_init(JNIEnv *env, jobject obj, jint pWidth, jint pHeight)

	LOGI_EU("init()");
	//创建一个引用
	instance = (Instance *)malloc(sizeof(Instance));
	memset(instance, 0, sizeof(Instance));

	GLuint shaders[2] = 0;
	//创建顶点shader和片元shader
	shaders[0] = initShader(codeVertexShader, GL_VERTEX_SHADER);
	shaders[1] = initShader(codeFragShader, GL_FRAGMENT_SHADER);
	//编译链接shader
	instance->pProgram = initProgram(shaders, 2);

	//获取mvp矩阵的索引
	instance->maMVPMatrixHandle = glGetUniformLocation( instance->pProgram, "uMVPMatrix");
	//获取顶点坐标索引
	instance->maPositionHandle = glGetAttribLocation(instance->pProgram, "aPosition");
	//获取纹理坐标索引
	instance->maTexCoorHandle = glGetAttribLocation(instance->pProgram, "aTexCoor");
	//获取采样器索引
	instance->myTextureHandle = glGetUniformLocation(instance->pProgram, "yTexture");
	instance->muTextureHandle = glGetUniformLocation(instance->pProgram, "uTexture");
	instance->mvTextureHandle = glGetUniformLocation(instance->pProgram, "vTexture");

	//获取对象名称 这里分别返回1个用于纹理对象的名称,后面为对应纹理赋值时将以这个名称作为索引
	glGenTextures(1, &instance->yTexture);
	glGenTextures(1, &instance->uTexture);
	glGenTextures(1, &instance->vTexture);

	LOGI_EU("init() yT = %d, uT = %d, vT = %d.", instance->yTexture, instance->uTexture, instance->vTexture);
	LOGI_EU("%s %d error = %d", __FILE__,__LINE__, glGetError());

	//为yuv数据分配存储空间
	instance->yBufferSize = sizeof(char) * pWidth * pHeight;
	instance->uBufferSize = sizeof(char) * pWidth / 2 * pHeight / 2;
	instance->vBufferSize = sizeof(char) * pWidth / 2 * pHeight / 2;
	instance->yBuffer = (char *)malloc(instance->yBufferSize);
	instance->uBuffer = (char *)malloc(instance->uBufferSize);
	instance->vBuffer = (char *)malloc(instance->vBufferSize);
	memset(instance->yBuffer, 0, instance->yBufferSize);
	memset(instance->uBuffer, 0, instance->uBufferSize);
	memset(instance->vBuffer, 0, instance->vBufferSize);
	//指定图像大小
	instance->pHeight = pHeight;
	instance->pWidth = pWidth;
	LOGI_EU("width = %d, height = %d", instance->pWidth, instance->pHeight);

	glClearColor(0.5f, 0.5f, 0.5f, 1.0f);

//	glEnable(GL_DEPTH_TEST);
	LOGI_EU("%s %d error = %d", __FILE__,__LINE__, glGetError());

####OpenGL ES绘制
主要步骤如下
1.use第一步生成的program
2.如果需要旋转缩放等操作,给gl传入mvp矩阵
3.传入顶点坐标,传入纹理坐标
4.绑定纹理。这里首先要激活上一步生成的纹理id,之后进行绑定,设置参数,最后将存放纹理的buffer传入。
5.将片元shader中定义的三个纹理设置为3个层
6.使能顶点坐标和纹理坐标
7.绘制上面所设置的内容

代码实现

void
drawFrame(void* ins)

	if(DEBUG)
	
		LOGI_EU("%s", __FUNCTION__);
	

	glEnable(GL_CULL_FACE);
	glCullFace(GL_BACK);
	glDisable(GL_DEPTH_TEST);

	Instance * instance = (Instance *)ins;
	if (instance == 0)
	
		LOGW_EU("%s Program is NULL return!", __FUNCTION__);
		return;
	

	//使用编译好的program
	glUseProgram(instance->pProgram);
	//图像旋转270度
	float * maMVPMatrix = getRotateM(NULL, 0, 270, 0, 0, 1);
	//float * maMVPMatrix = getRotateM(NULL, 0, 0, 0, 0, 1);
	//传入mvp矩阵
	glUniformMatrix4fv(instance->maMVPMatrixHandle, 1, GL_FALSE, maMVPMatrix);

	free(maMVPMatrix);
	//传入顶点坐标
	glVertexAttribPointer(instance->maPositionHandle,
						  3,//GLint size X Y Z
						  GL_FLOAT,//GLenum type
						  GL_FALSE,//GLboolean normalized
						  3 * 4,//GLsizei stride  dataVertex中三个数据一组
						  dataVertex//const GLvoid * ptr
	);
	//传入纹理坐标
	glVertexAttribPointer(instance->maTexCoorHandle,
						  2,//S T
						  GL_FLOAT,//GLenum type
						  GL_FALSE,//GLboolean normalized
						  2 * 4,//GLsizei stride   dataTexCoor中两个数据一组
						  dataTexCoor//const GLvoid * ptr
	);

	//绑定纹理
	bindTexture(GL_TEXTURE0, instance->yTexture, instance->pWidth, instance->pHeight, instance->yBuffer);
	bindTexture(GL_TEXTURE1, instance->uTexture, instance->pWidth / 2, instance->pHeight / 2, instance->uBuffer);
	bindTexture(GL_TEXTURE2, instance->vTexture, instance->pWidth / 2, instance->pHeight / 2, instance->vBuffer);

	//片元中uniform 2维均匀变量赋值
	glUniform1i(instance->myTextureHandle, 0); //对应纹理第1层
	glUniform1i(instance->muTextureHandle, 1); //对应纹理第2层
	glUniform1i(instance->mvTextureHandle, 2); //对应纹理第3层

	//enable之后这些引用才能在shader中生效
	glEnableVertexAttribArray(instance->maPositionHandle);
	glEnableVertexAttribArray(instance->maTexCoorHandle);

	//绘制 从顶点0开始绘制,总共四个顶点,组成两个三角形,两个三角形拼接成一个矩形纹理,也就是我们的画面
	glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);

####传入数据与显示
现在,就可以传入 YUV数据并进行显示了,这里调用eglSwapBuffers,当gl 绘制完毕后,显示缓存surface填充完毕,我们就可以通过eglSwapBuffers,让display也就是显示设备去显示surface中的内容了。
代码实现

//渲染数据
JNIEXPORT
void
Java_com_cm_opengles_CmOpenGLES_drawFrame(JNIEnv *env, jobject obj, jbyteArray yuvDatas, jint size)

	//将yuv数据分别copy到对应的buffer中
	jbyte * srcp = (*env)->GetByteArrayElements(env, yuvDatas, 0);

	memcpy(instance->yBuffer, srcp, instance->yBufferSize);
	memcpy(instance->uBuffer, srcp+instance->yBufferSize, instance->uBufferSize);
	memcpy(instance->vBuffer, srcp+instance->yBufferSize+instance->uBufferSize, instance->vBufferSize);

	(*env)->ReleaseByteArrayElements(env, yuvDatas, srcp, JNI_ABORT);

	//opengl绘制
	drawFrame(instance);

	//交换display中显示图像缓存的地址和后台图像缓存的地址,将当前计算出的图像缓存显示
	EGLBoolean res = eglSwapBuffers(eglInstance.display, eglInstance.surface);

	if(res == EGL_FALSE)
		LOGI_EU("eglSwapBuffers Error %d", eglGetError());
	else
		LOGI_EU("eglSwapBuffers Ok");
	


//释放资源
JNIEXPORT
void
Java_com_cm_opengles_CmOpenGLES_release(JNIEnv *env, jobject obj)

	LOGI_EU("release()");
	if(instance != 0)
		
			eglMakeCurrent(eglInstance.display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
			eglDestroyContext(eglInstance.display, eglInstance.context);
			eglDestroySurface(eglInstance.display, eglInstance.surface);
			eglTerminate(eglInstance.display);

			free(instance->yBuffer);
			free(instance->uBuffer);
			free(instance->vBuffer);
			instance->yBuffer = 0;
			free(instance);
			instance = 0;
		

这样就完成了OpenGL对YUV420P的渲染。

以上是关于视频学习笔记:Android OpenGL渲染YUV420P图像的主要内容,如果未能解决你的问题,请参考以下文章

Android MediaCodec+OpenGL视频编解码实践笔记

Android OpenGL ES 学习 –渲染YUV视频以及视频抖音特效

Android OpenGL ES 学习 –渲染YUV视频以及视频抖音特效

Android OpenGL 播放视频学习

Android OpenGL ES 学习 - MediaCodec + OpenGL 解析H264视频+滤镜

Android OpenGL ES 学习 - MediaCodec + OpenGL 解析H264视频+滤镜