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

Android Native中使用OpenGL ES实现基本的2D绘制。
Views: 559
3 0
Read Time:6 Minute, 0 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环境来获取或配置。如何区分EGL的API与OpenGL的API呢,凡是类似于eglxxx这样的即属于EGL的API,凡是类似于glxxxx这样的即属于OpenGL的API。

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环境的初始化非常重要,如果在EGL未完成初始化的情况下去调用OpenGL的API,都会是无用功,初学者尤其需要注意。关于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,NativeWindowType  native_window , 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是保存在显存中的帧。

在该接口中,我们已经通过 eglGetDisplay接口拿到了对应的表征“屏幕”的对象,但surface的起点、大小、layer层级都未可知,这就需要通过native_window来进行设定,在Android中也就是ANativeWindow,对ANativeWindow设定其起点、宽、高、zorder,format等属性。在Android中,一般我们可以通过三种方式来获取ANativeWindow:

//方式一:通过surfaceComposerClient—>createSurface获取到surfaceControl对象,通过surfaceControl对象获取ANativeWindow
sp<ANativeWindow> anw = mSurfaceControl->getSurface();

//方式二:通过IGraphicBufferProducer转换为bufferqueue
//定义与实现在frameworks/native/libs/bufferqueueconverter/ BufferQueueConverter.h
SurfaceHolderUniquePtr getSurfaceFromHGBP(const sp<HGraphicBufferProducer>& token); ANativeWindow* getNativeWindow(SurfaceHolder* surfaceHoldser);

//方式三:通过libgui中的Surface对象创建ANativeWindow,构造时使用的surface是//IGraphicBufferProducer,而Surface本身继承自ANativeWindow,参考://frameworks/native/libs/gui/include/gui/Surface.h
sp<ANativeWindow> anw = new Surface(surface, controlledByApp);

在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, EGLConfig config,EGLContext share_context, EGLint const * attrib_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;
}

需要说明的是,第四步与第五步并没有严格的顺序要求,实际上先调用eglCreateContext再调用 eglCreateWindowSurface 也是可以的

第六步:MakeCurrent,这一步的目的是将我们将要渲染的Context绑定到Surface、Display上。如果我们拥有多个绘制线程,在线程开始运作前都需要调用MakeCurrent,以便我们渲染的Context能够与原生Window进行绑定。在一般的工程实践中,我们都会将上述步骤封装成工具类,在使用时直接调用。

使用接口: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纹理进行绘制。这里简单将其分为三个环节:创建着色器程序、获取可移动的Buffer装载元数据(可选的)、绘制与提交。

第一步:创建着色器程序shaderProgram,着色器是作用于渲染Pipeline中具有特定作用的一段程序,使用GLSL(OpenGL Shading Language)进行编码,一般我们需要设定两种类型的Shader:顶点着色器shader与片段着色器shader,调用OpenGL接口来创建着色器程序,代码示例:

加载shader

// Given shader source, load and compile it
static GLuint loadShader(GLenum type, const char* shaderSrc, const char* name)
{
    // Create the shader object
    GLuint shader = glCreateShader(type);
    if (shader == 0)
    {
        return 0;
    }

    // Load and compile the shader
    glShaderSource(shader, 1, &shaderSrc, nullptr);
    glCompileShader(shader);

    // Verify the compilation worked as expected
    GLint compiled = 0;
    glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
    if (!compiled)
    {
        ALOGI("Error compiling %s shader for %s", (type == GL_VERTEX_SHADER) ? "vtx" : "pxl", name);

        GLint size = 0;
        glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &size);
        if (size > 0)
        {
            // Get and report the error message
            std::unique_ptr<char> infoLog(new char[size]);
            glGetShaderInfoLog(shader, size, NULL, infoLog.get());
            ALOGI("msg:%s", infoLog.get());
        }

        glDeleteShader(shader);
        return 0;
    }

    return shader;
}

链接到着色器

GLuint buildShaderProgram(const char* vtxSrc, const char* pxlSrc, const char* name)
{
    GLuint program = glCreateProgram();
    if (program == 0)
    {
        LOGI("Failed to allocate program object");
        return 0;
    }

    // Compile the shaders and bind them to this program
    GLuint vertexShader = loadShader(GL_VERTEX_SHADER, vtxSrc, name);
    if (vertexShader == 0)
    {
        LOGI("Failed to load vertex shader");
        glDeleteProgram(program);
        return 0;
    }
    GLuint pixelShader = loadShader(GL_FRAGMENT_SHADER, pxlSrc, name);
    if (pixelShader == 0)
    {
        LOGI("Failed to load pixel shader\n");
        glDeleteProgram(program);
        glDeleteShader(vertexShader);
        return 0;
    }
    glAttachShader(program, vertexShader);
    glAttachShader(program, pixelShader);

    // Link the program
    glLinkProgram(program);
    GLint linked = 0;
    glGetProgramiv(program, GL_LINK_STATUS, &linked);
    if (!linked)
    {
        LOGI("Error linking program");
        GLint size = 0;
        glGetProgramiv(program, GL_INFO_LOG_LENGTH, &size);
        if (size > 0)
        {
            // Get and report the error message
            std::unique_ptr<char> infoLog(new char[size]);
            glGetProgramInfoLog(program, size, NULL, infoLog.get());
            LOGI("  msg:  %s", infoLog.get());
        }

        glDeleteProgram(program);
        glDeleteShader(vertexShader);
        glDeleteShader(pixelShader);
        return 0;
    }
    return program;
}

第二步:这一步是可选的,取决于我们原始数据的类型,如果是类似于Camera数据的流式数据(使用一块儿Buffer承载),我们通常会申请一片GraphicBuffer来与之绑定,将其转换为EGLClientBuffer,后通过eglCreateImageKHR接口转换为EGLImage,方便后续操作(绑定到2D纹理),如果原始数据为PNG图片,则不需要这一步,可以直接加载PNG图片并生成纹理ID。这里给出使用eglCreateImageKHR将buffer数据转换为2D纹理的示例代码:

EGLint eglImageAttributes[] = { EGL_IMAGE_PRESERVED_KHR, EGL_TRUE, EGL_NONE };
//pGfxBuffer为Graphic Buffer
EGLClientBuffer clientBuf = static_cast<EGLClientBuffer>(pGfxBuffer->getNativeBuffer());
mKHRimage = eglCreateImageKHR(mDisplay, EGL_NO_CONTEXT,
                                  EGL_NATIVE_BUFFER_ANDROID, clientBuf,
                                  eglImageAttributes);
if (mKHRimage == EGL_NO_IMAGE_KHR)
{
   const char* msg = getEGLError();
   LOGE("Error creating EGLImage:%s", msg);
}

需要说明的是,如需使用eglCreateImageKHR相关接口,需要开启相关宏定义:

GL_GLEXT_PROTOTYPES
EGL_EGLEXT_PROTOTYPES

在上述步骤完成之后,就可以开始真正的绘制流程。

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

使用接口: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
FranzKafka95

极客,文学爱好者。如果你也喜欢我,那你大可不必害羞。

文章: 85

留下评论

您的电子邮箱地址不会被公开。 必填项已用*标注

zh_CNCN