视频学习笔记: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的渲染。

  • 5
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 14
    评论
OpenGL PBOOpenGL中的一个扩展,它允许通过在图形处理单元(GPU)上创建一个像素缓冲对象(PBO),将数据从CPU传输到GPU,然后可以使用这些数据在纹理中显示图像。 在使用PBOYUV420p格式的纹理显示图像之前,我们需要将YUV420p格式的图像数据转换为适用于OpenGL纹理的格式。YUV420p是一种常见的视频图像格式,它包含一个与图像分辨率相同的Y分量(明亮度)和两个与图像分辨率的四分之一相同的UV分量(色度)。Y分量与图像分辨率相同,而UV分量的分辨率被降低以节省存储和传输带宽。 首先,我们需要创建一个纹理,并将它与PBO关联。然后,我们可以将YUV420p数据传输到PBO中。将数据传输到PBO的过程涉及到将Y、U和V分量的数据按照特定的布局传输到PBO中。我们可以使用glBufferData函数将数据传输到PBO。 接下来,我们需要将PBO中的数据绑定到纹理,并对纹理进行设置以正确地显示图像。我们可以使用glBindTexture函数来绑定纹理,并使用glTexSubImage2D函数将PBO中的数据传输到纹理中。 最后,我们可以使用OpenGL渲染管线将纹理中的图像显示在屏幕上。我们可以使用一个简单的顶点着色器和一个片段着色器将纹理中的图像转换为可视化的图像。 总结起来,使用OpenGL PBOYUV420p纹理可以更高效地显示图像。通过将图像数据传输到PBO中,并将PBO纹理关联,可以在GPU上进行图像处理和渲染,从而提高了图像显示的效率和性能。
评论 14
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值