基于Android OpenGL ES2 简介

150 阅读10分钟

基于Android P版本分析

OpenGL

OpenGL是一个跨平台的软件接口语言,用于调用硬件的2D、3D图形处理器;

由于只是软件接口,所以具体底层实现依赖硬件设备制造商。

OpenGL ES

OpenGL ES是OpenGL的分支,针对嵌入式设备设计的,裁剪了OpenGL部分不必要的功能;

应用场景

  • 游戏;
  • 视频播放器、编辑应用;
  • 相机、图片处理应用;
  • 对图像处理及时性要求比较高的应用;

OpenGL绘制过程

GL是先绘制好图形的轮廓,再将颜色、图案绘制到这个轮廓上;

GL中,只提供了3种基本图形:点、线、三角形。我们所熟知的图形,都是基于这3中基本图形处理拼接合成的;

渲染流程

GL展示到屏幕上的流程可以划分为3部分、7个步骤:

GL渲染流程.png

  • 确定顶点的位置,通过这些顶点绘制出指定的 图形;
  • 为图形上色,可以是纯色、渐变彩色或者是图片纹理;
  • 缓冲与展示,将上述的图形加载到帧缓冲区(FrameBuffer)中,再展示到屏幕上;

虽然渲染流程有很多个步骤,但是我们在开发OpenGL相关内容时,重点则是在顶点着色器片段着色器将数据传递给着色器3个环节上;

GL开发流程.png

基本概念

状态机

OpenGL是一个状态机,它维持自己的状态,并根据用户调用的函数来改变自己的状态;

根据状态的不同,调用同样的函数也可能产生不同的效果;

在OpenGL中,大多数元素都可以用状态来描述:

  • 颜色、纹理坐标、光源的各种参数;
  • 是否启用了光照、是否启动了纹理、是否启用了混合、是否启用了深度测试等;

OpenGL会保持状态,除非我们调用OpenGL函数来改变它;

  • 比如你用 glEnablexxx 开启了一个状态,在以后的渲染中将一直保留并应用这个状态,除非你调用 glDisablexxx 及同类函数来改变该状态或程序退出。
  • 又或者当前颜色是一个状态变量,可以把当前颜色设置为白色、红色或其他任何颜色,在此之后绘制的所有物体都将使用这种颜色,直到把当前颜色设置为其他颜色。

上下文

上面提到的各种状态值,将保存在对应的上下文(Context)中;

OpenGL ES 上下文(EGLContext):管理所有要绘制的OpenGL ES信息;

通过保存这些状态信息到EGLContext中,可以跟踪用于渲染的帧缓存、用于几何数据、颜色等的缓存。还会决定是否使用纹理、灯光等功能以及会为渲染定义当前的坐标系统等;

渲染管道

Graphics Pipelin: 渲染管道,是显示芯片内部(GPU)处理图形信号相互独立的并行处理单元,也就是渲染流程;

在 OpenGL 中,任何事物都在 3D 空间中,而屏幕和窗口却是 2D 像素数组,这导致 OpenGL 的大部分工作都是关于把 3D 坐标转变为适应你屏幕的 2D 像素;

3D 坐标转为 2D 坐标的处理过程是由 OpenGL 的图形渲染管线(Graphics Pipeline,实际上指的是一堆原始图形数据途经一个输送管道,期间经过各种变化处理最终出现在屏幕的过程)管理的;

图形渲染管线可以被划分为两个主要部分:

  1. 把3D 坐标转换为 2D 坐标;
  2. 把 2D 坐标转变为实际的有颜色的像素;

2D 坐标和像素也是不同的,2D 坐标精确表示一个点在 2D 空间中的位置,而 2D 像素是这个点的近似值,2D 像素受到你的屏幕/窗口分辨率的限制;

图形渲染管线可以被划分为几个阶段,每个阶段将会把前一个阶段的输出作为输入。所有这些阶段都是高度专门化的(它们都有一个特定的函数),并且很容易并行执行;

