Android OpenGL ES视频渲染(二)EGL+OpenGL

相关文章:Android OpenGL ES视频渲染(一)GLSurfaceView

上一篇讲了如何在通过GLSurfaceView使用OpenGL进行视频, 当时我们也讲过,它是继承自SurfaceView,具备SurfaceView的特性,并加入了EGL的管理。那么本篇就介绍我们如何在native层使用EGL+OpenGL来完成视频渲染的。
为什么要这么麻烦呢,一是在native层使用能有更高的效率;二是在native层可以更好的结合OpenGL代码,更好的把一些开源工具引进来,使用OpenGL也会更加灵活,因为OpenGL本身就是底层代码;三是这样你会对OpenGL ES在Android上的使用有更深入的了解。

本文将使用FFmpeg解码出YUV数据,并将YUV420P类型的视频通过EGL+OpenGL渲染出来。

EGL简介

前一章节我们有描述到OpenGL ES的一些使用,但OpenGL ES是 定义了一个渲染图形的规范,但并没有定义窗口系统。也就是说OpenGL中并没有定义渲染的图像是如何绘制到Android的窗口上的,这也是因为OpenGL本身是跨平台的,所以这部分需要各个平台自己实现,那么EGL就是连接OpenGL ES和本地窗口系统的接口,本地窗口相关的API提供了访问本地窗口系统的接口,EGL提供了创建渲染表面,接下来OpenGL ES就可以在这个渲染表面上绘制,同时提供了图形上下文,用来进行状态管理。
在这里插入图片描述
EGL 具备以下功能:

  1. 和设备的本地窗口系统通信 ,Android上就是ANativeWindow
  2. 查询绘图表面的可用类型及配置
  3. 创建OpenGL ES可用的“绘图表面”,Android上就是Surface
  4. 在OpenGL ES 或是其它图形渲染 API 之间同步渲染
  5. 管理渲染资源,如纹理贴图

简单来说,就是EGL承担了为OpenGL ES提供上下文环境以及窗口管理的职责。

使用流程:

我们想实现一个视频渲染的功能,把FFmpeg解码出来的YUV420P数据显示出来,那么我们需要做以下事情:
1、OpenGL ES渲染程序的创建
2、EGL创建OpenGL ES上下文环境。
3、纹理更新并绘制。

1、OpenGL ES渲染程序的创建

这部分和在GLSurfaceView中使用时并没太大区别:
编写着色器代码
->创建渲染程序
->填充顶点坐标及纹理坐标

顶点着色器

#define GET_STR(x) #x
static const char *verShader = GET_STR(
                                      attribute vec4 aPosition; //顶点坐标
                                      attribute vec2 aTexCoord; //纹理坐标
                                      varying vec2 vTexCoord;   //最终传递给片元着色器的坐标
                                      void main()
    {
        vTexCoord = aTexCoord;
        gl_Position = aPosition;
    }
);

顶点着色器纹理坐标的计算略有不同,下面会说明。

片元着色器

static const char *fragShader_YUV420P = GET_STR(
                                     precision mediump float;    //精度
                                     varying vec2 vTexCoord;//顶点着色器传递的纹理坐标
                                     uniform sampler2D yTexture; //第一层纹理Y
                                     uniform sampler2D uTexture;//第二层纹理U
                                     uniform sampler2D vTexture;//第三层纹理V
                                     void main()
    {
        mediump vec3 yuv;
        lowp vec3 rgb;
        yuv.x= texture2D(yTexture, vTexCoord).r;
        yuv.y = texture2D(uTexture, vTexCoord).r - 0.5;
        yuv.z = texture2D(vTexture, vTexCoord).r - 0.5; 
        rgb = mat3(1.0,     1.0,    1.0,
                   0.0, -0.39465, 2.03211,
                   1.13983, -0.58060, 0.0) * yuv;
        gl_FragColor = vec4(rgb, 1.0);
    }
);

