Android OpenGL 开发 1 - OpenGL ES 3.0 简介

本文略讲了 Android OpenGL ES 3.0 的知识。

前言

自计算机诞生以来,图形处理就是其应用领域中的一个重要的方面,不管是用于科学研究、设计还是娱乐,强大的图形处理能力都是人们梦寐以求的。进入 PC 时代以来,和 CPU 相比,图形处理单元(GPU)的发展速度有过之而无不及。在移动设备大行其道的今天,图形处理的领域又扩展到了掌上娱乐等方向,与人们的日常生活更加息息相关。

如何充分利用 GPU 强大的性能呢?精心开发的驱动程序和 API 是其中的关键,有了它们,应用和游戏程序开发者才能够从繁琐的绘图实现中解放出来,将其才华应用到 app 中真正闪光的创意上。在 PC 时代出现的众多 API 和程序库中,Khronos 组织开发的 OpenGL 无疑是各种翘楚。OpenGL 是针对 GPU 驱动的一套标准接口,它所提供的丰富功能、极佳的性能以及跨平台的特性得到了广大硬件供应商和编程开发人员的喜爱,很快成为最受欢迎的桌面图形 API 之一。

随着时代的发展,移动和嵌入式设备日益成为人们生活中不可或缺的一部分,受限于较低的性能、硬件规格以及平台供应商的多样性,这些设备上的图形变成多年来一直困扰着设备制造商和应用开发人员。人们自然地想到,如果在移动平台上有类似于桌面 OpenGL 那样的 API,那该是多么令人期待的事情。Khronos 不负众望,在 OpenGL 的基础上开发出了用于手持和嵌入式平台的 OpenGL ES API 及配套的着色语言。这个程序库不仅保留了 OpenGL 中丰富的功能,还引入了许多根据目标设备特点优化的特性,为移动图形应用打开了一扇窗户,很快成为了业界领先的图形 API,被 Apple、Google 等移动平台供应商所采用和支持。

从 1.0 版本开始,随着硬件平台的发展以及桌面 OpenGL 的升级换代,OpenGL ES 也推出了数个版本,OpenGL ES 3.2 是其最新版本,它充分利用了硬件发展的最新成果,在性能上所做的妥协更少,对桌面版本功能的保留和兼容性更好,从而使得更多的桌面图形开发人员投身于移动平台,大大推进了这些平台上图形应用的发展。

EGL

执行 OpenGL ES 命令需要一个渲染上下文(rendering context)和一个绘制表面(adrawing surface)。

  • 渲染上下文存储相关的 OpenGL ES 状态。在 app 调用任何 OpenGL ES 的指令之前,需要首先创建一个 OpenGL ES 的上下文。这个上下文是一个非常庞大的状态机,保存了 OpenGL ES 中的各种状态,这也是 OpenGL ES 指令执行的基础。OpenGL ES 的函数不管在哪个语言中,都是类似 C 语言一样的面向过程的函数,本质上都是对 OpenGL ES 上下文这个庞大的状态机中的某个状态或者对象进行操作,当然我们得首先把这个对象设置为当前对象。由于 OpenGL ES 上下文是一个巨大的状态机,切换上下文往往会产生较大的开销,但是不同的绘制模块,可能需要使用完全独立的状态管理。因此,可以在 app 中分别创建多个不同的上下文,在不同线程中使用不同的上下文,上下文之间共享纹理、缓冲区等资源。这样的方案,会比反复切换上下文,或者大量修改渲染状态,更加合理高效。

  • 绘制表面是用于绘制图元的 surface,它指定渲染所需的缓冲区类型,例如颜色缓冲区、深度缓冲区和模版缓冲区。绘制表面还要指定所需的缓冲区的位深度。

OpenGL ES API 没有提及如何创建渲染上下文,或者渲染上下文如何连接到原生窗口系统。这部分功能规范是由 EGL 定义的。EGL 是 Khronos 渲染 API(如 OpenGL ES)和原生窗口系统之间的接口,和 OpenGL ES 是两个不同的规范;OpenGL ES 专注于与 GPU 打交道,目的是通过 GPU 的计算,得到一张图片,这张图片在内存中其实就是一块 buffer(缓存/缓冲区),存储有每个点的颜色信息等。而这张图片最终是要显示到屏幕上的,所以还需要具体的窗口系统来操作,OpenGL ES 并没有定义相关的函数。所以 OpenGL ES 有一个好搭档 EGL。

EGL,全称:embedded Graphic Interface,是 OpenGL ES 和底层 Native 平台窗口系统之间的接口。EGL 专注于与原生窗口系统打交道;OpenGL ES 工作组并没有硬性要求在实现 OpenGL ES 时,必须同时实现 EGL。开发者应该参考平台供应商的文档,以确定系统支持哪个接口。目前未知,市面上常见的系统,如 Windows、MacOS、Linux、Android、iOS 等,都实现了 EGL 规范。唯一要注意的是,iOS 系统提供的是 EAGL 实现,与标准 EGL 规范存在部分差异。

所有 OpenGL ES app 都必须在开始渲染之前使用 EGL 执行如下任务:

  • 查询并初始化设备可用的显示器。通过 EGL 获取到手机屏幕 的 handle,获取到手机支持的颜色配置(RGBA 8888/RGB 565 等)。例如,翻盖手机可能有两个液晶面板,我们可以使用 OpenGL ES 渲染可在某一或者两个面板上显示的 surface。
  • 创建渲染表面。根据颜色配置创建一块包含默认 buffer 的 surface,buffer 的大小是屏幕分辨率乘以每个像素信息所占大小。EGL 中创建的表面可以分为屏幕上的表面或者屏幕外的表面(离屏 surface)。屏幕上的表面连接到原生窗口系统;而屏幕外的表面是不显示但是可以用作渲染表面的像素缓冲区。这些表面可用于渲染纹理,并可以在多个 Khronos API (如 OpenGL 和 OpenVG)之间共享。
  • 创建渲染上下文。创建用于存放 OpenGL ES 状态集的 Context,并将状态启用。EGL 是创建 OpenGL ES 渲染上下文所必需的。这个上下文必须连接到合适的表面才能开始渲染。
  • 通过 OpenGL ES 操作 GPU 进行计算,将计算的结果保存在 surface 的 buffer 中。最后使用 EGL 将绘制的图片显示到手机屏幕上

EGL API 实现上述功能,以及像电源管理、在一个进程中支持多个渲染上下文、在一个进程中跨渲染上下文共享对象(如纹理或者顶点缓冲区)这样的附加功能,并提供获取给定 EGL 实现所支持的 EGL 或 OpenGL ES 扩展函数的函数指针的机制。

EGL 命令语法

  • 所有 EGL 命令都以 egl 前缀开始,命令名称的命名规则是小驼峰命名法(例如 eglCreateWindowSurface)。
  • 所有 EGL 数据类型都以 EGL 前缀开始,数据类型名称的命名规则是大驼峰命名法(例如 EGLBoolean 和 EGLContext),EGLint 和 EGLenum 除外。

