Android和OpenGL es讲解

本文深入浅出地介绍了OpenGL ES在Android中的应用,涵盖OpenGL ES的概念、版本对比、EGL核心接口、GLSurfaceView的使用以及3D图形基础。通过实例演示了如何设置渲染器、创建Surface和Context,以及如何利用EGL进行高效图形渲染。
摘要由CSDN通过智能技术生成

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.0Android1.0+OpenGL es1.x是针对固定硬件管线(Fixed Pipeline)的,通过它内建的functions来设置诸如灯光、vertexes(图形的顶点数)、颜色和camera。
OpenGL es2.0Android2.2(API 8)+OpenGL es2.x是针对可编程硬件管线(Programmable Pipeline)的,不兼容OpenGL es1.x,需要自己动手编写任何功能。2.0相比于1.0更具灵活性,功能也更强大。可以自定义顶点和像素计算,可以让表现方式更加准确。
OpenGL es3.0Android4.3(API 18)+向下兼容OpenGL es2.x,是OpenGL es2.0的扩展,支持许多新的渲染技术、优化和显示质量改进,包括引入了纹理相关的新功能,对着色语言进行了重大更新和支持着色器新功能的API特性,引入与几何形状规范和图元渲染控制相关的新功能,引入了新的缓冲区对象,增添了许多与屏幕外渲染到帧缓冲区对象相关的新功能。
OpenGL es3.xAndroid5.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的使用步骤

  1. 创建GLSurfaceView组件使用 Activity 来显示 GLSurfaceView 组件。
  2. 为GLSurfaceView 配置渲染类GLSurfaceView.Renderer,实现GLSurfaceView.Renderer接口并重写3个方法。
  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来渲染屏幕
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值