Physical Address:
ChongQing,China.
WebSite:
相信不少朋友一定听说过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原始数据转换为纹理,通过不断更新纹理达成更新画面内容的目的。