OpenGL ES 3.0 简介

OpenGL ES(OpenGL for Embedded Systems)是以手持和嵌入式设备为目标的高级 3D 图形 API。OpenGL ES 是当今智能手机中占据统治地位的图形 API,其作用范围已经扩展到桌面。OpenGL ES 支持的平台包括 iOS、Android、BlackBerry、bada、Linux 和 Windows,它还是 WebGL(基于浏览器的 3D 图形 Web 标准) 的基础。

从 2009 年 6 月 iPhone 3GS 发布和 2010 年 3 月 Android 2.0 发布以来,OpenGL ES 2.0 已经得到了 iOS 和 Android 设备的支持。随着时间的推移,从使用 iOS7 的 iphone 5s 和 Android 4.3 开始,iOS 和 Android 设备已经支持 OpenGL ES 3.0。OpenGL ES 3.0 向后兼容 OpenGL ES 2.0,也就是说,为 OpenGL ES 2.0 编写的 app 在 OpenGL ES 3.0 中可以继续使用。

OpenGL ES 是 khronos 组织创立的 API 套装之一。khronos 组织创立于 2000 年 1 月,是由成员提供资金的行业联盟。khronos 组织的部分成员如下:

khronos组织部分成员

khronos 组织专注于创建开放和无税版 API。khronos 组织还管理着 OpenGL,这是一个使用在 Linux、UNIX 家族、Mac OS X 和 Microsoft Windows 等桌面系统上的跨平台标准 3D API,是一个在现实中大量使用和广为接受的标准 3D API。

OpenGL ES 1.x、OpenGL ES 2.x、OpenGL ES 3.x 的规范都可以在 khronos.org/opengles 网站上找到。此外 Khronos 网站(khronos.org)上有所有最新的 Khronos 规范、开发人员留言板、教程和示例数据。

由于 OpenGL 是广泛采用的 3D API,因此从桌面 OpenGL API 开始开发手持和嵌入式设备的开放标准 3D API,然后修改它以符合手持和嵌入式设备领域需求和限制就很有意义了。在 OpenGL ES 的早期版本(1.0、1.1 和 2.0)中,设计时考虑了设备的限制,包括有限的处理能力和内存可用性、低内存带宽和电源消耗的敏感性。工作组使用如下标准定义 OpenGL ES 规范:

  • OpenGL API 规模庞大且复杂,OpenGL ES 工作组的目标是创建适合于受限设备的 API。为了实现这一目标,工作组从 OpenGL API 中删除任何冗余。在相同操作可以用多种方式执行的情况下,采用最实用的方法,将多余的技术删除。指定几何形状就是一个好的例子:在 OpenGL 中,app 可以使用立即模式、显示列表或者顶点数组;而在 OpenGL ES 中,只存在顶点数组,删除了立即模式和显示列表。
  • 删除冗余是个重要的目标,但是维护与 OpenGL 的兼容性也很重要。OpenGL ES 尽一切可能设计成 OpenGL 功能的嵌入式子集。这是一个重要的目标,因为这使得开发人员只需要编写一份代码,就可以完成基于两个 API 的 app 和工具。
  • 引入新功能来处理手持和嵌入式设备的特定限制。例如,为了降低电源消耗,提高着色器的性能,在着色语言中引入了精度限定符。
  • OpenGL ES 的设计者旨在确保实现图像质量的最小功能集。在早期的手持式设备中,屏幕尺寸有限,必须尽可能保证在屏幕上绘制的像素质量。
  • OpenGL ES 工作组希望实现某种可接受和公认的标准,以确保任何 OpenGL ES 实现都能够满足图像质量、正确性和稳定性。该标准通过开发相关的兼容性测试来实现,OpenGL ES 实现必须通过这些测试才被视为兼容。

到目前位置,Khronos 已经发布了 3 大类 OpenGL ES 规范:OpenGL ES 1.x、OpenGL ES 2.x、OpenGL ES 3.x。

  • OpenGL ES 1.x 规范采用的是固定功能管线,分别从 OpenGL 1.3 和 1.5 规范衍生而来。
  • OpenGL ES 2.0 规范采用了可编程图形管线,从 OpenGL 2.0 规范衍生而来。从 OpenGL 规范衍生意味着对应的 OpenGL 规范被用作基线,以确定特定 OpenGL ES 版本中包含的功能集。
  • OpenGL ES 3.0 是手持图形革命的下一个步骤,从 OpenGL 3.3 规范衍生而来。虽然 OpenGL ES 2.0 成功地在手持设备中引入了类似于 DirectX9 和 Microsoft Xbox 360 的功能,但是桌面 GPU 的图形能力也在继续发展。采用阴影贴图、体渲染(volume rendering)、基于 GPU 的粒子动画、几何形状实例化、纹理压缩和伽马校正等技术的重要功能在 OpenGL ES 2.0 中都不具备。而 OpenGL ES 3.0 将这些功能引入手持和嵌入式设备,同时继续适应设备的局限性。 当然,设计 OpenGL ES 旧版本时需要考虑的一些限制现在已经不再重要。例如,手持设备现在配备了大尺寸的屏幕(有些甚至提供了比大部分桌面 PC 显示器更高的分辨率)。此外,许多手持设备还配备了高性能的多核 CPU 和大内存。在开发 OpenGL ES 3.0 时,khronos 组织的重点已转向相关功能的选取,而不是应付设备的有限能力。

接下来的系列文章中,我将基于 OpenGL ES 3.0,为大家讲解关于 OpenGL ES 的相关知识:包括 OpenGL 的整体架构、着色器和程序、EGL、OpenGL ES 着色器语言等内容。

OpenGL ES 整体架构

前文讲过,自计算机诞生以来,图形处理就是其应用领域中的一个重要的方面。图形处理由专门的图形处理单元(GPU)负责。如何充分利用 GPU 强大的性能呢?精心开发的驱动程序和 API 是其中的关键。根据这种定义,我们可以将 OpenGL ES API 的使用粗略的归纳为 CS 架构(client-server),开发人员、OpenGL ES API 为 client 端,client 端的内容,经过驱动程序,传递给 GPU(Server 端)。Server 端可以是核显、独显、远程图像工作站等图像处理单元。因此我们在 Open ES 3.0 中要学习的图形管线,其实就是 GPU 处理图形数据的过程。

OpenGL ES 3.0 实现了具备可编程着色功能的图形管线,由两个规范组成:OpenGL ES 3.0 API 和 OpenGL ES 着色器语言 3.0 规范(OpenGL ES SL)。下图展示了 OpenGL ES 3.0 图形管线。

OpenGL_ES_3_0图形管线

上图描述了 OpenGL ES 3.0 的整体架构、提供的 API 及其定义的图像处理流程。

OpenGL ES 3.0 图像处理流程