和GLSurfaceView中不同,片元着色器的代码使用的是sampler2D 类型的纹理数据。
我们知道,GPU中用的都是RGBA颜色,所以要进行YUV420P转RGB的计算,最后把RGB颜色设置给gl_FragColor,alpha设置为1就行。
这里简单说下转换过程,更详细的请查看一些相关资料。
YUV420P是平面类型的数据,可以理解为Y/U/V数据是分三层存放的,每一层都是单像素数据,所以我们分别从三层纹理中取出其第一个数据,例如yuv.x= texture2D(yTexture, vTexCoord).r;即通过texture2D函数从坐标vTexCoord取出yTexture这一层纹理的像素,r是第一个数据,就是Y数据。
U/V同理,但U/V取出来的数据需要-0.5,因为UV的默认值是127(OpenGL ES的Shader中会把内存中0~255的整数数值换算为0.0~1.0的浮点数值)。
最后通过矩阵转换YUV为RGB,代码是其中一种转换方式,YUV转RGB有很多不同的公式,对应不同的标准。
上面是一种标准,例如下面这种转换标准也是可以的。

//yuv转rgb
rgb.r = y + 1.4075 * v; 
rgb.g = y - 0.3455 * u - 0.7169 * v;
rgb.b = y + 1.779 * u;

创建渲染程序
在《Android OpenGL ES视频渲染(一)GLSurfaceView》中已详细描述,不赘述,还是按照下面的流程。
在这里插入图片描述
填充顶点坐标及纹理坐标
在《Android OpenGL ES视频渲染(一)GLSurfaceView》中已详细描述。
这里有个小区别,因为没有纹理坐标转换矩阵,我们就直接在填充阶段,就将坐标对应好。把纹理按照,左下->右下->左上->右上的顺序,贴到物体上。对应代码如下:

//顶点坐标
    static 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,
    };

    GLuint apos = (GLuint)glGetAttribLocation(program, "aPosition");
    glEnableVertexAttribArray(apos);
    glVertexAttribPointer(apos, 3, GL_FLOAT, GL_FALSE, 12, vers);

//纹理坐标,注意计算机坐标系和纹理坐标系的转换。这里是转换后的
    static float txts[] =
    { 
        0.0f, 1.0f, 
        1.0f, 1.0f, 
        0.0f, 0.0f, 
        1.0f, 0.0f 
    };
    GLuint atex = (GLuint)glGetAttribLocation(program, "aTexCoord");
    glEnableVertexAttribArray(atex);
    glVertexAttribPointer(atex, 2, GL_FLOAT, GL_FALSE, 8, txts);

设置片元着色器各层纹理ID

    glUniform1i(glGetUniformLocation(program, "yTexture"), 0); //Y 纹理index0
    glUniform1i(glGetUniformLocation(program, "uTexture"), 1); //U 纹理index1
    glUniform1i(glGetUniformLocation(program, "vTexture"), 2); //V 纹理index2
EGL创建OpenGL ES上下文环境

流程如下:
初始化EGLDisplay
->创建合适的绘制上下文EGLContext
->创建EGLSurface
->绑定线程

初始化EGLDisplay
EGLDisplay是一个封装系统物理屏幕的数据类型(可以理解为绘制目标的一个抽象),它将OpenGL ES的输出和设备的屏幕桥接起来。

调用eglGetDisplay方法返回EGLDisplay来作为OpenGL ES渲染的目标。传入EGL_DEFAULT_DISPLAY,这样会返回一个默认显示设备。
eglInitialize初始化display,后两个参数是Major和Minor的版本号,传0即可。

        //1 获取EGLDisplay
        EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
        //2 初始化EGLDisplay
        if(EGL_TRUE != eglInitialize(display, 0, 0))
        {
            LOGE("eglInitialize failed!");
            return false;
        }

创建合适的绘制上下文EGLContext
创建上下文需要指定一些配置项,类似于色彩格式、像素格式、RGBA的表示以及SurfaceType等,不
同的系统以及平台使用的EGL标准是不同的。

一种方式是使用eglGetConfigs函数获取底层窗口系统支持的所有EGL表面配置,然后再使用eglGetConfigAttrib依次查询每个EGLConfig相关的信息,EGLConfig包含了渲染表面的所有信息,包括可用颜色、缓冲区等其他特性。

EGLBoolean eglGetConfigs(EGLDisplay display, EGLConfig *configs, EGLint maxReturnConfigs,EGLint *numConfigs);
EGLBoolean eglGetConfigAttrib(EGLDisplay display, EGLConfig config, EGLint attribute, EGLint *value)

我们常用的是另一种方式,调用eglChooseConfig让EGL选择一个合适的配置,参数如下:

EGLBoolean eglChooseConfig(EGLDisplay display,
                           const EGLint* attribs,     // 想要的属性事先定义到这个数组里
						   EGLConfig* configs,       // 图形系统将返回若干满足条件的配置到该数组
						   EGLint maxConfigs,        // 上面数组的容量
						   EGLint* numConfigs);      // 图形系统返回的可用的配置个数

选择好配置后,我们就可以创建上下文了。调用函数eglCreateContext。

EGLContext eglCreateContext
(
EGLDisplay display, 
EGLConfig config, // 前面选好的可用EGLConfig
EGLContext shareContext, // 允许多个EGLContext共享特定类型的数据,传递EGL_NO_CONTEXT表示不与其他上下文共享资源
const EGLint* attribList // 指定操作的属性列表,只能接受一个属性EGL_CONTEXT_CLIENT_VERSION用来表示使用的OpenGL ES版本
 );

完整代码如下:

        //3 配置
        EGLint attribs[] =
        {
            EGL_RED_SIZE, 8,
            EGL_GREEN_SIZE, 8,
            EGL_BLUE_SIZE, 8,
            EGL_SURFACE_TYPE, EGL_WINDOW_BIT,
            EGL_NONE
        };
        EGLConfig config = 0;
        EGLint numConfigs = 0;
        if(EGL_TRUE != eglChooseConfig(display, attribs, &config, 1, &numConfigs))
        {
            LOGE("eglChooseConfig failed!");
            return false;
        }
        //4 创建EGL context
        const EGLint ctxAttr[] = { EGL_CONTEXT_CLIENT_VERSION , 2, EGL_NONE};
        EGLContext context = eglCreateContext(display, config, EGL_NO_CONTEXT, ctxAttr);

创建EGLSurface
我们已经创建好上下文,那么应该如何将OpenGL ES的输出渲染到设备的屏幕上呢?需要将EGL和设备的屏幕连接起来,只有这样EGL才是一个“桥”的功能,从而使得OpenGL ES的输出可以渲染到设备的屏上。所以接下来我们要创建EGLSurface ,EGLSurface回合原生窗口ANativeWindow 关联起来,那么渲染输出就可以显示到设备屏幕。

调用eglCreateWindowSurface创建EGLSurface,可以看到参数三,就是原生窗口,在Android里为ANativeWindow 。

EGLSurface eglCreateWindowSurface(EGLDisplay display,
                                  EGLConfig config, // 前面选好的可用EGLConfig
                                  EGLNatvieWindowType window, // 原生窗口
                                  const EGLint *attribList) // 指定窗口属性列表,可以为null,一般指定渲染所用的缓冲区使用但缓冲或者后台缓冲,默认为后者。

ANativeWindow 可以从Java层获取:Surface

      SurfaceView mSurfaceView = (SurfaceView)findViewById(R.id.surfaceView1); 
      final SurfaceHolder sh = mSurfaceView.getHolder();
      Surface surface = mSurfaceView.getSurface();
      将这个surface传入到JNI

native端:

ANativeWindow *win = ANativeWindow_fromSurface(env, jsurface);

其实Surface类型就是ANativeWindow的子类,所以也可以这样获取

sp<Surface> surface(android_view_Surface_getSurface(env, jsurface));
ANativeWindow *win = (ANativeWindow *) surface;

当然还有很多中方式,之前其他文章的SoftwareRenderer的介绍中也说过如何创建,不赘述。
完整代码:

EGLSurface  surface = eglCreateWindowSurface(display, config, win, NULL);

绑定线程
OpenGL ES的绘制都是需要在独立的线程之中(GLSurfaceView中其实系统帮我们创建了GLThread),线程要绑定显示设备(EGLSurface)与上下文环境(EGLContext),这样才可以执行OpenGL的指令。

调用eglMakeCurrent进行绑定。

EGLBoolean eglMakeCurrent(EGLDisplay display,
                          EGLSurface draw, // EGL绘图表面
                          EGLSurface read, // EGL读取表面
                          EGLContext context // 指定连接到该表面的渲染上下文
                         );

完整代码:

        if(EGL_TRUE != eglMakeCurrent(display, surface, surface, context))
        {
            LOGE("eglMakeCurrent failed!");
            return false;
        }
        return true;
3、纹理更新并绘制

做好OpenGL及EGL的初始化工作后,我们在FFmpeg解码出YUV数据线程中,将数据更新到纹理,然后交换缓冲swapbuf,这样就会输出到屏幕。为什么要swapbuf呢?因为是双缓冲绘制,绘制在后台,显示在前台,交换缓冲,即将渲染的画面输出到前台。

