Android Native使用OpenGL ES进行图像绘制

Android Native中使用OpenGL ES实现基本的2D绘制。
Views: 39
0 0
Read Time:3 Minute, 41 Second

相信不少朋友一定听说过Open GL,但也仅限于听说而已,往往没有机会也可能是没有需求去了解乃至使用过。这篇文章将结合我个人在开发流程中的一些经验,简单讲解如何在安卓原生系统中使用OpenGL来实现基础的图像绘制。

Open GL其实是Open Graphic Library的英文缩写,其作用是为上层应用提供统一的、跨平台且可跨语言的2D/3D图形渲染接口。Open GL针对不同的系统提供不同的接口子集,在安卓这类嵌入式系统中称之为OpenGL ES(Open GL for Embedded System)。

在Android12中,谷歌提供了OpenGL ES 1.0与2.0和OpenGL ES 3.0的接口,而OpenGL本身已经发展到了4.x版本,不过等到Android系统支持仍需要一段时间。而在我们基本的开发中,OpenGL ES 2.0版本其实就已经能满足我们基本的开发需求。

Android将以so库的形式为原生应用提供OpenGL ES的支持,所以我们在使用时,需要确保导入以下共享库:

libEGL
libGLESv2
libGLESv3

如在Android.bp中,将上述共享库添加到shared_libs字段内,如果你是使用Android.mk,则应将上述共享库添加到LOCAL_SHARED_LIBRARIES字段。同时,还应该添加如下头文件才能正常使用相应的API:

#include <EGL/egl.h>
#include <EGL/eglext.h>
#include <GLES2/gl2.h>
#include <GLES2/gl2ext.h>
#include <GLES3/gl3.h>
#include <GLES3/gl3ext.h>

这是正式使用前的准备工作。在我们进行下一步之前,我们还得简单了解一下以下概念:

1.EGL:Embedded Graphic Interface,是OpenGL ES和底层视窗系统之间的适配层,OpenGL之所以具有这样的跨平台性,就是因为有类似于EGL这样的适配层存在。在OpenGL ES工作流中的Display、Config和Surface等都需要EGL环境来获取或配置。

2.Surface:Surface由每个应用自行创建,每个Surface具有相同的属性,只是具体的值不同。如宽高、数据格式、层级等等。在Android系统中,Surface一般都会交由SurfaceFlinger集中管理,之后会由hwcomposer将不同的Surface合成显示。最终你会看到我们多个应用的界面显示在同一个显示屏上。

3.Framebuffer:Framebuffer则是用于存储我们需要显示在Display设备上的具体数据的一块内存空间。Framebuffer的管理会由专门的驱动进行管理,设备节点通常是/dev/fbx,在Android设备中则为/dev/graphics/fbx,通常是fb0。Framebuffer内的内容可以直接显示到Display设备上,具体与渲染模式有关。

很多人可能会对Surface与Framebuffer的概念感到难以理解。这里我谈谈我的理解,Surface其实也是需要Buffer空间来承载数据的,不过这部分的Buffer通常是来自于/dev/pmem、/dev/ashmem等,其主要目的是用于硬件加速,一般这部分Buffer数据是送往GPU处理的,而Framebuffer则是送往显示器。Surface的概念抽象层次更高,具有更多的属性,Framebuffer则比较好理解。我们在理解这两个概念时,其实只要记住两者的目的地不同就能比较方便的厘清两者的差异。

在理解上述概念后,我们开始看如何进行图像绘制。

1.EGL初始化

前面我们已经讲到EGL是OpenGL ES能够工作的基础。在我们正式使用OpenGL ES的API之前,我们需要配置好EGL,这一环节即是EGL的环境初始化。关于EGL环境的初始化,大致分为如下几个步骤:

第一步:获取Dispaly,Display代表显示器,在有些系统中存在多个显示器,也就会有多个Display。display指定显示连接,一般使用默认的EGL_DEFAULT_DISPLAY,即返回与默认原生窗口的连接。同时这里也需要注意,display并不一定指物理的屏幕,也可以是虚拟出来的display。

使用接口:EGLboolean eglGetDisplay(NativeDisplay dpy)

一般使用默认的EGL_DEFAULT_DISPLAY,代码示例:

EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
if (display == EGL_NO_DISPLAY)
{
       LOGE("Failed to get egl display");
       return false;
}

第二步:初始化egl,这一步会进行内部初始化工作,并返回EGL的版本号(major,minor)

使用接口:EGLBoolean  eglInitialize(EGLDisplay display,EGLint  *majorVersion,EGLint  *minorVersion)

代码示例:

EGLint major = 0;
EGLint minor = 0;
if (!eglInitialize(display, &major, &minor))
{
     LOGE("Failed to initialize EGL:%s ", getEGLError());
     return false;
}
else
{
     LOGI("Intiialized EGL at major:%d,minor:%d", major, minor);
}

第三步:选择config配置,配置的内容实际上是Framebuffer的参数

使用接口:EGLboolean eglChooseConfig(EGLDisplay dpy, const EGLint * attr_list, EGLConfig * config, EGLint config_size, EGLint *num_config)

代码示例:

  //Select the configuration that "best" matches our desired characteristics
  EGLConfig egl_config;
  EGLint num_configs;
  if (!eglChooseConfig(display, config_attribs, &egl_config, 1,&num_configs))
  {
        LOGE("eglChooseConfig() failed with error:%s", getEGLError());
        return false;
  }

其中attr_list是一个参数数组,以id,value的形式依次存放,在Android中常用的attr_list如下:

// Hardcoded to RGBx output display
 const EGLint config_attribs[] = {
        // Tag  Value
        EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT,
        EGL_RED_SIZE, 8,
        EGL_GREEN_SIZE, 8,
        EGL_BLUE_SIZE, 8,
        EGL_NONE
};

我们也可以通过如下接口来获取所有支持的config配置:

EGLboolean eglGetConfigs(EGLDisplay dpy, EGLConfig * config, EGLint config_size, EGLint *num_config)

每个config有众多的Attribute,这些Attribute决定了Framebuffer的格式和能力,通过如下接口来获取:

eglGetConfigAttrib ()

第四步:构造Surface,用于承载Framebuffer

使用接口:EGLSurface eglCreateWindowSurface(EGLDisplay dpy, EGLConfig confg,NativeWindow win, EGLint *cfg_attr)

示例代码:

 // Create the EGL render target surface
   mSurface = eglCreateWindowSurface(mDisplay, egl_config, mWindow, nullptr);
   if (mSurface == EGL_NO_SURFACE)
   {
       ALOGE("eglCreateWindowSurface failed");
       return false;
    }

除了WindowSurface,还支持另外的Surface:PixmapSurface和PBufferSurface,这两种Surface都不是可显示的Surface,其中PixmapSurface是保存在系统内存中的位图,而PBufferSurface是保存在显存中的帧。

在CreateWindowSurface时,我们也可以设定Surface的Attribute,一些常见的Attribute:

EGL_HEIGHT
EGL_WIDTH
EGL_LARGEST_PBUFFER
EGL_TEXTURE_FORMAT
EGL_TEXTURE_TARGET
EGL_MIPMAP_TEXTURE
EGL_MIPMAP_LEVEL

可以通过 eglSurfaceAttrib() 设置、eglQuerySurface()读取。

第五步:创建Context,Context内包含当前的颜色、纹理坐标、变换矩阵、渲染模式等一堆状态,这些状态结合顶点坐标、shader等图元从而形成帧缓冲区内的像素。

使用接口:EGLContext eglCreateContext(EGLDisplay dpy, EGLSurface write,EGLSurface read, EGLContext * share_list)

代码示例:

// Create the EGL context
// NOTE:  Our shader is (currently at least) written to require version 3, so this
//        is required.
const EGLint context_attribs[] = { EGL_CONTEXT_CLIENT_VERSION, 3, EGL_NONE };
mContext = eglCreateContext(mDisplay, egl_config, EGL_NO_CONTEXT, context_attribs);
if (mContext == EGL_NO_CONTEXT)
{
       ALOGE("Failed to create OpenGL ES Context:%s", getEGLError());
       return false;
}

第六步:MakeCurrent,这一步的目的是将我们将要渲染的Context绑定到Surface、Display上。

使用接口:EGLBoolean eglMakeCurrent(EGLDisplay display,EGLSurface draw,EGLSurface read,EGLContext context);

 代码示例:

// Activate our render target for drawing
if (!eglMakeCurrent(mDisplay, mSurface, mSurface, mContext))
{
     ALOGE("Failed to make the OpenGL ES Context current:%s", getEGLError());
     return false;
}

到此,EGL的初始化算是完成了,接下来的工作则是利用OpenGL ES的API来开始真正的绘制行为。

2.OpenGL绘制

其实整个的绘制过程也是有一定流程规范的,这里给出比较通用的一个流程:使用2D纹理进行绘制。

第一步:设置视窗入口,用于设定我们显示窗口的的大小。视窗入口可以决定我们最终显示的图像的画幅。

使用接口:glViewport(GLint x, GLint y, GLsizei width, GLsizei height,GLclampf alpha);

接口参数中,(x,y)是左下角起始顶点的坐标,width与height代表宽高。

第二步:清除颜色缓冲区,用于清屏。

使用接口:void glClearColor(GLclampf red,GLclampf green,GLclampf blue,)

代码示例:

glClearColor(0.1f, 0.5f, 0.1f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);

第三步:选择对应纹理的Shader program

使用接口:void glUseProgram(GLuint program);

第四步:将Shader Program绑定到纹理上(内部会进行采样)

代码示例:

glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, mTextureMap);
GLint sampler = glGetUniformLocation(mShaderProgram, "tex");
glUniform1i(sampler, 0);

第五步:设定颜色矩阵与顶点矩阵。这一步没有API调用,只是根据需要设定矩阵参数即可。

第六步:绘制所有的二位矩阵,代码如下:

//vertsPos与vertsTex
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, vertsPos);
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 0, vertsTex);
glEnableVertexAttribArray(0);
glEnableVertexAttribArray(1);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
// Clean up and flip the rendered result to the front so it is visible
glDisableVertexAttribArray(0);
glDisableVertexAttribArray(1);

第七步:提交所有的OpenGL指令,交由GPU执行,执行完成后返回。与该函数比较类似的是glFlush,不过glFlush是异步的、非阻塞式的。

使用接口:void glFinish(void)

第八步:送显,在绘制完成后,调用egl接口来完成Framebuffer的交换,从而送显。

使用接口:EGLBoolean eglSwapBuffers(EGLDisplay dpy, EGLContext ctx)

以上就是使用2D纹理进行绘制的过程了。在我们实际使用过程中,我们需要不断更新我们的纹理,一般我们可以通过PNG图片或者直接将YUV原始数据转换为纹理,通过不断更新纹理达成更新画面内容的目的。

Happy
Happy
100 %
Sad
Sad
0 %
Excited
Excited
0 %
Sleepy
Sleepy
0 %
Angry
Angry
0 %
Surprise
Surprise
0 %
默认图片
FranzKafka95
极客,文学爱好者。如果你也喜欢我,那你大可不必害羞。
文章: 51

留下评论

zh_CNCN