在图形学的视角中,所有的图片都是由三角形构成的。所以通过 OpenGL ES 绘制图片的时候,我们需要通过 OpenGL ES API 创建用于在 GPU 上运行的 shader,然后将通过 CPU 获取到的图片顶点信息传入位于 GPU 中的 Shader 中。在 Vertex Shader 中,顶点着色器通过矩阵变换对顶点坐标做出以下变换操作:模型坐标系 »> 世界坐标系 »> 观察坐标系 »> 裁剪坐标系 »> 屏幕坐标系,最后计算出在屏幕上各个顶点的坐标。顶点坐标处理后,GPU 通过光栅化,以插值的方法得到所有像素点的信息,并在 Fragment Shader 中计算出所有像素点的颜色。最后,检查使用 OpenGL ES API 设定的状态,对得到的像素信息进行 depth/stencil test、blend 等操作,并得到最终的图片。

OpenGL ES 3.0 定义的图像处理流程为:

OpenGL_ES_3_0图像处理流程

图中带有绿色的部分为 OpenGL ES 3.0 中管线的可编程阶段。

顶点着色器

顶点着色器,英文名 Vertex Shader。顾名思义,是对图形顶点进行一系列操作的着色器。顶点除了最基本的位置属性外,还可能包含很多其他属性,比如纹理,法线等等。显卡可以通过顶点着色器确定顶点应该绘制在什么位置。在 OpenGL ES 3.0 中,顶点着色器是不可或缺的。顶点着色器作用于每个顶点,可以生成每个顶点的最终位置。针对每个顶点,它都会执行一次,一旦每个顶点的最终位置确定了,GPU就可以把这些可见顶点的集合组装成点、直线以及三角形,从而提高渲染场景和模型的速度。

着色器在官方文档中的定义就是一个运行在图形渲染管线中某个阶段的一段开发者写的程序。既然是自定义程序代码,那必然就有对应的编程语言,这个语言就是 OpenGL ES SL(在后续的文章中讲解)。通过着色器,我们可以自定义渲染的效果。由于图形渲染管线是运行在硬件中(一般是 GPU),所以着色器一般就是运行在 GPU 中,这和我们以前写的运行在 CPU 的代码就必然有所不同。

作为渲染管线的起点,顶点着色器不止承担着接收顶点的任务。作为一段拥有具体逻辑的程序,它还承受着处理顶点位置等任务,以便完成一些效果(比如位置变换、调整形状,或者三维变换),另外还负责传递从 app 传入的各种数据(比如颜色、变换矩阵、时间参数等),并将数据传递给后面阶段的任务。

图元装配

顶点着色器之后,OpenGL ES 3.0 图形管线的下一个阶段是图元装配,图元装配的英文名是 Primitive Assembly。图元是三角形、直线或者点等几何对象,传入顶点着色器的顶点数据,在图元装配期间,会被组合成图元。图元强调的是,用什么方式去将一系列顶点连接成为一个图形。

对于每个图元,必须经过裁剪和淘汰(可选)操作,以确定图元是否位于视锥体(屏幕上可见的 3D 空间区域)内。如果图元没有完全在视锥体内,则可能需要进行裁剪。如果图元完全处于该区域之外,它就会被抛弃。裁剪之后,顶点位置被转换为屏幕坐标。也可以执行一次淘汰操作,根据图片面向前方或者后台抛弃它们。裁剪和淘汰之后,图元便准备传递给管线的下一阶段-光栅化阶段。

光栅化

光栅化的英文名是 Rasterization。在光栅化阶段中,OpenGL ES 会绘制对应的图元(点、直线或者三角形)。光栅化是将图元转换为一组二维片段的过程,每个片段由屏幕空间中的整数位置(x,y)标识。然后,这些片段由片段着色器处理。这些二维片段代表着可在屏幕上绘制的像素。

光栅化

片段着色器

在光栅化阶段之后,OpenGL ES 可编程管线的下一个阶段就是片段着色器。片段着色器的英文名是 Fragment Shader。

对于片段着色器,官方的解释为:片段着色器是将光栅化生成的每个片段处理为一组颜色和单个深度值的着色器阶段。每个片段都有一个窗口空间位置和一些其他值,并且它包含来自最后一个顶点处理阶段的所有插值的顶点输出值。片段着色器的输出是一个深度值、一个可能的模板值(未被片段着色器修改)和零个或多个可能写入当前帧缓冲区的颜色值。

简单的讲,片段着色器为片段上的操作实现了通用的可编程方法,对光栅化阶段生成的每个片段执行这个着色器,如图所示,它使用一组输入,并产生一组输出。

片段着色器

片段着色器采用如下输入:

  • 着色器程序:描述片段上所执行操作的片段着色器程序的源代码或者可执行文件
  • 输入变量:光栅化单元用插值为每个片段生成的顶点着色器输出
  • 统一变量:片段(或者顶点)着色器使用的不可变数据
  • 采样器:代表片段着色器所用纹理的特殊统一变量类型

片段着色器可以抛弃片段,也可以生成一个或者多个颜色值作为输出。一般来说,除了渲染到多重渲染目标之外,片段着色器只输出一个颜色值;当需要渲染到多重渲染目标时,片段着色器会为每个目标输出一个颜色值。光栅化和片段着色器阶段生成的颜色、深度、模板和屏幕坐标位置(x,y)会变成管线逐片段操作阶段的输入。

逐片段操作

在片段着色器之后,管线的下一个阶段是逐片段操作。光栅化阶段生成的屏幕坐标为(x,y)的片段只能修改帧缓冲区中位置为(x,y)的像素。逐片段操作的英文名是 Per-Fragment Operations。在逐片段操作阶段,管线会在每个片段上执行如下功能和测试:

逐片段操作

  • 像素归属测试:像素归属测试英文名为:Pixel ownership test,这个测试用于确定帧缓冲区位置为(x,y)的像素当前是不是归 OpenGL ES 所有。这个测试使窗口系统能够确定和控制帧缓冲区中的哪些像素属于当前 OpenGL ES 上下文。例如,如果一个用于展示 OpenGL ES 帧缓冲区窗口的窗口被另一个窗口遮蔽,则窗口系统可以确定被遮蔽的像素不属于 OpenGL ES 上下文,从而完全不展示这些像素。注意虽然像素归属测试是 OpenGL ES 的一部分,但是它不由开发人员控制,而是在 OpenGL ES 内部进行。
  • 裁剪测试:裁剪测试英文名为:Scissor test。裁剪测试用于确定(x,y)是否位于裁剪矩形范围内,裁剪矩形范围是 OpenGL ES 状态的一部分。如果片段在裁剪区域之外,则片段被丢弃。
  • 模板和深度测试:模板和深度测试英文名为:Stencil and depth tests。这两个测试是在输入片段的模板和深度值上进行的,以确定片段是否应该被拒绝。
  • 混合:混合操作英文名为:Blending。混合将新生成的片段颜色值与保存在帧缓冲区中(x,y)位置的颜色值组合起来。
  • 抖动:抖动操作英文名为:Dithering。抖动可用于最小化因为使用有限精度在帧缓冲区中保存颜色值而产生的伪像。