OpenGL ES 采用服务器/客户端编程模型,客户端运行在 CPU 上,服务端运行在 GPU 上,调用 OpenGL ES 函数的时,由客户端发送至服务器端,并被服务端转换成底层图形硬件支持的绘制命令。

左边的客户端程序通过调用 OpenGL ES 接口,将顶点,着色器程序,纹理,以及其他一些 GL 状态参数传入右边的 GL 服务端, 然后在客户端调用绘制命令的时候, GL 便会将输入的图元,逐一执行渲染管线的每个阶段,然后将每个像素的颜色值写入到帧缓存中, 最后视窗系统就可以将帧缓存中的颜色值显示在屏幕上。 此外,应用程序也可以从帧缓存中读取数据到客户端。

缓存区

OpenGL主要有3种Buffer。

帧缓存区(Frame Buffer)

帧缓存是接收渲染结果的缓存区,为GPU指定存储渲染结果的区域,存储最终渲染输出结果的地方。

它存储着OpenGL ES绘制的每个像素最终的所有信息:颜色、深度和模板值,可以简单的理解为存储屏幕上最终显示的一帧画面的区域,包含多个图像的集合,例如颜色图像、深度图像、模板图像等;

渲染缓冲区(Render Buffer)

渲染缓存存储着呈现在屏幕上的渲染图像,也被称为颜色缓冲区,是帧缓存区(Frame Buffer)的子集。

本质上是存储要显示的颜色。多个纹理对象或多个渲染缓存对象,可通过连接点连接到帧缓存对象上;

缓冲区对象(Buffer Object)

缓冲区对象就是程序员输入到 OpenGL 的数据,分为结构类和索引类的。

结构类被称为”数组缓冲区对象“或“顶点缓冲区对象”(“Array Buffer Object”或“Vertex Buffer Object”),即用来描述模型的数组,如顶点数组、纹理数组等;

索引类被称为“索引缓冲区对象”(“Index Buffer Object”),是对上述数组的索引;

纹理

纹理是一个用来保存图像的色值的OpenGL ES缓存;

纹理最通常的作用是装饰我们的物体模型,可以理解为像是贴纸一样贴在物体表面,使得物体表面拥有图案;

坐标系

OpenGL 渲染管线整个流程中,涉及了多个坐标系变化,看起来非常繁琐。但是针对 2D 图像处理,我们其实不需要关心这些变化,我们只需要了解标准化设备坐标即可。

与Android中的Canvas或者屏幕坐标体系不同(原点为屏幕左上角),GL的坐标起始位置在屏幕中心,(0,0)作为中心点,X坐标从左到右,Y坐标从下到上,在[-1,1]之间取值,再映射到屏幕上。而超出[-1,1]范围的,将不会显示在屏幕上。而无论屏幕是什么大小,什么形状,坐标范围都是不变的;

GL坐标系.png

可编程着色器

从渲染通道流程中可知,其中包含了顶点着色器和片元着色器;

这两个着色器是有先后顺序的,先执行顶点着色器 -> 图元组装 -> 光栅化 -> 片元着色器;

通俗的理解这两个着色器的作用:类似于画画,先打线稿,再上色;

顶点着色器

通过编程确定我们绘制的区域,片元(绘制区域)就是通过描述的顶点来计算生成的;

代码实例:

private static final String VERTEX_CODE = "attribute vec4 aVertexCo;\n"
        + "attribute vec2 aTextureCo;\n"
        + "uniform mat4 uVertexMatrix;\n"
        + "uniform mat4 uTextureMatrix;\n"
        + "\n"
        + "varying vec2 vTextureCo;\n"
        + "\n"
        + "void main(){\n"
        + "    gl_Position = uVertexMatrix*aVertexCo;\n"
        + "    vTextureCo = (uTextureMatrix*vec4(aTextureCo,0,1)).xy;\n"
        + "}";

