Android和OpenGL es讲解
文章目录
前言
为了学习Android GUI之Surfaceflinger做铺垫-由于GUI系统是基于OpenGL/EGL实现的,所以为了做到事半功倍,进而先学习它。我们会从 OpenGL es是什么、OpenGL es的版本区别、EGL是什么、需要知道的方法、在Android中使用OpenGL es的实现步骤等。
1. OpenGL ES分别是什么?
OpenGL: 渲染2D、3D矢量图形的跨语言、跨平台的应用程序编程接口(API),使简单的图形构建出复杂的三维景象。
OpenGL es: OpenGL三维图形 API 的子集,针对手机、PDA和游戏主机等嵌入式设备而设计。由于OpenGL的运行对设备要求较高,很难直接应用到CPU和内存匮乏,用电量严格控制甚至没有浮点数硬件协助的嵌入式设备上-所以诞生了es…
OpenGL es较于OpenGL:
- 尽量精简实现方式。如:在OpenGL中一种问题可以找到N种解法,但是在es中只有固定的一种
- 保证兼容性
- 不断改进。根据硬件升级来完善自己
2. 为什么要使用OpenGL es?
- 系统中CPU和GPU是协同工作的,CPU把计算好的显示内容提交给GPU,GPU渲染完成后将渲染结果放入帧缓冲区,随后视频控制器会按照VSync信号逐行读取帧缓冲区的数据,经过可能的数模转换传递给显示器显示。所以尽可能让CPU和GPU各司其职发挥其作用提高渲染效率的关键。
- 由于OpenGL es为我们提供了访问GPU的能力,还引入了缓存Buffer机制大大提高的效率。
- 从一个内存区域复制到另一个内存区域的速度是相对较慢的,并且在内存复制的过程中,CPU 和 GPU 都不能处理这区域内存,避免引起错误。此外CPU / GPU 执行计算的速度是很快的,而内存的访问是相对较慢的,这也导致处理器的性能处于次优状态,这种状态叫做 数据饥饿,简单来说就是空有一身本事却无用武之地。
- OpenGL 为了提升渲染的性能,为两个内存区域间的数据交换定义了缓存。缓存是指 GPU 能够控制和管理的连续 RAM。程序从 CPU 的内存复制数据到 OpenGL es的缓存。通过独占缓存,GPU 能够尽可能以有效的方式读写内存。 这也意味着 GPU 使用缓存中的数据工作的同时,运行在 CPU 中的程序可以继续执行。
3. OpenGL es的版本
OpenGL es版本 | Android版本 | 描述 |
---|---|---|
OpenGL es1.0 | Android1.0+ | OpenGL es1.x是针对固定硬件管线(Fixed Pipeline)的,通过它内建的functions来设置诸如灯光、vertexes(图形的顶点数)、颜色和camera。 |
OpenGL es2.0 | Android2.2(API 8)+ | OpenGL es2.x是针对可编程硬件管线(Programmable Pipeline)的,不兼容OpenGL es1.x,需要自己动手编写任何功能。2.0相比于1.0更具灵活性,功能也更强大。可以自定义顶点和像素计算,可以让表现方式更加准确。 |
OpenGL es3.0 | Android4.3(API 18)+ | 向下兼容OpenGL es2.x,是OpenGL es2.0的扩展,支持许多新的渲染技术、优化和显示质量改进,包括引入了纹理相关的新功能,对着色语言进行了重大更新和支持着色器新功能的API特性,引入与几何形状规范和图元渲染控制相关的新功能,引入了新的缓冲区对象,增添了许多与屏幕外渲染到帧缓冲区对象相关的新功能。 |
OpenGL es3.x | Android5.0 (API 21)+ | 向下兼容OpenGL es3.0/2.0,Android5.0(API 21)和更高的版本支持这个API规范。 |
4. EGL是什么?
本地窗口系统和Rendering API(这里是指OpenGL es)之间的一层接口,EGL主要负责图形环境管理、Surface/buffer绑定、渲染同步等。
另外,EGL定义了控制Displays、Contexts以及Surfaces的统一平台接口,但一般情况在Android平台上开发OpenGL es应用,无需直接使用javax.microedition.khronos.egl包中的类按照EGL步骤来使用OpenGL es绘制图形,因为在Android平台中提供了一个android.opengl包,GLSurfaceView类提供了对Display、Surface和 Context的管理,大大简化了OpenGL es的程序框架,对应大部分 OpenGL es开发,只需调用一个方法来设置OpenGLView需要的GLSurfaceView.Renderer即可。
主要提供了如下功能:
- 创建rendering surface,Surface的字面意思是“表面”,通俗地讲就是能够承载图形的介质,如一张“画纸”。只有成功申请到Surface,应用程序才能真正“作图”到屏幕上。
- 创造图形环境(graphics context) OpenGL es 说白了就是一个Popeline,因而它需要状态管理-这就是context的主要工作:
- 同步应用程序和本地平台渲染API;
- 提供了对显示设备的访问;
- 提供了对渲染配置的管理
Mesa 3D: 引擎库,Mesa 是兼容OpenGL协议的3D图形处理软件库;同时还是开放原始代码的 如下:
4.1 egl.cfg
图形渲染采用的方式(硬件 、软件)是在系统启动后动态确定的-framework/native/opengl/libs/egl/Loader.cpp,如果是在硬件加速的情况下,系统首先要加载相应的libhgl库;否则加载libagl来由CPU进行图形处理。参数解析 第一个参数代表显示设备,硬件库1,软件库0,第三个参数是库的名称。
4.2 OpenGL函数执行
当调用其中任何接口时,egl都会自动加载OpenGL的实现库-通过解析egl.cfg文件来判断是加载软件库还是硬件库。
4.3 EGL接口解析
以下接口来源:framework/native/opengl/include/egl/Egl.h
4.3.1 eglGetDisplay
接口得到的是EGLDisplay就是一个与具体系统平台无关的对象,对于任何需要使用EGL的应用来说,首先就需要调用eglGetDisplay来取得设备的Display。
4.3.2 eglGetError
他是返回当前EGL中已经发生的错误的信息。
4.3.3 egllnitialize
这个函数就是将GRL内部数据进行初始值设定,并返回当前版本号
4.3.4 eglGetConfigs
初始化EGL完成后,下一步要获取一个最佳的Surface。方法有两种:其一是通过查询当前系统中所有Surface的配置(Configuration),然后手动选择一个:其二就是填写我们的需求,然后由EGL推荐一个最佳匹配的Surface。
这个函数的使用分为两种情况:
- 如果将入参configs设为NULL,则能得到当前系统中所有Surface配置的数量(numConfigs)。
- 否则,我们需要指定maxReturnConfigs,然后EGL会把结果填充到configs中。
4.3.5 eglGetConfigAttrib
EGLConfig包含了一个有效Surface的所有详细信息,如颜色数量、额外的缓冲区、Surface类型等重要属性。此接口就是来指定需要查看的具体属性项。
4.3.5 eglChooseConfig
自动选择并直接返回匹配结果
4.3.6 eglCreateWindowSurface
一旦我们选择好最佳的EGLConfig,接下来就可以创建一个window了。用于在终端屏幕显示
4.3.7 eglCreatePbufferSurface
此接口生成的结果则是“离屏”(off-screen)的渲染区。所有适用windowSurface的渲染方法同样能被Pbuffer surface使用,只不过执行的结果不需要通过swap buffer来最终输出到屏幕上。
4.3.8 eglCreateContext
OpenGL是一个状态机,需要对诸多状态进行管理。所以此接口的诞生
4.3.9 eglMakeCurrent
一个进程中可能会同时创建多个Context,所以我们必须选择其中的一个作为当前的处理对象。
5. OpenGL es呈现形状
在OpenGL es中只有点、直线和三角形,点和直线可以用于某些效果,但是只有三角形才能用来构建复杂的对象和纹理场景。具体使用是将点放到一个组里构建出三角形,再告诉OpenGL es如何连接这些点。如果想要构建出更复杂的图形,例如拱形,圆球等等,那么我们就需要足够的点拟合这样的曲线。
两个重点渲染方法:
- GLSurfaceView: 渲染表面类(android.opengl.GLSurfaceView)自动负责管理EGL执行步骤,但是用户需要确定针对渲染表面OpenGL
es的版本,即调用setEGLContextClientVersion(int
version)方法。然后调用setRenderer()方法来为OpenGL
es配置渲染表面。此外还有其它的setEGL*方法去配置上下文环境,例如渲染表面的RGB颜色分量的位深。 - GLSurfaceView.Renderer: 渲染器类(android.opengl.GLSurfaceView.Renderer),GLSurfaceView 需要通过渲染器对象完成实际的渲染操作,自定义渲染器对象需要继承 Renderer 接口类,并实现三个方法:onSurfaceCreated()、onSurfaceChanged()和onDrawFrame()。
6. OpenGL es的使用步骤
- 创建GLSurfaceView组件使用 Activity 来显示 GLSurfaceView 组件。
- 为GLSurfaceView 配置渲染类GLSurfaceView.Renderer,实现GLSurfaceView.Renderer接口并重写3个方法。
- 调用GLSurfaceView组件的setRenderer()方法指定Renderer对象,该Renderer对象会完成GLSurfaceView里的3D图形的绘制。
- onSurfaceCreated(): 当Surface被第一次创建或从其他Activity切换回来都会调用此方法,方法中还可以初始化OpenGL es图形(背景色)。
- onSurfaceChanged(): 在Surface被创建后,每次Surface尺寸变化,还有屏幕横竖切换都会调用此方法。
- onDrawFrame(): 每绘制一帧都会调用此方法,在这个方法中必须绘制点什么,即使只是清空屏幕。因为如果什么都没画,会导致屏幕不断闪烁。重点:此方法是绘制图形的主要执行点。
6.1 创建GLSurfaceView实例
通过GLSurfaceView来初始化OpenGL es,如配置显示设备Display 以及在后台线程中渲染。在此之前,我们需要对设备的进行版本检查,以及在Android Activity生命周期的维护、渲染请求方式进行配置,最后才会配置这个Surface视图和传入自定义Renderer类。
import android.app.ActivityManager;
import android.content.Context;
import android.content.pm.ConfigurationInfo;
import android.opengl.GLES20;
import android.opengl.GLSurfaceView;
import android.os.Bundle;
import android.util.Log;
import androidx.appcompat.app.AppCompatActivity;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
public class MainActivity extends AppCompatActivity {
private static final String TAG = MainActivity.class.getName();
private GLSurfaceView glSurfaceView;
private boolean isRendererSet;
private ActivityManager activityManager;
private ConfigurationInfo configurationInfo;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//检测版本 OpenGL es
activityManager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
configurationInfo = activityManager.getDeviceConfigurationInfo();
//获取版本号 输出196608 16进制=0x30000 3.0版本
int glVersion = configurationInfo.reqGlEsVersion;
Log.i(TAG, "glVersion=" + glVersion);
if (glVersion >= 196608) {
Log.i(TAG, "OpenGLES version is 3.0 or more");
isRendererSet = true;
//执行3.0版本的方法
} else if (glVersion >= 131072) {//输出131072 16进制=0x20000 2.0版本
Log.i(TAG, "OpenGLES version is 2.0");
isRendererSet = true;
//执行2.0版本的方法, 配置ES 2.0的上下文
glSurfaceView = new GLSurfaceView(this);
//确定针对渲染表面 OpenGL ES 的版本
glSurfaceView.setEGLContextClientVersion(2);
glSurfaceView.setRenderer(new glEs20Renderer());
//1.连续刷新频率不停渲染,0.按请求来渲染
/**
* 1.RENDERMODE_CONTINUOUSLY:按设备刷新频率不断地渲染。
* 0.RENDERMODE_WHEN_DIRTY:按请求方式来渲染。
*/
glSurfaceView.setRenderMode(1);
setContentView(glSurfaceView);
} else {
Log.i(TAG, "Current devices do not support OpenGLES");
}
}
@Override
protected void onResume() {
super.onResume();
if (glSurfaceView != null) glSurfaceView.onResume();
}
@Override
protected void onPause() {
super.onPause();
if (glSurfaceView != null) glSurfaceView.onPause();
}
}
6.2 创建Renderer类
class glEs20Renderer implements GLSurfaceView.Renderer {
/**
* GL10是OpenGLES 1.0的API遗留下来的。
* 但是要编写使用OpenGLES1.0的渲染器,就用这个参数。
* 但是对于OpenGLES2.0就直接通过静态方法GLES20来直接获取。
* OpenGLES3.0也是直接使用GLES30。
*/
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
//第一次创建或切换回来都会调用此方法
//其中的glClearColor是设置清空屏幕用的颜色,四个参数:红绿蓝透明,都是float类型,最大值为1,最小值为0。
GLES20.glClearColor(1.0f, 0.0f, 0.0f, 0.0f);
}
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
//每次Surface尺寸变化,比如屏幕横竖切换都会调用此方法
//其中的glViewport是设置视口的尺寸,告诉OpenGLES用来显示Surface的大小。
GLES20.glViewport(0, 0, width, width);
}
@Override
public void onDrawFrame(GL10 gl) {
//每绘制一帧都会调用此方法
//其中的glClear(GL_COLOR_BUFFER_BIT)表示清空屏幕,并会用glClearColor再次填充整个屏幕。
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
}
}
通过上面例子我们了解了OpenGL ES2.0程序的基本写法,了解到必须要有GLSurfaceView渲染表面和GLSurfaceView.Renderer渲染器才可以实现。同时我们也清楚知道OpenGL es绘画的内容主要在Rederer里面去编写的。
6.3 GLSurfaceView的特点
- 管理EGLDisplay,它表示一个显示屏
- 管理Surface(本质上就是一块内存区域)
- GLSurfaceView会创建新的线程,以使整个渲染过程不至于阻塞UI主线程2
- 用户可以自定义渲染方式,如通过setRenderer()设置一个Renderer
6.4 Surface创建过程
通过查看源码发现GLSurfaceView继承至SurfaceView又继承之View,由于应用程序的View树一定会通过ViewRoot申请到一个Surface,这里注意 SurfaceView中的surface与ViewRoot的surface不是一个。surfaceview中的surface是另外分配得到的。如下图:
简单描述:ViewRoot成功AttachToWindow后,重载dispatchAttachedToWindow,SurfaceView收到AttachedToWindow成功消息后通过getWindowSession向ViewRoot获取一个IWindowSession(WMS一个介质),surfaceView在updateWindow时利用IwindowSession.relayout重新申请一个surface,surfaceview在得到surface后最通知所有注册了callback的对象
6.5 mGLThrad
在创建GLSurfaceView时会启动一个线程防止主线程阻塞。追朔代码发现有一个sGLThreadManager他管理的是不同线程间的互斥访问。
下面我们看看GLThread 中的 guardedRuncao()
private void guardedRun() throws InterruptedException {
...
while (true) {
synchronized (sGLThreadManager) {
while (true) {
if (mShouldExit) {
return;
}
if (! mEventQueue.isEmpty()) {
event = mEventQueue.remove(0);
break;
}
...
// ...//释放surface 页面暂停
if (pausing && mHaveEglSurface) {
if (LOG_SURFACE) {
Log.i("GLThread", "releasing EGL surface because paused tid=" + getId());
}
stopEglSurfaceLocked();
}
...
// Have we lost the SurfaceView surface?
//判断SURFACE 是否丢失等操作
if ((! mHasSurface) && (! mWaitingForSurface)) {
...
if (mHaveEglSurface) {
stopEglSurfaceLocked();
}
mWaitingForSurface = true;
mSurfaceIsBad = false;
sGLThreadManager.notifyAll();
}
// Have we acquired the surface view surface?
if (mHasSurface && mWaitingForSurface) {
...}
// Ready to draw?
if (readyToDraw()) {//根据自定义的Renderer来渲染的
if (! mHaveEglContext) {//判断有没有 EGL context
...//是否建立有效的Context
}
...
if (mHaveEglSurface) {//确保我们有EglSurface
...
mRequestRender = false;
sGLThreadManager.notifyAll();
...
break;
}
sGLThreadManager.wait();//通过wait结束循环
}
} // end of synchronized(sGLThreadManager)
if (event != null) {
event.run();
event = null;
continue;
}
if (createEglSurface) {
...//需要创建EglSurface
}
if (sizeChanged) {
...//通知应用层尺寸变化
}
int swapError = mEglHelper.swap();//通过swap 渲染显示内容
...
} finally {
...
}
由于这个方法很长很多没做详细说明细则可以自行阅读,目前总结以下几点:
- 整个方法分为两个循环,内外
- 如果事件队列有元素就会跳出内循环,然后在外循环中处理部分
- 当前状态是否适合渲染,是否有时间要通知应用层
- 需要渲染也跳出内循环
- 处理事件和渲染
- 持续循环
6.5.1 readyToDraw()
- 程序当前不处于暂停
- 已经成功获得Surface
- 有合适的尺寸
- 处于持续自动渲染模式或者用户发起渲染
6.6 EglHelper
它是对EGL的一层快捷封装
private static class EglHelper {
...
public void start() {
...
mEgl = (EGL10) EGLContext.getEGL();//获取EGL实例
mEglDisplay = mEgl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY);//获取EglDisplay
...
//用上面的EGLdISPLAY来初始化EGL
int[] version = new int[2];
if(!mEgl.eglInitialize(mEglDisplay, version)) {//初始化
throw new RuntimeException("eglInitialize failed");
}
GLSurfaceView view = mGLSurfaceViewWeakRef.get();
if (view == null) {
mEglConfig = null;
mEglContext = null;
} else {
mEglConfig = view.mEGLConfigChooser.chooseConfig(mEgl, mEglDisplay);
//选取EGL配置
mEglContext = view.mEGLContextFactory.createContext(mEgl, mEglDisplay, mEglConfig);
}
//创建一个EGLContext 来控制Opengl es 状态机运转环境
if (mEglContext == null || mEglContext == EGL10.EGL_NO_CONTEXT) {
mEglContext = null;
throwEglException("createContext");
}
if (LOG_EGL) {
Log.w("EglHelper", "createContext " + mEglContext + " tid=" + Thread.currentThread().getId());
}
mEglSurface = null;
}
}
执行start后,就可以利用EGL和Renderer进行Opengl es 渲染,在通过EglHelper.swap()来绘制surface到屏幕。
public int swap() {
if (! mEgl.eglSwapBuffers(mEglDisplay, mEglSurface)) {
return mEgl.eglGetError();
}
return EGL10.EGL_SUCCESS;
}
7. 3D图形学基础
7.1 计算机3D图形
三维的图形,一般情况下及指长、宽和深度(高度)三个维度。但是即便我们所说的计算机中的3D图形,也只是在2D屏幕上创造出来的立体效果,那就问题的关键就在于,如何才能在二维的界面上创造出三维的“错觉”。
- 光线是感知物体的基础
- 人体有左右两个眼球,并且两者之间有一定间距,这样他们可以从不同角度来获取到一个物体的信息。这些信息在传输到两个眼球时有略微差别的,在经过大脑的处理后,便形成了物体的三维效果。
7.2 图形管线
在计算机图形处理中,任何复杂的图像都可以由固定数量的基础几何元素,通过一系列手法逐步加工出来。以OpenGL es为例,它只支持三种基本的几何元素:
- 点
- 线段
- 三角形
图形管线:图形硬件设备(GPU)支持的渲染流程。但是由于绝大多数用户都是在二维的终端显示屏上所以在 OpenGL es的 3D 空间中,屏幕和窗口却是 2D 像素数组,这就导致 OpenGL es的大部分工作都是关于把 3D 坐标转变为适应屏幕的 2D 像素。3D 坐标转为 2D 坐标的处理过程是由 OpenGL es的图形渲染管线(Graphics Pipeline,实际上指的是一堆原始图形数据途经一个输送管道,期间经过各种变化处理最终出现在屏幕的过程)管理的。图形渲染管线可以被划分为两个主要部分: - 第一部分: 是把3D 坐标转换成 2D 坐标;
- 第二部分: 是将2D 坐标转换成有颜色的像素;
2D坐标和像素的区别: 2D 坐标精确表示一个点在 2D 空间中的位置,而 2D 像素是这个点的近似值,2D 像素受到屏幕/窗口分辨率的限制。
OpenGL ES 采用C/S编程模型,客户端运行在 CPU 上,服务端运行在 GPU 上,调用 OpenGL ES 函数的时,由客户端发送至服务器端,并被服务端转换成底层图形硬件支持的绘制命令。
8. 总结
GLSurfaceView中涉及的EGL接口都是Java层的,而且很好的封装在其中-那么也就意味着其几乎不同添加任何代码就拥有Opengl es的环境。
那么通过上面的分析描述GLSurfaceView使用EGL的步骤大致如下:
- 获取一个EGL实例
- 获取一个EGL Display
- 利用Display来初始化EGL并返回EGL版本号
- 获取EGL配置
- 创建EGL Context
- 创建EGL Surface
- 通过Swap来渲染屏幕