在逐片段操作阶段的最后,片段会被以下两种方式之一处理:

  • 片段被拒绝
  • 片段颜色、深度或模板值被写入帧缓冲区的(x,y)位置。片段颜色、深度和模板值是否可以书写,取决于对应的写入标识(write masks)是否被启用。写入标识可以更精细地控制写入相关缓冲区的颜色、深度和模板值。例如,可以设置颜色缓冲区(color buffer)的写入标识,使得任何红色值都不被写入颜色缓冲区。

此外,OpenGL ES 3.0 提供了一个接口,以从帧缓冲区中读取像素。

在 OpenGL ES 3.0 中,Alpha 测试和逻辑操作(LogicOp)不再是逐片段操作的一部分,这两个阶段存在于 OpenGL 2.0 和 OpenGL ES 1.x 中。

  • 不再需要 alpha 测试是因为片段着色器可以丢弃片段;因此 alpha 测试可以在片段着色器阶段中执行。
  • LogicOp 被删除是因为它很少被 app 使用,并且 OpenGL ES 工作组没有接到独立软件供应商(independent software vendors, ISV)在 OpenGL 2.0 中支持这一特性的请求。

OpenGL ES 3.0 API

OpenGL ES API

OpenGL ES 是图形 API,OpenGL ES 的函数不管在哪个语言中,都是类似 C 语言一样的面向过程的函数,本质上都是对 OpenGL ES 上下文这个庞大的状态机中的某个状态或者对象进行操作,当然你得首先把这个对象设置为当前对象。为了方便开发者操作,OpenGL ES 提供了许多 API,其主要分为两类:图像处理过程 API 以及 OpenGL ES SL,这两类 API 彼此之间紧密相关。

图像处理过程 API 主要是上述总体图中的红色部分。

图像处理过程API

在 OpenGL ES 3.0 的可编程管线中,我们能感知和更改的只有三个阶段:顶点着色器、变换反馈和片段着色器阶段。变换反馈(Transform Feedback)是 OpenGL ES 3.0 中一个非常实用的功能,它是顶点着色器阶段之后,图元装配和光栅化之前的一个阶段,我们可以在变换反馈阶段拿到顶点着色器阶段生成的顶点信息,并做一些我们想要实现的操作。

在 OpenGL ES 3.0 中,我们也可以获取到帧缓冲区相关的信息。

OpenGL ES 3.0 提供了三种方式以方便我们向 GPU 传递数据:

  • Attributes:Attributes 意为属性,是经常发生更改的数据,比如纹理坐标、光照法线、顶点坐标、颜色数据等。在 OpenGL ES 3.0 中,属性可叫做可变变量,其通常是指顶点缓冲区/顶点数组对象,顶点数组对象位于内存中,传递到 GPU 后,就是顶点缓冲区,存在于显存中。属性(顶点缓冲区/顶点数组对象)只能传递给顶点着色器。
  • Uniform:Uniform 意为统一变量,是不可更改的数据。取名为统一变量的原因是其可以传递给顶点着色器和片段着色器,并且可以在二者中读取使用。
  • Texture Data:Texture Data 意为纹理数据,其可以传递给顶点着色器和片段着色器。由于顶点着色器主要是处理顶点数据的,所以我们将纹理数据传过去并没有太大意义。纹理数据的处理逻辑主要在片元着色器中进行。

OpenGL ES SL 介绍

着色器是 OpenGL ES 3.0 API 的一个基础核心概念。每个 OpenGL ES 3.0 程序都需要一个顶点着色器和片段着色器,以渲染有意义的图片。如果我们想要开发自己的 OpenGL ES 3.0 app,则可能需要编写许多的着色器。要想编写着色器,则需要用到 ESSL。OpenGL ES Shading Language 是 OpenGL ES 着色器语言,可以简称为 OpenGL ES SL 或者 ESSL。用这个语言可以编写小程序运行在 GPU 上。ESSL 的语法和 C 语言有很多相似之处,如果我们能理解 C 语言代码,则理解 ESSL 代码没有什么太大的难度。当然,两种语言之间也有着许多差异,比如版本规范和所支持的原生数据类型。

在这里需要先讲下 CPU 和 GPU 的区别,它们的功能都是用于计算,也都是由很多核组成,但区别在于 CPU 的核比较少,但是单个核的计算能力比较强,而 GPU 的核很多,但是每个核的计算能力都不算特别强。目前 GPU 的主要工作是用于生成图片(现在也有通过 GPU 进行高性能运算/并行运算,但是在这里不属于讨论的范围)。图片是由很多像素组成,每个像素都包含有颜色、深度等信息,为了得到这些信息数据,可以针对每个像素点进行计算,而这个计算可以通过统一的算法来完成。GPU 擅长的就是处理针对这种大规模数据使用同一个算法进行计算。这个算法,就是使用 GLSL 写成的 Shader。

ESSL 定义的内容规范大致如下:

ESSL定义的内容

在编写任何 OpenGL ES 3.0 的代码前,我们必须知道所要包含的头文件以及应用程序需要链接的库文件。理解 EGL 使用的语法以及 GL 命令名、命令参数也很有帮助。EGL 命令上面讲解过了,此处就不再赘述了。

OpenGL ES 包含的库和头文件

OpenGL ES 3.0 必须与 OpenGL ES 3.0 库 libGLESv2.lib 和 EGL 库 libEGL.lib 链接。

OpenGL ES 3.0 必须包含 OpenGL ES 3.0 头文件 <GLES3/gl3.h> 和 EGL 头文件 <EGL/egl.h>。另外应用程序可以选择性地包含 Khronos 制定的 OpenGL ES 2.0/3.0 标准扩展的头文件 <GLES2/gl2ext.h>。

#include <EGL/egl.h>
#include <GLES3/gl3.h>
#include <GLES2/gl2ext.h>

注意头文件和库的具体名称取决于平台。OpenGL ES 工作组尝试定义头文件和库的名称并制定这些文件的如何组织,但是这种安排可能不会出现在所有 OpenGL ES 实现上。所以开发人员应参考平台供应商的文档,以获取头文件和库正确的名称和组织方式。OpenGL ES 官方的头文件由 Khronos 维护,可以从 http:// khronos.org/registry/gles/ 网站获得。

OpenGL ES 命令语法

所有 OpenGL ES 命令都以 gl 前缀开始,命令名称的命名规则是小驼峰命名法(例如 glCreateShader)。类似地,所有 OpenGL ES 数据类型都以 GL 前缀开始。