顶点着色器业务:

  • 矩阵变换位置;

  • 计算光照公式生成逐顶点颜色;

  • 生成/变换纹理坐标;

    总结: 它可以用于执行自定义计算,实施新的变换,照明或者传统的固定功能所不允许的基于顶点的效果;

片元着色器

用于为绘制的区域进行颜色或图像的填充;

代码实例:

private static final String FRAGMENT_CODE = "#extension GL_OES_EGL_image_external : require\n"
        + "precision mediump float;\n"
        + "varying vec2 vTextureCo;\n"
        + "uniform samplerExternalOES uTexture;\n"
        + "void main() {\n"
        + "    gl_FragColor = texture2D( uTexture, vTextureCo);\n"
        + "}";

片段/片元着色器业务:

  • 计算颜色;

  • 获取纹理值;

  • 往像素点中填充颜色值(纹理值/颜色值);

    总结: 它可以用于图片/视频/图形中每个像素的颜色填充(比如给视频添加滤镜,实际上就是将视频中每个图片的像素点颜色填充进行修改);

图元装配

顶点着色器之后,下一个阶段就是进行图元装配,其中图元分为:点、线、三角形等,而图元装配就是将顶点数据计算成一个个图元,在这个阶段会执行,裁剪、透视分隔和Viewport变换操作;

图元类型和顶点确定将被渲染的单独图元。对于每个单独的图元及其对应的顶点,图元装配阶段执行的操作包括:将顶点着色器的输出值执行裁剪、透视分割、视口变换,之后进入光栅化阶段;

光栅化

光栅化,其实就是把矢量图形转换为像素点的过程;

光栅化.jpg

我们屏幕上显示的都是像素点,但是三维物体都是点、线、面构成的。要让点线面变成能在屏幕上显示的像素,就需要Rasterize这个过程。就是从矢量的点线面的描述,变成像素的描述

在这个阶段绘制对应的图元(点、线、三角形)。光栅化就是将图元转化成一个二维片段的过程,而这些转化的片段将由片元着色器处理,这些二维片段就是屏幕上可绘制的像素;

EGL

EGL 是 OpenGL ES 渲染 API 和本地窗口系统(native platform window system)之间的一个中间接口层,它主要由系统制造商实现。EGL提供如下机制:

  • 与设备的原生窗口系统通信
  • 查询绘图表面的可用类型和配置
  • 创建绘图表面
  • 在OpenGL ES 和其他图形渲染API之间同步渲染
  • 管理纹理贴图等渲染资源

为了让OpenGL ES能够绘制在当前设备上,我们需要EGL作为OpenGL ES与设备的桥梁。

EGL类

android.opengl.EGL14: android.opengl.EGLConfig:配置参数来获取 EGL支持的EGLConfig; android.opengl.EGLContext:创建上下文环境,存储OpenGL ES绘图的一些状态信息; android.opengl.EGLDisplay:获取默认的显示设备(渲染的目标),并初始化,是对实际显示设备的抽象; android.opengl.EGLExt: android.opengl.EGLSurface:创建EGLSurface来连接EGL和设备的屏幕,用来存储图像的内存区域;

EGL绘图步骤

  1. 获取 EGL Display 对象:eglGetDisplay();
  2. 初始化与 EGLDisplay 之间的连接:eglInitialize();
  3. 获取 EGLConfig 对象:eglChooseConfig();
  4. 创建 EGLContext 实例:eglCreateContext();
  5. 创建 EGLSurface 实例:eglCreateWindowSurface();
  6. 连接 EGLContext 和 EGLSurface:eglMakeCurrent();
  7. 使用 OpenGL ES API 绘制图形:gl_*();
  8. 切换 front buffer 和 back buffer 送显:eglSwapBuffer();
  9. 断开并释放与 EGLSurface 关联的 EGLContext 对象:eglRelease();
  10. 删除 EGLSurface 对象;
  11. 删除 EGLContext 对象;
  12. 终止与 EGLDisplay 之间的连接;