更新纹理代码如下:
基本流程和之前一样,调用glTexImage2D更新纹理数据,glTexImage2D的format参数需要根据数据类型来指定,因为是YUV数据,所以format为GL_LUMINANCE。其他参数比较好理解,参看代码注释即可。

void UpdateTexture(unsigned int index, int width, int height, unsigned char *buf)
{
	unsigned int format = GL_LUMINANCE;
    if(texts[index] == 0)
    {       
        glGenTextures(1, &texts[index]);
        glBindTexture(GL_TEXTURE_2D, texts[index]);
        //设置缩放参数
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    }
    
	//激活第N层纹理,绑定到创建的opengl纹理
    glActiveTexture(GL_TEXTURE0 + index);
    glBindTexture(GL_TEXTURE_2D, texts[index]);

    //更新纹理
    glTexImage2D(GL_TEXTURE_2D,
         0,           //细节基本 0默认
         format,//gpu内部格式 亮度,灰度图
         width, height, //拉升到全窗口
         0,             //边框
         format,//数据的像素格式,亮度,灰度图 要与上面一致
         GL_UNSIGNED_BYTE, //像素的数据类型
         buf                    //纹理的数据
}

绘制每一层数据,

  1. 更新纹理。 需要注意的是YUV4420P中,UV数据是Y的1/4,一半宽,一半高。具体不赘述,了解YUV420的构成即可。
  2. glDrawArrays以三角形方式绘制。
  3. EGL eglSwapBuffers交换缓冲,显示出来。

完整代码如下:

	void renderFrame(unsigned char *data[], int width, int height)
    {
        AutoMutex l(mux);
        UpdateTexture(0, width, height, data[0]); // Y
        UpdateTexture(1, width / 2, height / 2, data[1]); // U
        UpdateTexture(2, width / 2, height / 2, data[2]); // V
        glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
        eglSwapBuffers(display, surface);
    }
  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
OpenGL中进行离屏渲染通常使用EGL来实现。EGL是一个跨平台的图形库,可以在不同的操作系统和图形硬件上运行。以下是一个简单的示例代码,说明如何使用EGL进行离屏渲染: ```c++ #include <EGL/egl.h> #include <GLES2/gl2.h> void RenderOffscreen() { // 创建EGLDisplay EGLDisplay eglDisplay = eglGetDisplay(EGL_DEFAULT_DISPLAY); EGLint major, minor; eglInitialize(eglDisplay, &major, &minor); // 配置EGLContext属性 EGLint contextAttribs[] = { EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE }; // 创建EGLContext EGLConfig config; EGLint numConfigs; EGLint attribs[] = { EGL_SURFACE_TYPE, EGL_PBUFFER_BIT, EGL_RED_SIZE, 8, EGL_GREEN_SIZE, 8, EGL_BLUE_SIZE, 8, EGL_ALPHA_SIZE, 8, EGL_DEPTH_SIZE, 0, EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, EGL_NONE }; eglChooseConfig(eglDisplay, attribs, &config, 1, &numConfigs); EGLSurface eglSurface = eglCreatePbufferSurface(eglDisplay, config, NULL); EGLContext eglContext = eglCreateContext(eglDisplay, config, NULL, contextAttribs); // 绑定EGLContext和EGLSurface eglMakeCurrent(eglDisplay, eglSurface, eglSurface, eglContext); // 在离屏上下文中进行渲染 glClearColor(1.0f, 1.0f, 1.0f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); // 将渲染结果保存到文件或内存中 // ... // 解绑EGLContext和EGLSurface eglMakeCurrent(eglDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); // 销毁EGLContext和EGLSurface eglDestroySurface(eglDisplay, eglSurface); eglDestroyContext(eglDisplay, eglContext); // 终止EGLDisplay eglTerminate(eglDisplay); } int main() { // 在主上下文中进行渲染 // ... // 在离屏上下文中进行渲染 RenderOffscreen(); return 0; } ``` 在上面的示例代码中,我们使用EGL创建一个离屏上下文和表面,使用glClear()函数在离屏上下文中进行渲染,然后将渲染结果保存到文件或内存中。最后,我们销毁离屏上下文和表面,终止EGLDisplay。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值