有些不同的 OpenGL ES 命令可能采用不同的参数,但其作用是一样的。参数不同指的是参数数量(1~4个)、参数类型(整型 i、浮点型 f 等)、参数是否以向量(v)形式传递等的不同。下面列举两个例子。

  • 下面两条命令等价,唯一的区别是指定的同一变量值类型不同,一个是浮点型,一个是整型。

    // 2f 传递两个 float 型参数
    glUniform2f(location, 1.0f, 0.0f);
    // 2i 传递两个 int 型参数
    glUniform2i(location, 1, 0);
    
  • 下面两条命令等价,唯一的区别是其中一条命令以向量形式传递参数,另一个不是。

    GLfloat coord[4] = {1.0f, 0.75f, 0.25f, 0.0f};
    // 4fv 以向量形式传递 4 个 float 型参数
    glUniform4fv(location, coord);
    // 4f 传递 4 个 float 型参数
    glUniform4f(location, coord[0], coord[1], coord[2], coord[3]);
    

另外,OpenGL ES 定义了 GLvoid 类型,该类型表示指针,以方便 OpenGL ES 命令接收指针类型的参数。

通常我们可以使用星号指代 OpenGL ES 命令的不同风格。比如 glUniform*() 表示用于指定统一变量的命令的所有变种,glUniform*v() 表示用于指定统一变量的命令的所有向量版本。

OpenGL ES 流程串讲

在讲解了 EGL 和 OpenGL ES 的知识后,我们来汇总下整体结构。

OpenGL_ES_整体结构

以下面的图片为例,我们讲解下标准示例的整体流程。

标准示例的整体流程

首先,我们通过 EGL 获取手机屏幕,进而获取到手机屏幕对应的这个画布,同时根据手机的配置信息,在手机的 GPU 中生成另外一个画布和一个本子,本子用于记录这个画布初始颜色等信息。

而 OpenGL ES 就好像开发者的画笔,开发者需要知道自己想画什么东西。比如想画一个苹果,那么就需要通过为数不多的基本几何图元(如点、直线、三角形)来创建所需要的模型。比如用几个三角形和点和线来近似的组成这个苹果。然后根据这些几何图元建立数学模型,比如每个三角形或者线的顶点坐标位置、每个顶点的颜色。得到这些信息之后,开发者可以先通过 OpenGL ES 将 EGL 生成的画布(buffer)进行颜色初始化,通常这个初始化颜色为黑色。然后将刚才我们获取到的顶点坐标位置,通过矩阵变化的方式,进行模型变换、观察变换、投影变换,最后映射到屏幕上,得到屏幕上的坐标。这个步骤可以在 CPU 中完成,也就是在 OpenGL ES 把坐标信息传给 Shader 之前,可以选择在 CPU 中通过矩阵相乘等方式更新顶点坐标,或者是直接把坐标信息通过 OpenGL ES 传给 Shader(同时也把矩阵信息传给 Shader),通过 Shader 在 GPU 端进行坐标更新,更新的算法通过 GLSL 写在 Shader 中。这个进行坐标更新的 Shader 被称为 vertex shader,简称 VS,中文名称顶点着色器,是 OpenGL ES2.0 中最重要两个 shader 之一,其作用是完成顶点操作阶段中的所有操作。经过矩阵变换后的像素坐标信息,是屏幕坐标系中的坐标信息。在 VS 中,最重要的输入是顶点坐标、矩阵(还可以传入顶点的颜色、法线、纹理 坐标等信息),而最重要的运算结果,就是这个将要显示在屏幕上的坐标信息。VS 会针对传入的所有顶点进行运算,比如在 OpenGL ES 中绘制一个三角形和一条线,而这两个图元不共享顶点,那么在 VS 中,也就传入了 5 个顶点信息,根据矩阵变换,这 5 个顶点的坐标被转换成了屏幕上的顶点坐标信息。

再然后,当顶点坐标生成之后,我们知道了图元在屏幕上的顶点位置,而顶点的颜色在 VS 中没有发生变化,所以图元的顶点颜色我们也是知道的。接着就是根据 OpenGL ES 中设置的状态,表明哪些点连成线,哪些点组成三角形,这一步就是进行图元装配。但是图元装配生成的样子不会在 GPU 中显示,那几条线也是虚拟的线,是不会在画布 buffer (屏幕 buffer)中显示的,因为 GPU 需要进行光珊化。光栅化这一步是发生在从 VS 出来,进入另外一个 Shader(Pixel shader,也称 fragment shader)之前,在 GPU 中进行的。其作用是把线上,或者三角形内部所有的像素点找到,并根据插值或者其他方式计算出其颜色等信息(如果不使用插值,也可以使用其他的方法,这些在 OpenGL ES 和 GLSL 中都可以设置)。

找到了大量的像素点后,这些点全部都会进入 FS 阶段。在 FS 中可以对这些点的颜色进行操作,比如可以只显示这些点的红色通道(绿蓝通道的值设置为 0):某个点的 RGB 为 200,100,100。在 FS 中可以将其更新为 200,0,0。这样做的结果就是所显示的图片均为红色,只是深浅程度不同。就好像戴上了一层红色的滤镜,其他颜色均被滤掉了。所以用 FS 来做滤镜是非常方便的。

经过 VS 和 FS 之后,开发者想要画的东西,实际上就已经可以被画出来了。但是我们仍然可以根据 OpenGL ES 的设置,对新绘制出来的图片进行 Depth/Stencil Test、剔除掉被遮挡的部分、将剩余部分与原图片进行 Blend 等操作,生成新的图片。

最终生成的图片需要通过 EGL 把这个生成的画布 buffer 和手机屏幕上对应的画布 buffer 进行调换,让手机屏幕显示这个新生成的棋盘,旧的那个棋盘再去绘制新的图片信息。周而复始,不停的把棋盘进行切换(即双缓冲技术)。这就是屏幕上图片的产生过程。

最后说明一下,我们知道 CPU 现在一般都是双核或者 4 核,多的也就是 8 核或者 16 核,但是 GPU 动辄就是 72 核,多的还有上千核,这么多核的目的就是进行并行运算,虽然单个的 GPU 核不如 CPU 核,但是单个的 GPU 核足够进行加减乘除运算,所以大量的 GPU 核用在图形学像素点运算上,是非常有效的。而 CPU 虽然单个很强大,而且也可以通过多级流水来提高吞吐率,但是终究还是不如 GPU 的多核来得快。但是需要注意的一点是:在通过 GPU 进行多核运算的时候,如果 shader 中存放了判断语句,就会对 GPU 造成比较大的负荷,不同 GPU 的实现方式不同,多数 GPU 会对判断语句的两种情况都进行运算,然后根据判断结果取其中一个。

通过这个例子,我们再次清楚了 EGL 和 OpenGL ES 绘制的整个流程,而这个例子也是最简单的一个例子,其中有很多操作没有被涉及到,这些操作在后续的系列文章中会陆续提到。

OpenGL ES 3.0 新功能

OpenGL ES 2.0 开创了手持设备可编程着色器(programmable shaders for handheld devices)的时代,并在助力大量设备的游戏、app 和用户界面方面取得了巨大成功。OpenGL ES 3.0 扩展了 OpenGL ES 2.0,支持许多新的渲染技术、优化和视觉质量增强。下面概述下 OpenGL ES 3.0 中新增的主要功能。每种功能后面都将详细阐述。

纹理

纹理的英文名是 texturing 或者 texture。OpenGL ES 3.0 引入了许多和纹理相关的新功能。

  • sRGB 纹理和帧缓冲区:该功能允许 app 执行伽马校正渲染。纹理可以保存在经过伽马校正的 sRGB 空间中,在着色器中获取时反校正到线性空间,然后在输出到帧缓冲区时转换回 sRGB 伽马校正空间。通过在线性空间中正确进行照明和其他计算,可能得到更高的视觉保真度。
  • 2D 纹理数组:保存 2D 纹理数组的纹理目标。例如,此类数组可能被用于执行纹理动画。在 2D 纹理数组出现之前,此类动画通常是通过在单个 2D 纹理中平铺动画帧,并修改纹理坐标,进而改变动画帧来实现的。有了 2D 纹理数组,动画的每一帧都可以在数组的一个 2D 切片中指定。
  • 3D 纹理:虽然一些 OpenGL ES 2.0 的实现通过扩展支持了 3D 纹理,但 OpenGL ES 3.0 已将此作为强制功能。3D 纹理在许多医学成像 app 中都是必不可少的,例如那些执行 3D 数据(例如 CT、MRI 或者 PET 数据)的直接体积渲染(direct volume rendering)的 app。
  • 深度纹理和阴影比较:使深度缓冲区(depth buffer)能够存储在纹理中。深度纹理最常见的用途是渲染阴影,此时从光源的视点角度渲染深度缓冲区;然后在渲染场景时深度纹理被比较,以确定片段是否在阴影中。OpenGL ES 3.0 还允许在获取深度纹理时与深度纹理进行比较,从而允许对深度纹理进行双线性过滤(也称为百分比最近过滤 [PCF])。
  • 整型纹理:OpenGL ES 3.0 引入了渲染和读取整型纹理的能力。整型纹理可以是未规范化的有符号或无符号 8 位、16 位和 32 位整型纹理。
  • 浮点纹理:OpenGL ES 3.0 大大扩展了支持的纹理格式。支持并可以过滤半浮点(16位)纹理;也支持全浮点(32位)纹理,但不可被过滤。访问浮点纹理数据的能力有很多应用,例如用于通用计算的高动态范围纹理(high dynamic range texturing to general-purpose computation)。
  • 其他纹理格式:除了前面提到的纹理格式外,OpenGL ES 3.0 还包含了对 11-11-10 RGB 浮点纹理、共享指数 RGB 9-9-9-5 纹理、10-10-10-2 整型纹理和 8 位分量有符号规范化纹理的支持(11-11-10 代表了 RGB 不同颜色分量的位数,总共 32 位)
  • 无缝立方体贴图:Seamless cubemaps 可以翻译为无缝立方体贴图或者无缝立方图。在 OpenGL ES 2.0 中,使用立方体贴图进行渲染可能会在立方体贴图的各面之间的边界处产生伪影。在 OpenGL ES 3.0 中,可以对立方体贴图进行采样,以便过滤使用来自相邻面的数据,并移除接缝处的伪像。
  • ETC2/EAC 纹理压缩:虽然一些 OpenGL ES 2.0 实现支持供应商专用压缩纹理格式(例如高通的 ATC、Imagination Technologies 的 PVRTC 和索尼爱立信的爱立信纹理压缩(Ericsson Texture Compression)),但开发人员没有可以依靠的标准压缩格式。OpenGL ES 3.0 强制支持 ETC2/EAC 纹理压缩。ETC2/EAC 格式为 RGB888、RGBA8888、单通道/双通道 有符号/无符号的纹理数据提供压缩。纹理压缩具有更多好处,包括更好的性能(因为更好地利用了纹理缓存),以及减少 GPU 内存使用。
  • 非 2 的幂次方纹理(Non-power-of-2 NPOT):在 OpenGL ES 3.0 中,纹理可以指定为非 2 的幂次方尺寸。这在许多情况下都很有用,例如来自视频或者摄像头捕获/录制的纹理,这些纹理的尺寸通常都不是 2 的幂次方。
  • 纹理细节级别(level of detail LOD)功能 - 在 OpenGL ES 3.0 中,可以强制限制纹理的 LOD 参数,LOD 参数用于确定读取哪个 mipmap;此外,可以强制限制基础的和最大的 mipmap 级别。这两个功能相结合,使得流式传输 mipmap 成为可能。当更大的 mipmap 级别可用时,可以提高基础级别并平滑地增加 LOD 值,以提供看起来平滑的流式纹理。这一功能非常有用,例如,当需要通过网络链接下载纹理 mipmap 数据时。
  • 纹理调配(texture swizzles):OpenGL ES 3.0 引入了新的纹理对象状态,以允许独立控制纹理数据的每个通道(R、G、B、A)在着色器中映射到的位置。
  • 不可变纹理(Immutable textures):OpenGL ES 3.0 为 app 提供了在加载数据之前指定纹理的格式和大小的机制。当这样做的时候,纹理格式便不可变,OpenGL ES 驱动程序可以预先执行所有一致性和内存检查(consistency and memory checks)。通过允许驱动程序在绘制时跳过一致性检查,可以改进性能。
  • 最小尺寸增大:所有的 OpenGL ES 3.0 实现都必须支持远大于 OpenGL ES 2.0 的纹理资源。 例如,OpenGL ES 2.0 中支持的最小 2D 纹理尺寸为 64,但在 OpenGL ES 3.0 中,这一数值增加到了 2048。

着色器

着色器的英文时 shader。OpenGL ES 3.0 包括对 OpenGL ES 着色语言(ESSL v3.00)的重大更新,和支持着色器新功能的新 API。OpenGL ES 着色语言的英文为 OpenGL ES Shading Language,可以简称为 OpenGL ES SL 或者 ESSL。

  • 二进制程序文件:在 OpenGL ES 2.0 中可以以二进制格式保存着色器,但是在运行时仍然必须链接到程序中。在 OpenGL ES 3.0 中完全链接过的二进制程序文件(包括 vertext shader 和 fragment shader)可以保存为离线二进制格式,运行时不需要链接步骤。这有助于减少 app 的加载时间。此外,OpenGL ES 3.0 提供了从驱动程序中检索程序二进制文件的一个接口,因此无需任何离线工具即可使用程序二进制代码。
  • 强制的在线编译器:在 OpenGL ES 2.0 中可以选择驱动程序是否支持着色器的在线编译,这样设计的目的是减少驱动程序的内存需求。但这一功能的代价很大,开发人员不得不依靠供应商的专用工具来生成着色器。在 OpenGL ES 3.0 中,所有实现都将有一个在线着色器编译器。
  • 非方矩阵:OpenGL ES 3.0 支持方矩阵(矩阵的行列尺寸相同)之外的新矩阵类型,并在 API 中增加了相关的统一调用,以支持这些矩阵的加载。非方阵可以减少执行变换所需的指令数。 例如,如果执行仿射变换,可以使用 4 × 3 矩阵代替最后一行为 (0, 0, 0, 1) 的 4 × 4 矩阵,从而减少执行变换所需的指令。例如,执行仿射变换时,可以用 4 × 3 矩阵代替最后一行为(0, 0, 0, 1)的 4 × 4 矩阵,从而减少执行变换所需的指令。
  • 全整型支持:ESSL 3.00 支持整型(和无符号整型)标量和向量类型,以及全整型操作。有各种内置函数提供如从 int 到 float 和从 float 到 int 的转换、从纹理中读取 integer 值、将 integer 输出到整型颜色缓冲区(integer color buffers)的功能。
  • 质心采样(centroid sampling):为了避免在多重采样时产生伪影,可以使用质心采样声明顶点着色器的输出变量和片段着色器的输入。
  • 平坦/平滑的插值器(Flat/smooth interpolators):在 OpenGL ES 2.0 中,所有插值器都隐式地在图元之间采用线性插值。在 ESSL 3.00 中,可以显式声明插值器(顶点着色器输出/片段着色器输入)的 flat 或者 smooth 着色。
  • 统一变量块:统一变量可以组合为统一变量块。统一变量块可以更高效地加载,也可以在多个着色器程序之间共享。
  • 布局限定符:可以使用布局限定符声明顶点着色器输入,以显式绑定着色器源代码中的位置,而无需进行 API 调用。布局限定符也可用于片段着色器输出,以便在渲染到多个渲染目标时将输出绑定到每个目标。此外,布局限定符可以用于控制统一变量块的内存布局。
  • 实例和顶点 ID:在 OpenGL ES 3.0 中,可以在顶点着色器中访问顶点索引,如果使用实例渲染,还可以访问实例 ID。
  • 片段深度:片段着色器可以显式控制当前片段的深度值,而不是依赖于其深度值的插值。
  • 新的内置函数:ESSL 3.00 引入了许多新的内置函数,以支持新的纹理功能、片段导数、半浮点数据转换、矩阵/数学运算。
  • 放宽限制:ESSL 3.0 大大放宽了对着色器的限制。着色器不再受指令长度限制,完全支持变量循环和分支逻辑,支持数组索引。

几何形状

OpenGL ES 3.0 引入了多种与几何形状规范和图元渲染控制相关的新功能:

  • 变换反馈:可以在缓冲区对象中捕获顶点着色器的输出。这对许多在 GPU 上执行动画而不需要 CPU 干预的技术很实用。例如,粒子动画或者使用"渲染到顶点缓冲区"的物理模拟。
  • 布尔遮挡查询:app 可以查询一个或一组绘制调用的任何像素是否通过了深度测试。这个功能可以在各种技术中使用,例如,镜头光晕效果的可见性确定,以及避免在边界被遮挡的对象上进行几何形状处理的优化。
  • 实例渲染:高效地渲染包含相似几何形状但属性(例如变换矩阵、颜色或大小)不同的对象。此功能在渲染大量相似对象时很有用,例如人群的渲染。
  • 图元重启:在 OpenGL ES 2.0 中为新图元使用三角形条带时,app 必须将索引插入索引缓冲区(index buffer),以表示退化三角形。在 OpenGL ES 3.0 中,可以使用一个特殊的索引值表示新图元的开始。这避免了在使用三角形条带时生成退化三角形。
  • 新的顶点格式:OpenGL ES 3.0 支持新的顶点格式,包括 10-10-10-2 有符号和无符号的规范化顶点属性;8 位、16 位和 32 位的整型属性;以及 16 位半浮点顶点属性。

缓冲区对象

OpenGL ES 3.0 引入了许多新的缓冲区对象,以提高图形管线各个部分指定数据的效率和灵活性:

  • 统一变量缓冲区对象:用于存储/绑定大的统一变量块的一种高校的方法。统一变量缓冲区对象可用于降低将统一变量值绑定到着色器的性能消耗,这是 OpenGL ES 2.0 应用程序中的常见瓶颈。
  • 顶点数组对象:用于绑定和切换顶点数组状态的一种高效的方法。顶点数组对象本质上是顶点数组状态的容器对象。使用它们,应用程序可以在一次 API 调用中切换顶点数组状态,而不是进行多次调用。
  • 采样器对象:将采样器状态(纹理包装模式和过滤)与纹理对象分开。用于在纹理间共享共享采样器状态的一种高效的方法。
  • 同步对象:为应用程序提供检查一组 OpenGL ES 操作是否已在 GPU 上完成执行的机制。相关的一个新功能是栅栏(fence),它为应用程序提供了一种通知 GPU 应该等到一组 OpenGL ES 操作完成执行后,才能接受更多操作进入执行队列的方法。
  • 像素缓冲区对象:使应用程序能够执行异步传输数据到像素操作和纹理传输操作。这种优化主要是为了在 CPU 和 GPU 之间提供更快的数据传输。在传输操作期间,应用程序可以继续工作。
  • 缓冲区子范围映射:允许应用程序映射缓冲区的一个子区域,以供 CPU 访问。这可以提供比传统缓冲区映射更好的性能,在传统缓冲区映射中,必须是整个缓冲区对 client 端可用。 -缓冲区对象拷贝:提供了无需 CPU 干预,即可高效地从一个缓冲区对象向另一个缓冲区对象传输数据的机制。

帧缓冲区

实际生活中,如果我们需要绘图,那么我们需要准备一块画板和一些画布,画布放在画板上,绘图作在画布上。

OpenGL ES 是图形 API,就像真正的绘图一样。在 OpenGL ES 中,帧缓冲区(FrameBuffer)就是画板,但是注意帧缓冲区不是常规意义上的数据缓冲区(就像鲸鱼不是鱼一样),它并不是实际存储数据的对象。实际存储绘图数据的是纹理(Texture)或者是渲染缓冲区(RenderBuffer),他们就是 OpenGL ES 中的画布。画布需要放在画板上才能绘图。在 OpenGL ES 中,放置这些画布的位置被称为帧缓冲区的附着(Attachment)。

附着可以理解为画板上的夹子,夹住了哪个画布,就往对应画布上输出数据。

在帧缓冲区中有 3 种类型的附着,颜色附着(ColorAttachment),深度附着(DepthAttachment),模板附着(StencilAttachment)。这三种附着对应的存储区域也被称为颜色缓冲区(ColorBuffer),深度缓冲区(DepthBuffer),模板缓冲区(StencilBuffer)。

颜色附着输出绘制图像的颜色数据,也就是平时常见的图像的 RGBA 数据。如果使用了多渲染目标(Multiple Render Targets)技术,那么颜色附着的数量可能会大于一。

深度附着输出绘制图像的深度数据,深度数据主要在 3D 渲染中使用,一般用于判断物体的远近来实现遮挡的效果。

模板附着输出模板数据,模板数据是渲染中较为高级的用法,一般用于渲染时进行像素级别的剔除和遮挡效果,常见的应用场景比如三维物体的描边。

OpenGL ES 3.0 为帧缓冲区对象增添了许多与离屏渲染相关的新特性:

  • 多渲染目标(multiple render targets MRT):允许应用程序同时渲染到多个颜色缓冲区。利用 MRT 技术,片段着色器输出多个颜色,每个颜色都用于一个与之相连的颜色缓冲区。MRT 被用于许多高级的渲染算法,例如延迟着色。
  • 多重采样渲染缓冲区:使应用程序能够渲染到具有多重采样抗锯齿功能的离屏帧缓冲区。多重采样渲染缓冲区不能直接绑定到纹理,但可以使用 OpenGL ES 3.0 的帧缓冲区移动(blit)技术将它们解析为单采样纹理。
  • 帧缓冲区失效提示:OpenGL ES 3.0 的许多实现都使用基于图块渲染(tile-based rendering TBR)的 GPU。TBR 常常在必须为了进一步渲染到帧缓冲区,而毫无必要地恢复图块内容时,出现很高的性能消耗。帧缓冲区失效为应用程序提供了通知驱动程序不再需要帧缓冲区内容的机制。这使驱动程序能够采取优化步骤,以跳过不必要的图块恢复操作。对于在应用程序中实现最佳性能,这一功能非常重要,尤其是那些需要进行大量离屏渲染的程序。
  • 新的混合方程式:OpenGL ES 3.0 支持 min/max 函数作为混合方程式。

OpenGL ES 3.0 和向后兼容性

OpenGL ES 3.0 向后兼容 OpenGL ES 2.0。这意味着,任何使用 OpenGL ES 2.0 编写的 app 在 OpenGL ES 3.0 的各个实现上都能运行。后续版本的一些小修改可能会影响少数 app 的向后兼容性。如帧缓冲区对象不再在各个上下文之间共享、立方图总是使用无缝过滤、有符号定点数转换为浮点数的方法也有小修改。

虽然 OpenGL ES 3.0 向后兼容 OpenGL ES 2.0,但是 OpenGL ES 2.0 向后兼容 OpenGL ES 1.x。OpenGL ES 2.0/3.0 不支持 OpenGL ES 1.x 的固定功能管线。OpenGL ES 2.0/3.0 中可编程顶点着色器替代了 OpenGL ES 1.x 中实现的固定功能顶点单元。固定功能顶点单元实现了一个特殊的顶点转换和照明方程式,可以用它们变换顶点位置、变换或者生成纹理坐标、计算顶点颜色。类似地,OpenGL ES 2.0/3.0 中可编程片段着色器替代了 OpenGL ES 1.x 中实现的固定功能纹理组合单元。固定功能纹理组合单元实现了每个纹理单元的纹理组合阶段。纹理颜色使用一组输出组合,该输出组合是扩散颜色与前一个纹理组合阶段的输出组合,前一个纹理组合阶段是一组固定操作(如加、减、求模和点乘)。

OpenGL_ES向后兼容性

基于如下理由,OpenGL ES 工作组决定不提供 OpenGL ES 2.0/3.0 和 OpenGL ES 1.x 之间的向后兼容性:

  • 在 OpenGL ES 2.0/3.0 中支持固定功能管线意味着该 API 有不止一种方法实现相同功能,这违反了工作组采用的支持功能确定标准。可编程管线使 app 能够用着色器实现固定功能管线,所以向后兼容 OpenGL ES 1.x 没有具有说服力的理由。
  • ISV 的反馈表明,大部分游戏并不混合使用可编程管线和固定功能管线。也就是说,游戏要么使用固定功能管线,要么使用可编程管线。一旦有了可编程管线,就没有理由使用固定功能管线,因为可编程管线在可渲染特效上有更多的灵活性。
  • 如果 OpenGL ES 2.0/3.0 必须同时支持固定功能管线和可编程管线,则其驱动程序的内存占用将大得多。对于 OpenGL ES 所支持的设备,最小化内存占用是重要的设计原则。将固定功能支持分隔到 OpenGL ES 1.x API 中,在 OpenGL ES 2.0/3.0 API 中放置可编程着色器支持,可以使不需要 OpenGL ES 1.x 支持的供应商不再包含该驱动程序。

错误处理

若 app 不正确地使用了 OpenGL ES 命令,则 OpenGL ES 会生成一个错误码,该错误码将被记录。我们可以使用 glGetError 命令查询该错误码。在生成的错误码被查询之前,其他的错误将不会被记录下来。一旦错误码被查询了,则 OpenGL ES 会将当前的错误码重置为 GL_NO_ERROR,表示自上次调用 glGetError 命令以来,没有生成任何错误。除了生成 GL_OUT_OF_MEMORY 错误的命令外,执行出错,生成错误码的命令将会被忽略,并且不会影响 OpenGL ES 的状态。

glGetError 命令返回的数据是个 GLenum 类型的枚举,其有不同的取值。比如 GL_INVALID_OPERATION 表示特定命令在当前 OpenGL ES 状态下不能执行,忽略生成该错误的命令;又或者 GL_OUT_OF_MEMORY 表示执行命令时内存不足,如果当前错误代码是这个错误,则 OpenGL ES 管线的状态会被认定为 undefined。

基本状态管理

penGL ES 3.0 可编程管线的每个阶段都有一个可以启用或者禁用的状态,每个上下文维护着相应的状态值(就是上文提到的 OpenGL ES Context 本质上是个巨大的状态机)。这些状态包括混合启用(blending enable)、混合因子(blend factors)、剔除启用(cull enable)、剔除曲面(cull face)等。初始化 OpenGL ES 上下文(EGLContext) 时,这些状态会被初始化为默认值。除了 GL_DITHER 状态被默认设置为 GL_TRUE 外,其它状态的默认值均为 GL_FALSE。

我们可以使用 glEnable 和 glDisable 命令启用或禁用对应状态。

void glEnable(GLenum capability);
void glDisable(GLenum capability);

注意如果传入的参数不是有效的值,则会生成 GL_INVALID_ENUM 错误码。capability 参数的值可以是:GL_BLEND、GL_CULL_FACE、GL_DEPTH_TEST、GL_DITHER、GL_POLYGON_OFFSET_FILL、GL_PRIMITIVE_RESTART_FIXED_INDEX、GL_RASTERIZER_DISCARD、GL_SAMPLE_ALPHA_TO_COVERAGE、GL_SAMPLE_COVERAGE、GL_SCISSOR_TEST、GL_STENCIL_TEST

我们可以使用 glisEnable 命令查询某个状态是否启用。返回的结果如果是 GL_TRUE,则表示状态启用,如果返回的结果是 GL_FALSE,则表示状态被禁用。注意如果传入的参数不是有效的值,则会生成 GL_INVALID_ENUM 错误码。

GLboolean gIisEnabled(GLenum capability);

注意部分特殊的状态值,比如混合银子、深度测试值等,可以使用 glGet*** 命令查询。