1 前言
从这个系列的第一篇博客到现在也快写一年了,在第一篇文章中对OpenGL进行了整体介绍,后面的博客也陆陆续续按照OpenGL程序、数据、OpenGL图像渲染管道、计算着色器、帧缓存、高级渲染技术、光影艺术、性能调优这条路线对OpenGL的整个知识树有了详细的介绍,现在到了该收尾的时候了。
在本篇博客中,主要对OpenGL扩展,OpenGL适用的平台,OpenGL的移动版本-OpenGL ES简单介绍。
2 扩展
到目前为止本系列博客中出现的示例程序都只依赖了OpenGL的核心版本,然而OpenGL的一个强大之处在于它的扩展模式支持硬件制造商、操作系统供应商甚至哪些工具和调试人员作为发布者对其进行扩展和加强。
一个扩展指的是在某个官方版本的OpenGL提供的API之外的内容,所有的扩展都被列举在官方网站上。在该网站上详细的介绍了该扩展是对于哪个版本的OpenGL开发,以及它与该版本的官方文档有什么区别。这意味着当你使用该扩展后,你当前的OpenGL的某些行为和其对应的官方文档会有一定的差异。然而流行的以及常用的扩展都会被整合至OpenGL的官方版本之中,这意味着如果你使用的是最新版本的OpenGL,则你需要使用的扩展可能并不是很多,因为它们已经成为核心部分。被整合至核心版本OpenGL的扩展清单以及这部分扩展对应的功能说明被包含在每个版本的OpenGL官方文档的末尾,这些文档都可以在上文列的官方网站上找到。
扩展有三分主要的集合,供应商、EXT和ARB(OpenGL架构审核委员会)。供应商开发的扩展在它们所提供的硬件上实现,通过首字母的缩写来表示特定的供应商,这部分缩写通常在扩展签名中体现,如AMD表示AMD公司,NV表示英伟达公司,等等。有时一个供应商的扩展也会被其他供应商支持,特别是当这个扩展被广泛接受后。EXT就表示该扩展被超过两个以上的供应商硬件支持。通常情况下一个供应商以自己的标识注册了一个扩展,另外的供应商接受了这个扩展,他联合该扩展的发布者对其进行一定修改,推出EXT版本的扩展。ARB扩展是指被OpenGL架构审核委员会(Architecture Review Board,ARB)认可的扩展,它们是OpenGL的官方部分。它们通常被大多数或者主流的硬件支持,并且可能是从供应商扩展和EXT扩展演化而来。
当扩展被广泛接受后,它就会被包含在下一个OpenGL的官方版本中,这种自然选择的方式确保了只有有用的和重要的OpenGL扩展才能被包含在OpenGL的核心版本中。
Realtech VR发布的程序OpenGL Extensions Viewer能够帮助我们了解当前电脑的OpenGL实现能够支持哪些扩展,该程序可以从其官方网站上获取。
2.1 使用扩展加强OpenGL
在使用任何一个扩展之前,你必须确保该扩展被当前的OpenGL实现所支持,通过调用函数glGetIntegerv()
可以查出当前支持的扩展数量,接着调用如下函数依次查询所支持的扩展名称。
const GLubyte* glGetStringi(GLenum name, GLuint index);
在上面的函数中,参数name
传入枚举值GL_EXTENSIONS
,参数value
传入的值需要大于等于0,并且小于所支持的扩展数。想要确定是否支持某个扩展,则需要通过上面的方式遍历完所有支持的扩展名,并和待验证的扩展名比较。
引用扩展通常可以通过如下方式加强OpenGL。
- 通过移除某个版本OpenGL官方文档中的限制,使得一些不合法的事变得合法。
- 它们能添加令牌或者扩展已经存在的函数参数的取值范围。
- 它们能够扩展着色器语言,添加一些功能,内建函数,变量或者数据类型。
- 它们直接添加了一些新的函数。
对于前两种情况,在程序中不需要做特殊的处理,直接使用在扩展中被允许的行为,或者直接使用或者提供的新的参数取值。新的取值在系统提供的库头文件中并不存在,你需要在扩展的官方文档中查询。
想要在着色器语言中使用扩展,必须在着色器代码的前面加上一行声明,告诉编译器你将会使用扩展提供的特性。例如,假如存在一个名为GL_ABC_foobar_feature的扩展,你想要在着色器语言中使用这个特性,则你需要在着色器代码的最前端加上如下声明。
#extension GL ABC foobar feature : enable
这个声明告诉OpenGL你打算在着色器中使用该扩展的函数,如果编译器知道这个扩展,该着色器就能被成功编译,即使底层的硬件并不支持这个扩展。对于这种情况,如果编译器检查到你真正使用了该扩展内部的功能,将会抛出警告信息。另外GLSL所支持的扩展都会添加预编译标记以表明它们是被支持的,例如如果GL_ABC_foobar_feature被支持,则会隐式的包含如下定义。
#define GL ABC foobar feature 1
这意味着你可以按如下方式编写代码。
#if GL ABC foobar feature
// 使用扩展foobar中的函数
#else
// 使用其他函数
#endif
这种方式使我们可以条件编译或者执行一个扩展中的函数,尽管这个扩展可能不被底层的OpenGL实现所支持。如果你的着色器必须要求底层的OpenGL实现支持某个扩展才能够正常工作,则你可以按如下方式声明。
#extension GL ABC foobar feature : require
此时如果底层的OpenGL不支持这个扩展,则着色器不能被成功编译,并且会在声明扩展的行报错。实际上OpenGL扩展的声明是可选的,也就是大多数OpenGL实现默认会启用一些扩展中的函数,但是如果你依靠这种默认实现,你的程序很可能在部分OpenGL的驱动中无法工作,因此当你想要使用某个扩展时,务必先声明扩展。
对于最后一种扩展形式,即引入了新的函数。在大多数平台上,你都不能够直接访问OpenGL驱动器,扩展中的函数也不会魔法般的出现供你调用。相反,你需要向OpenGL驱动器获取一个你需要调用的函数指针。函数指针的定义通常包含两部分,第一部分是函数指针类型的定义,第二部分是函数指针变量的定义,示例如下。
typedef void (APIENTRYP PFNGLDRAWTRANSFORMFEEDBACKPROC) (GLenum mode, GLuint id);
PFNGLDRAWTRANSFORMFEEDBACKPROC glDrawTransformFeedback = NULL;
上面的代码块声明了一个函数指针类型PFNGLDRAWTRANSFORMFEEDBACKPROC,该函数需要一个GLenum以及一个GLuint类型的参数。接着声明了一个该函数指针类型的变量glDrawTransformFeedback。实际上在大多数平台中,函数glDrawTransformFeedback()就是如此声明的。这种方式看上去十分繁琐,幸运的是,如下的头文件中已经包含了所有注册的OpenGL中的扩展的函数原型、函数指针类型以及标示变量的声明,我们只需要导入这些头文件即可。
#include <glext.h>
#include <glxext.h>
#include <wglext.h>
在OpenGL的官方注册网站可以找到上面的头文件,头文件glext.h
中包含了标准的OpenGL扩展和很多供应商提供的OpenGL扩展,头文件wglext.h
中包含了很多Windows平台的扩展,头文件glxext.h
中则包含了X提供支持的扩展,X系统是使用在Linux和很多其他的Unix衍生物和实现中的一个窗口系统。
查询函数指针的方法和特定平台的实现相关,这里不做具体介绍。可以参考原书中的函数void * sb6GetProcAddress(const char * funcname);
。
需要注意的是当获取到某个扩展中函数的指针时,并不表明当前的OpenGL实现支持该扩展,因为有时多个扩展中会定义相同的函数,有时供应商发布的驱动器只包含某个扩展的部分实现。因此在确定是否支持某个扩展时,请使用前文的标准函数。
3 平台
OpenGL时一个强大的接口集合,它较底层的天性使得程序开发着能够控制更多细节。另外核心的OpenGL代码具有跨平台和跨操作系统的可移植性。因为不同的操作系统都有不同的管理窗口的方式,因此每个操作系统也有不同的层来帮助程序和OpenGL交互。这种方式帮助来驱动实现理解缓存的类型,颜色的格式以及其他应该被用于特定目的的特征。
本系列博客是在最开始的时候已经介绍过iOS和Mac OS平台上如何开发OpenGL程序,关于在其他各个平台上使用OpenGL的细节这里不会覆盖。
3.1 Windows
在微软公司的Windows操作系统中,提供了一套和操作系统相绑定的函数集合,被称为WGL(Windows-GL)。WGL函数方法签名中都包含wgl前缀,在标识中都包含WGL_的前缀。
3.2 Mac OS
在苹果公司的Mac OS平台上,最初OpenGL是苹果推荐以及系统底层所依赖的API,但在苹果公司自己的图像处理库Metal发布后,和OpenGL相关的接口已经被标记为废弃,但是你仍然能够使用这些API来编写并发布你的应用程序。
苹果对于OpenGL版本的支持速度总是很慢的,着其中有一个原因是因为苹果将用户的体验放在了很高的位置,苹果公司的工程师们看重的是一个程序的统一性,它能够运行在当前的主流硬件之上。因此如果你的应用是只为苹果的生态开发的,那么选择Metal吧,如果你想要开发一个程序不用考虑太多和图像显卡以及驱动程序相关的事,可以选择苹果平台,如果你想使用最新的OpenGL功能,那么换个平台吧。也正因为在Mac OS中最新版的OpenGL并没有得到支持,因此本系列博客的部分技术点的示例程序在该平台上无法实现。
3.3 Linux
在Linux、Unix等类似平台的大量版本中,OpenGL已经成为3D图形渲染的跳转API。Linux提供了多种方式访问OpenGL,大多数主流的图形硬件也提供一些硬件加速方案。Mesa3D是一种OpenGL的软件实现形式,它不依赖于图形处理硬件,能够被安装在大多数X系统的服务器上。
在八十年代末期,硅谷图形公司(Silicon Graphics, SGI)采用了一套专有的二维和三维图形渲染API,它被运行在IRIS GL(Integrated Raster Imaging System Graphics Library)工作站之上。在1992年,硅谷图形公司修订了官方文档,并将其作为一个开放的工业标准公布,将其命名为OpenGL。在1993年,Brian Paul创立了一个项目Mesa3D,它是对OpenGL的软件实现,使得3D图形渲染能够被更容易的使用,不再依赖于特定的硬件供应商。Mesa最近的版本使用了Gallium3D架构使其能够支持硬件加速,并且这种硬件加速实现通常都是可用的。
现在可用的大多数计算机系统都包含某种形式的3D加速方案。现代3D图形硬件供应商通过向Mesa或者其他开源工程提供支持,或者通过发布最近的特定硬件的驱动程序的方式来支持最新版本的OpenGL。总的来说发布驱动的方式总能够快速的跟进OpenGL版本的发布。
3.4 移动平台
通过精简OpenGL而设计了其移动平台版本OpenGL ES,目前最新的版本是OpenGL ES 3.0。
3.4.1 OpenGL瘦身
OpenGL ES和OpenGL很相似,它的官方文档是通过多个版本的OpenGL整理出来的。OpenGL提供了一套很完整的3D渲染接口,这套接口非常灵活,并且可以用于开发不同类型的应用程序,从游戏到CAD制图软件以及医学程序程序都有涉及。
随着时间的发展,OpenGL相关的API因为需要支持更多的功能而被扩展,这使得整个API变得异常臃肿,甚至同一个功能会有多个不同的方式实现,因此在最新的版本的OpenGL中已经采取激进的方式移除了一些过时的特性。而OpenGL ES精简的OpenGL提供的特性集合,只包含一些常用特性,它是OpenGL的一个子集。OpenGL ES 3.0在灵活性和可用行上坐了很好的平衡,更适用于嵌入式和移动平台环境。
随着硬件成本变得更低,越来越多的功能能够在较小的半导体空间内实现,对于嵌入式的设备,用户界面也变得越来越复杂。最常见的例子就是汽车,在80年代默契第一个对于汽车计算机可视化反馈系统通过以单行或者多行文字的方式提供。这些界面提供如安全带使用,当前的燃油剩余了,形式里程等信息。后来显示屏开始流行,这些设备通常以位图的方式向用户传递信息。最近具有3D功能的系统也被整合到了汽车中,它能够向用户提供GPS导航,环境控制,娱乐以及其他密切依赖图形处理的功能。实际上在很多更新型号汽车上的设备是采用OpenGL ES 2.0来完成图形渲染的。对于航空仪器和手机而言也存在同样的科技演变过程。
早期的嵌入式3D图形接口通常和特定的硬件密切相关,这是因为被支持功能集合通常较简单,并且在不同的设备之间有很大的差异。随着每个供应商的3D引擎变得越来越复杂,想要在不同设备之间移植程序就会十分耗时,并且充满挑战。唯一的解决办法就是定义一套标准的接口,而负责维护这个标准的组织就是Khronos Group。
3.4.2 Khronos
Khronos Group在2000年有OpenGL架构审核委员会的成员成立。当时PC端已经存在很多兼容的图形处理接口,但是Khronos的目的是想要定义一套能够在个人电脑设备之外更加实用的接口。它们开放的第一套嵌入式API被就是OpenGL ES。Khronos由很多在硬件和软件工业的领头者组成,当前的成员包括AMD、Apple、ARM、Intel、Google、NVIDIA和高通等知名公司,其完整的成员可以在官方网站查询。
3.4.3 历史版本
第一个OpenGL ES版本是OpenGL ES 1.0,它以OpenGL 1.3的标准定义为基础,精简了大部分特性,它同样采用的是固定管线策略,即顶点转换和片段处理是不可编程的。OpenGL ES SC 1.0时一个专为可靠性要求极高的场景设计的版本,这种环境中汤匙讲安全性认为是至关重要的(Safety Critical),因此加上SC标示符。典型的成员应用在航空、汽车和军事领域,在这些场景中,3D程序通常用于仪表可视化,绘图和地形绘制。
ES 1.1基于OpenGL 1.5标准定义开发,它和ES 1.0版本类似,仅仅增加了如缓存对象、纹理绘制等一些有趣的新特性。
ES 2.0基于OpenGL 2.0规范开发,它和之前的版本不兼容。这个版本最大的改变就是将固定管线部分完全移除,而是采用可编程的着色器来执行顶点和片段处理逻辑。ES 2.0采用了OpenGL ES着色器语言(OpenGL ES Shading Language),它和OpenGL 2.0+相匹配的OpenGL着色器语言非常相似。
最新版本的OpenGL ES是3.0,它向后兼容OpenGL ES 2.0。这个版本从多个版本的OpenGL中移植了大量的特性,另外新版的着色器语言OpenGL ES SL 3.0也扩展了着色器的能力,OpenGL ES 3.0也支持较早版本的着色器语言。
OpenGL ES和其参考的OpenGL规范对应关系如下表。
OpenGL ES 版本 | OpenGL 版本 |
---|---|
OpenGL ES 1.0 | OpenGL 1.3 |
OpenGL ES SC 1.0 | OpenGL 1.3 |
OpenGL ES 1.1 | OpenGL 1.5 |
OpenGL ES 2.0 | OpenGL 2.0 |
OpenGL ES 3.0 | OpenGL 4.0+ |
通常情况下硬件是为某个特定版本的API制造的,这些平台可能仅支持OpenGL ES的某个版本。考虑不同的OpenGL ES版本的差异是有益的,不同版本的ES表示了不同底层依赖硬件具备的能力。
对于OpenGL而言,新的硬件通常都会支持到最新版本。但是对于OpenGL ES则不同,新硬件采用的特性类型都是基于多个因素考量的,如针对性的产品成本,典型的用途,以及系统的支持等。在最近5年半导体科技取得了很大突破,现在已经能够生产非常小型,成本划算的以及高性能的芯片。几乎所有的智能手机如谷歌公司研发的Android系统,和苹果公司研发的iPhone手机都采用OpenGL ES作为图形渲染API。
本小节不会介绍之前历史版本的OpenGL ES,而是会聚焦在OpenGL ES 3.0,另外这里不会再重复介绍图形渲染接口的特性,这部分在前面的章节中已经覆盖,这里只是介绍这些功能在OpenGL ES上和OpenGL上的差异。
3.4.4 OpenGL ES 3.0
OpenGL ES 3.0和OpenGL 4.3在API的设计上惊人的一致,它们都移除了过时的接口,具有更精简的接口集合。不同的是OpenGL 4.3添加了很多新特性,这部分特性并不被在嵌入式硬件支持。另外集合着色器,曲面细分着色器,计算着色器,浮点型的缓存对象以及其他在OpenGL上的新特性要在当前的移动设备或者嵌入式硬件上实现会太复杂,因此这部分功能并为被添加到OpenGL ES中。但是水准时间的迁移,移动版本硬件和全功能的桌面级别的图形硬件之间的界限会越来越模糊。正如平板电脑究竟应该被归类为移动设备还是桌面电脑?手持式游戏设备和汽车上娱乐设备的界限在哪儿?因此我们能够看见嵌入式硬件的能力会逐渐增强,直至能够使用所有OpenGL的功能。
顶点处理和着色
渲染的第一步是定义几何图元的顶点,顶点规范要求必须使用顶点缓存对象,或者顶点数组对象。可以使用和OpenGL 4.3同样的方式映射顶点缓存对象。通过调用函数glVertexAttribPointer()
指定顶点属性。
void glVertexAttribPointer(GLuint index,
GLuint size,
GLenum type,
GLboolean normalized,
sizei stride,
const void *ptr);
渲染图形可以调用的函数有glDrawArrays()
、glDrawArraysInstanced()
、glDrawElements()
、glDrawElementsInstanced()
和glDrawRangeElements()
。在OpenGL4.3中可以使用的函数glMultiDrawArrays()
、glMultiDrawElements()
等在OpenGL ES 3.0中并不可用。另外OpenGL ES 3.0支持顶点数组对象,该对象用于管理顶点缓存对象,可以通过函数glGenVertexArrays()
、glDeleteVertexArrays()
和glBindVertexArray()
操作,具体细节在前面的章节已经覆盖。
着色器
OpenGL ES 2.0和3.0都使用了和OpenGL 4.3相似的可编程着色器,但是仅仅支持顶点着色器和片段着色器。它们使用的着色器语言也和GLSL语言规范类似,被称为OpenGL ES着色器语言,但针对嵌入式环境和硬件做了一定调整。
多年之前,当OpenGL ES 2.0刚开始流行时,通常移动平台不会包含内置的编译器。这些平台依靠程序被开发以及为不同平台发布二进制包时去编译着色器。现在大多数移动平台都包含一个内置的编译器,但是一些嵌入式的环境可能仍然不支持运行时编译着色器。
不管怎样,在OpenGL ES中使用着色器的方式和在OpenGL中类似。相同的程序语意和着色器管理仍然有效。使用可编程图形管道的第一步是调用如下函数创建必要的着色器对象。
GLuint glCreateShader(GLenum type);
接下来以字符串方式加载着色器源码,并使用和OpenGL 4.3类似的函数编译着色器。
void glShaderSource(GLuint shader,
GLsizei count,
const char **string,
const GLint *length);
void glCompileShader(GLuint shader);
如果你运行的平台仅仅支持二进制的着色器,你需要在这里加载已经预编译好的二进制数据而不是源码。一种在运行时检查支持的二进制格式方式是查询GL_NUM_SHADER_BINARY_FORMATS
。更多的细节需要参考特定设备的SDK官方文档。如果片段着色器和顶点着色器时离线合并编译的,则仅需加载一个片段-顶点对二进制资源。
void glShaderBinary(GLsizei count,
const GLuint *shaders,
GLenum binaryformat,
const void *binary,
GLsizei length);
所有的平台都必须支持源代码或者二进制资源中的一种格式。OpenGL ES 3.0要求必须支持运行时的编译器,二进制的着色器方式是可选项。浏览你需要开发的设备的文档,确定哪种格式是最高效的。如果你开发的是Android或者iOS程序,可以直接使用源代码的方式。
上面的代码块执行完毕后后,调用如下函数创建程序对象,并将着色器对象附着在程序对象之上,然后删除着色器对象。
GLuint glCreateProgram(void);
glAttachShader(GLuint program, GLuint shader);
public func glDeleteShader(_ shader: GLuint)
当着色器被加载并编译,程序也附着这些着色器后,通过在着色器中声明的顶点属性字符串为参数,调用如下函数绑定顶点属性和对应的索引。
glBindAttribLocation(GLuint program, GLuint index, const char *name);
需要注意的是在OpenGL ES 3.0中可用直接在着色器中通过修饰符直接管理顶点属性和索引,不需要调用上述函数。
在完成上述工作后需要链接程序。
glLinkProgram(GLuint program);
如果你定义了统一变量,需要在这之后调用如下函数获取这些统一变量在当前程序中的索引。
public func glGetUniformLocation(_ program: GLuint,
_ name: UnsafePointer<GLchar>!) -> GLint
当上述逻辑都成功执行后,接下来在每次绘制场景时,你可以通过函数glUseProgram()
切换你需要使用的OpenGL程序。你可以使用在OpenGL4.3中更新统一变量类似的方法在OpenGL ES中完成同样的操作。需要注意的是在更新矩阵类型统一变量值的时候,参数transpose
必须设置为GL_FALSE
,矩阵转置的功能并不是必须的,因此不被OpenGL ES支持。下面是一些更新统一变量值的函数原型。
void glUseProgram(GLuint program);
void glUniform{1234}{if}(GLint location, T values);
void glUniform{1234}{if}v(GLint location, GLsizei count, T value);
void glUniformMatrix{234}fv(GLint location, GLsizei count,
GLboolean transpose, T value);
另外,在OpenGL ES 3.0中也支持统一闭包,和它相关的交互函数如下。
glGetUniformBlockIndex(GLuint program, const char *uniformBlockName);
glGetActiveUniformBlockName(GLuint program, GLuint uniformBlockIndex,
GLsizei bufSize, GLsizei *length,
char *uniformBlockName);
实际上OpenGL ES 3.0的追哦色全语言和GLSL 3.3几乎相同,你可以在PC或者Mac环境中开发着色器,并对其做很少的修改就能够移植到移动平台。
尽管OpenGL ES 3.0原生不支持几何和曲面细分着色器,但是它支持转换反馈模式。这种渲染模式是你可以捕获顶点着色器的输出,并将其直接存入到缓存对象中。这样你可以建立一个工程只包含顶点着色器用于处理复杂的顶点处理逻辑,并在合适的时候使用这部分数据。在前面的章节中已经对顶点反馈做了详细的介绍,这里不再深入展开。在OpenGL ES 3.0中,你可以通过如下函数使用这种顶点渲染模式。
void glGenTransformFeedback(GLsizei n, GLuint *ids);
void glDeleteTransformFeedback(GLsizei n, const uint *ids);
void glBindTransformFeedback(GLenum target, GLunit id);
void glBeginTransformFeedback(GLenum primitiveMode);
void glEndTransformFeedback();
void glPauseTransformFeedback();
void glResumeTransformFeedback();
光栅化
抗锯齿线条在OpenGL ES中不支持,另外OpenGL ES 3.0也不支持多变形平滑,多边形抗锯齿以及多重多边形模式。
纹理
在OpenGL ES 3.0中,2D纹理,2D纹理数组,3D纹理以及立方体贴图都被支持,另外它们对应的采样器对象也被支持。关于采样器对象也在前面章节中讲过,这里不再重复。OpenGL ES同样支持一种新的方式一次为整个纹理分配显存,这种方式会减少驱动器在加载纹理时的校验工作,能够提升纹理加载速度。其函数原型如下。
void glTextureStorage2D(GLenum target,
GLsizei levels,
GLenum internalformat,
GLsizei width,
GLsizei height);
ES 3.0所支持的纹理格式相对于ES 3.0已经扩展了很多,但是相对于OpenGL 4.3仍然较少,在使用一个纹理格式之前,首先先确认在你使用的OpenGL ES版本中该格式是否可用。
帧缓存
和OpenGL4.3类似,OpenGL ES 3.0也支持帧缓存和渲染缓存对象。应用可以创建并绑定自己的帧缓存对象,在上面附着渲染缓存或者纹理对象来完成离屏渲染工作。和ES 2.0相比,ES 3.0允许使用多重采样的渲染缓存,也支持在帧缓存对象中使用深度纹理。同样你可以将一个纹理的任意一层附着到帧缓存对象中。
片段操作
在OpenGL ES 3.0中对于逐片段操作也有一些差异,它要求至少存在一个可用的能够支持深度缓存和模版缓存的配置。这样保证了依赖深度信息和模版比较的应用能够运行在任何支持OpenGL ES 3.0的实现上。
一些在OpenGL 4.3规范中的特性也被OpenGL ES移除了。首先,透明度测试功能被移除,ES将这部分工作转移给片段着色器。函数glLogicOp()
不再被支持。只有新的Boolean类型的遮蔽查询机制称为ES规范的一部分。Boolean遮蔽查询操作和它在OpenGL 4.3上的工作模式类似,但是它不再返回通过管道的图元数量,而是仅仅告诉你是否有图元通过了图形管道。
尽管混合模式在ES中仍然得以保留,但是已经被精简了。混合模式不能再为每个渲染靶点做不同的配置,并且双源混合也不再被支持。
状态
OpenGL ES 3.0仍然支持和OpenGL 4.3类似的状态查询机制。你可以使用函数glGetBooleanv()
、glGetIntegerv()
、和glGetFloatv()
查询大多数状态,另外ES 3.0也支持函数glGetInteger64v()
。
3.4.5 OpenGL ES环境
嵌入式或者移动端的硬件资源较PC端更紧张,因此在这种环境中开发OpenGL程序和PC端环境有很大不同。
程序设计考虑事项
OpenGL ES时间跨度很广,可能存在各种各样的硬件配置。其中能力最强的可能是拥有特别设计图形显卡的多核系统,如索尼的PlayStation 3,或者也可能是一部入门级的智能手机,这个设备仅有一颗工作频率为1到2GHz的处理器以及1GB的内存。
在有限的资源背景下,指令的数量应该特别关注。一些特定的操作可能会很慢,比如计算一个角度的正弦值,和调用函数sin()
相比,在一个预先准备好的查询表中查询近似值会快很多。总而言之,将PC程序使用的算法迁移到嵌入式系统中都需要精心优化。一个例子是物理仿真计算,这些计算通常具有很高的性能成本,它们通常能够被简化以求得近似值从而被部署在如移动电话等嵌入式系统中。
ARM架构的CPU占据了大多数的嵌入式环境,特别是几乎所有的移动电话和平板电脑。这种现状能够减轻在多平台间移植程序的负担,但是相较于桌面系统不同的指令集和性能情况仍然是一个挑战。ARM处理器和移动系统被认为比传统的计算机在能耗上有更高的效率,通常一次充电就能使用一天甚至更长的时间。但是这也意味着程序的能耗也是我们需要关注的一个点。通常情况下,性能和能耗需要权衡,但是在移动平台上开发程序是需要避免不必要的浪费。使用遮蔽查询等手段判断图元是否是不可见的,这样能够帮助我们优化程序能耗。
应对有限的资源
在开发嵌入式系统的OpenGL程序是,不仅开发环境是受限的,和PC端相比,图像显卡处理的能力也是不足的。
为存储空间做好预算可能有一定的帮组。你可以将可用的最大的显卡和系统内存分解成多个碎片,并将其用于内存密集的分类。这种方式可以提供一个角度观察不同的应用片段能够使用的内存,以及这些内存何时被耗尽。最明显的内存密集分类就是纹理采样。大尺寸,详细的纹理能够在PC端的程序中提供丰富的,细腻的环境特征。这样做有利于提升用户体验,但是在大多数嵌入式的系统中,纹理是一个吞噬存储资源的怪兽。这些场景在大量片段采样甚至多重采样时能够造成很大的性能损耗,特别是当每个重叠的几何图元片段都被采样并且以错误的顺序绘制的时候。
除了核心硬件纹理采样性能之外,纹理的尺寸也是一个主要的限制。3D和立方体纹理都很容易快速占用大量的显存,这也是3D纹理是OpenGL ES 2.0规范中的可选项的一个原因。通常情况下内存和显存都是有限的,屏幕的尺寸也比较小。这意味着使用一个更小尺寸的纹理能够得到近似的视觉效果。避免多重采样也是有必要的,因为它会占用更多的存储空间。
和纹理类似,顶点存储也会影响存储空间,处理为顶点数据使用内存设置一个上限外,确定场景中重要的部分,并在这些地方增加顶点的密度,其他地方相应减少也是一个有效的办法。
当在屏幕上渲染大量模型时,一个保证平滑渲染的方式时根据模型和观察者的距离,改变模型的顶点数量。这是一个非常细节的几何模型管理方式。例如,当你需要渲染一片森林时,你可以使用三个不同顶点数量的模型。距离观察者最远的树使用最少顶点的模型渲染,中间距离的树使用中等数量顶点模型,而最近的树使用最高顶点数量的模型。这样那些离得比较远的树渲染效率就会大幅提高,并且由于这些树距离观察者远,因此它们通常仅能覆盖很少的像素,因此使用较少顶点的模型看上去的细节差异也很难被注意到。然而我们需要处理的顶点数量却大幅减小,这样更有利于提升程序渲染性能。
定点数计算
OpenGL ES 3.0支持全浮点型数据,但是一些更老的平台原生并不支持浮点型数据。浮点型数据计算在CPU模拟中非常慢,应该避免这些操作。在这些场景下,一种浮点型数据的表示方式坑过被用于和非完全数值集交互。即使你的处理器和GPU硬件理解完全浮点型数据,利用定点数据使得数据存储空间明显减小,数据加载速度更快,以及其他优势。
浮点型数据由小数部分(m)和指数部分(e)组成,可以表示为m✖️2e,这种表示方式使得大的浮点数和小的浮点数具有相同的有效位。而定点数的表示方式则不同,它看上去更像是普通的整型数据。它的二进制位被分为多个段,最高位是符号位,然后是整数位和小数位,如果一个s15.16格式的定点数表示由1个符号位,15个整数位和16个小数位组成。这是OpenGL ES默认的表示定点数格式。
两个定点数的加法和减法运算比较简单,因为对应的整数部分和小数部分直接按标量形式相加或相减即可。但是需要注意的是,两个定点数必须转化到相同的格式才能进行运算。
定点数的乘法和除法运算相对而言更复杂,当它们相乘时,小数部分所占用的位数是两个定点数小数位之和,也就是说两个s23.8格式的定点数相乘,其值的格式为s15.16。因此在执行乘法运算之前最后将两个定点数的格式转换到具有合适的精度。你可能并不会对两个s15.16格式的定点数执行乘法运算,因为如果当其结果大于1时,没有任何空间能够保存整数数据。除法运算也类似,只是得到的值小数部分的位数是第一个和第二个定点数小数位数的差。
在使用定点数计算时,你还需要特别留心溢出问题。对于通常的浮点数而言,当小数部分溢出时,指数部分会发生变化从而确保数据准确性并防止溢出。对于定点数而言并不存在这个机制。为了避免定点数计算时数据溢出,可能你需要改变其数据格式。对于乘法操作而言,当定点数据被从一个格式转换到另外一个格式时会存在小数部分精度丢失问题。有很多数学库可以帮助我们指定定点数运算,如果你打算在整个程序中使用定点数计算,这是一个不错的选择。
需要注意的是在OpenGL ES的着色器语言中,对于所有数据类型我们都必须声明精度,可以声明的精度有highp、mediump和lowp,每种数据类型都有不同的精度和取值区间,可以参考OpenGL ES着色器语言的标准规范。对单个变量的精度声明示例代码。
lowp float color;
varying mediump vec2 Coord;
lowp ivec2 foo(lowp mat3);
highp mat4 m;
你也可以定义每个数据类型的默认精度,示意代码如下。
precision highp float;
precision highp int;
precision lowp sampler2D;
precision lowp samplerCube;
上面就是关于定点数学计算的基本知识,它们足以支持开发简单的嵌入式OpenGL ES应用。关于定点数学计算的更多细节,可以参考Essential Mathematics for Games and Interactive Applications by James Van Verth and Lars Bishop (Elsevier, Inc., 2004)。
3.4.6 EGL一个新的窗口环境
前面的文章中已经简单介绍过分别应用于Linux、Windows和Mac OS系统中的GLX、AGL和WGL接口。要在对应平台上创建和管理OpenGL使用的系统资源需要使用相应的接口,需要注意的是苹果64位操作系统已经不支持AGL。通常显卡供应商也提供EGL实现,和其他窗口接口不同,EGL和平台不相关。它被设计用于在Windows、Linux或者如Android和iOS等嵌入式的系统中。EGL和OpenGL在嵌入式系统中的交互关系如下。
EGL和OpenGL ES一样有着自己的原生数据类型。EGLBoolean
也包含两个对应的值,分别是EGL_TRUE
和EGL_FALSE
。同样的EGL定义了EGLint
类型,该类型和具体平台上的整型大学相同。最新的EGL版本是EGL1.4。
EGL Displays
大多数OpenG的切入点都以EGLDisplay
作为参数,它是对渲染靶点的引用。最简单的方式可以将其理解为一个物理显示器。程序的第一步是通过如下函数设置EGL从而得到默认显示设备。
EGLDisplay eglGetDisplay(NativeDisplayType display_id);
上述函数的参数display_id
的传值取决于具体的系统,如果你在Windows上使用EGL,该参数传入设备上下文。如果你并不知道display_id
但是你只是想在默认的设备上绘制图形,你可以传入常量EGL_DEFAULT_DISPLAY
。如果该函数的返回值是EGL_NO_DISPLAY
,这意味着内部发生了错误。当该函数成功执行后就可以得到一个显示句柄,你可以使用如下函数来初始化EGL。如果你在未初始化EGL之前就调用其相关的接口,那么将会得到一个标识为EGL_NOT_INITIALIZED
的错误。
EGLBoolean eglInitialize(EGLDisplay dpy, EGLint *major, EGLint *minor);
参数major
和minor
是一个地址,当前平台支持的最低和最高EGL版本将会被写入到该地址中。调用该初始化函数后,EGL就能够创建和配置必要的资源。EGL提供接口eglBindAPI()
用于应用从如OpenGL、OpenGL ES和OpenVG中选择需要使用的接口类型,该函数参数需要从枚举值EGL_OPENGL_API
、EGL_OPENGL_ES_API
或者EGL_OPENVG_API
中选取。同样,在每个线程中只能有1个激活的EGL上下文,因此接下来需要调用函数eglMakeCurrent()
指定当前线程激活的上下文。OpenVG是一个在较老的嵌入式设备中用于向量图形运算的API。绑定API类型的函数原型如下。
EGLBoolean eglBindAPI(EGLenum api);
通过调用如下函数可以查询当前使用的API类型,其返回值是EGL_OPENGL_API
、EGL_OPENGL_ES_API
或者EGL_OPENVG_API
中的一个。
EGLenum eglQueryAPI(void);
在退出程序或者完成渲染工作后,必须调用如下函数清空所有分配的资源。当该函数调用后,在当前显示设置中对EGL资源的所有引用将会被标记为无效,直到函数eglInitialize()
被再次调用。
EGLBoolean eglTerminate(EGLDisplay dpy);
同样的某个线程退出或者当前线程中的渲染任务完成后,需要调用函数eglReleaseThread()
释放在当前线程上的所有资源。如果当前线程有绑定的上下文,该函数同样会释放这个上下文资源。在该函数之后,你仍然可以正常调用EGL的资源,只是这样会使EGL重新分配被释放的资源。
EGLBoolean \eglReleaseThread(EGLDisplay dpy);
有关EGL的细节这里就不再继续覆盖,因为这不是本文的主要类型,只需要再了解和OpenGL一样,EGL同样支持扩展。
3.4.7 嵌入式环境
在简单介绍完OpenGL ES和EGL如何在嵌入式系统中工作后,下面会进一步介绍嵌入式系统环境以及它是如何影响一个OpenGL ES应用。在创建ES程序的时候环境总扮演了一个重要的角色。
流行的操作系统
OpenGL ES和大多数3D图形API仅限于在特定的操作系统上不同,它可以应用在大量的操作系统中。现今最常见的两个手机平台是谷歌的Android和苹果公司的iOS系统。
特定供应商的扩展
每个OpenGL ES供应商通常有一套仅仅限于它的硬件和实现的扩展,这些扩展通常增加了可用格式的数量和类型。由于它们仅仅用于有限的硬件集合,这里不深入介绍。
模拟器的ES程序
如果你无法在嵌入式环境中开发ES程序,你仍可以使用一些其他方式在桌面操作系统中开发ES程序,使用这些OpenGL ES的实现来做简单图形程序开发也是一个不错的主意。英伟达和AMD公司都提供一些方式使得我们可以在使用独立显卡的桌面电脑中创建创建ES配置,你可以使用这些配置创建OpenGL ES 2.0或者3.0的应用。另外在谷歌和苹果的平台上开发OpenGL ES的程序都十分容易。
安卓平台
安卓系统如今占据了智能手机市场超过百分之五十的份额。当你在安卓应用市场上发布程序时,这意味着将有上千万的手机或者平板电脑设备能够看到你的应用。因为安卓应用运行的设备来自于很多制造商,因此你需要记住你的应用需要面对不同能力和性能的硬件条件。在安卓2.2及其以后,安卓系统支持OpenGL ES 1.1和ES 2.0,后来安卓也继续支持了ES 3.0。
谷歌提供了很多如何在安卓系统中开发OpenGL ES应用的教程,但是这部分内容超出了本文探讨的范畴,它们可以在其官方网站上找到。开发安卓应用以及在自己的设备上运行都是免费的,但是当你将其部署在官方的安卓商店中需要支付少量的费用。
安卓开发环境
谷歌提供了很多示例程序帮助开发者快速熟悉安卓操作系统的很多特性,总的来说谷歌提供了原生开发库(Native Development Kit, NDK)和软件开发库(Software Development Kit, SDK)两套开发框架供我们使用。
使用NDK开发应用可以使用C和C++语言,该库对于迁移已有的应用到安卓平台特别有用。对于大型游戏引擎,特别是不使用Java库的应用而言,这是一个理想的方式。另外,使用该库也能获得更好的性能。同样的你也可以更精细的控制窗口系统以及EGL环境从而使它们更匹配当前应用。但是,使用NDK也能够使得代码更复杂,从而影响应用的移植性。
SDK提供了更简单的API,使得程序的开发和调试都更容易,对于新手而言这种方式是一个理想的选择。另外它包含一些工具和组建使我们可以在Eclipse集成开发环境中开发应用。SDK会处理大多数管理EGL和设置GL环境的细节,我们并不需要直接调用OpenGL ES中的API,直接调用Java桥接层的接口即可。原书中使用这种方式介绍了如何在Android环境中开发应用,并给出了相关的例子,但是这暂不在本文的探讨范畴内。
iOS开发环境
苹果公司的iOS系统在早期运行在其主流的设备iPhone、iPod Touch和iPad之上,尽管如今iPad有了独立的系统Pad OS,但是这几种设备的最新型号都支持OpenGL ES 3.0。另外iOS系统还运行在iWatch,iTV和iMicrowaves之上。
本系列博客的第一篇就对OpenGL ES的iOS开发环境做了介绍,并且给出了示例程序,因此这里不再重复。本书除了少量的示例程序运行在iOS系统上,剩余的大部分示例程序都运行在mac OS之上。
iOS原生编程环境使用的语言是Objective-C,而大多数的非苹果编程者使用的都是C或者C++语言。但是如果你想要使用苹果提供的系统库,那么必须熟悉Objective-C。Objective-C可以简单的理解为面向对象的C语言,但是它和C++又不同。将一个文件的后缀由m改为mm可以将该文件设置为C++和OC混编模式,即可以在当前文件中使用两种语言开发。另外需要注意的是,苹果自己目前已经全面采用自家的Swift语言,并且推荐开发者也使用该语言。Swift语言比OC更安全和高效,但是具体细节超出了本文的讨论范围,可以参考官方文档。
在使用OpenGL ES 2.0的时,需要注意部分功能并不被核心版本支持,但是苹果提供扩展使得我们可以正常使用这些功能,并切这些扩展也被OpenGL工作组定义,并被包含在其后续版本ES 3.0中。以扩展形式使用这些功能时需要注意加上OES后缀,它们包括如下功能。
public func glGenVertexArraysOES(_ n: GLsizei, _ arrays: UnsafeMutablePointer<GLuint>!)
public func glBindVertexArrayOES(_ array: GLuint)
public func glDeleteVertexArraysOES(_ n: GLsizei, _ arrays: UnsafePointer<GLuint>!)
public func glUnmapBufferOES(_ target: GLenum) -> GLboolean
public func glMapBufferOES(_ target: GLenum, _ access: GLenum) -> UnsafeMutableRawPointer!
后两个是操作顶点数组对象的扩展,而第四个函数是映射缓存地址的扩展函数。需要注意的是函数glMapBufferOES()
的参数access
参数只能传入GL_WRITE_ONLY
。
OpenGL ES 2.0使用的着色器语言基于OpenGL 2.0修改,尽管ES 3.0要求在着色源码的前面加上指定当前着色器版本的修饰符#version 300 ES
,但在ES 2.0使用的着色器语言中并不能直接识别这条预编译指令。在对OpenGL所使用的着色器语言有了解后,再熟悉其和ES使用的着色器语言的差异,就能很快开始编写ES的着色器。
在ES 2.0中不能使用关键字in
和out
,ES 2.0所使用的GLSL是基于只存在顶点着色器和片段着色器的双着色器器系统设计的,在顶点着色器中所有的顶点属性都使用关键字attribute
声明如下。
attribute vec4 vVertexPos;
顶点着色器输出的值对经过插值计算被传入到片段着色器中,在这两个着色器中这些变量都使用关键字varying
声明如下。另外对于插值计算方式控制的关键字smooth
和centroid
也被支持。
varying vec2 vTexCoordVary;
在片段着色器中,正如在顶点着色器中存在内建变量gl_Position用于指定顶点的输出数量,在片段着色器中存在内建变量gl_FragColor用于指定片段的最终颜色。另外,你可以使用如下方式进行纹理采样并计算片段颜色。
gl_FragColor = texture2D(colorMap, vTexCoordVary.st);
在桌面版本的GLSL中现在也支持数据精度修饰符,而在ES 2.0后必须为片段着色器中的浮点数据设置精度。在前面的小节中已经介绍过如何声明数据精度,这里不再累述。需要注意通常情况下medium声明的精度最够应对浮点数据计算,特别是对于颜色数据的计算,使用high声明默认浮点精度可能会耗费大量计算资源降低程序性能。
精度声明的关键字仅是对OpenGL ES实现的一个暗示,暗示被声明变量的用途,大多数iOS设备使用的PowerVR显卡能够利用这些关键字,并且小心的适配从而得到很好的性能收益。
4 其他资料
本系列文章到这里已经全面的覆盖了OpenGL 4.1核心版本的所有知识。实时3D图形学和OpenGL是一个很流行的话题,还有很多资料可以加强我们在这个方向的知识积累和技术深度。
4.1 OpenGL书籍
- McReynolds, T., and Blythe, D. (2005). Advanced Graphics Programming Using OpenGL. Morgan Kaufmann.
- Angel, E., and Shreiner, D. (2011). Interactive Computer Graphics: A Top-Down Approach with Shader-Based OpenGL (6th Edition). Addison-Wesley.
- Astle, D. (ed.) (2006). More OpenGL Game Programming. Thomson Course Technology.
- Munshi, A., Ginsburg, D., and Shreiner, D. (2008). OpenGL ES 2.0 Programming Guide. Addison-Wesley.
- Shreiner, D., Sellers, G., Kessenich, J., and Licea-Kane, B. (2013). OpenGL Programming Guide, 8th Edition: The Official Guide to Learning OpenGL, Version 4.3. Addison-Wesley.
- Cozzi, P., and Riccio, C. (eds.) (2012). OpenGL Insights. CRC Press.
- Wolff, D. (ed.) (2011). OpenGL 4.0 Shading Language Cookbook. Packt Publishing.
4.2 3D图形学书籍
- Watt, A. (1999). 3D Computer Graphics, 3rd Edition. Addison-Wesley.
- Dunn, F., and Parberry, I. (2011). 3D Math Primer for Graphics and Game
Development, 2nd Edition. A.K. Peters / CRC Press. - Van Verth, J., and Bishop, L. (2008). Essential Mathematics for Games and
Interactive Applications, 2nd Edition. Morgan Kaufmann. - Foley, J. D., et al. (1993). Introduction to Computer Graphics.
Addison-Wesley. - Lengyel, E. (2011). Mathematics for 3D Game Programming & Computer
Graphics, 3rd Edition. Course Technology PTR. - Akenine-Moller, T., Haines, E., and Hoffman, N. (2008). Real-Time
Rendering, 3rd Edition. A.K. Peters. - Engel, W. (ed.) (2006). Shader X 4: Advanced Rendering Techniques. Charles
River Media.
4.3 网站和论坛
- The OpenGL® SuperBible, Sixth Edition, Web site
- The official OpenGL Web site
- The OpenGL SDK (lots of tutorials and tools)
上面三个网站是学习OpenGL的门户网站,也是馊油OpenGL和OpenGL SuperBible资料的官方网站。下面的这些网站也适用于本系列博客中的知识,在这些网站中包含特定供应商的OpenGL教程,示例程序和新特性。
- The Khronos Group OpenGL ES home page
- The OpenGL Extension Registry
- AMD’s developer home page
- NVIDIA’s developer home page
- The Mesa 3D OpenGL “work-alike”
- GLView OpenGL Extension Viewer
下面是一个在线着色器调试的网站,我们可以从中找到一些高级特效的示例代码,也可以在线调试我们自己的着色器代码。
5 总结
本章介绍了如何在Windows中使用Win32 API、在Mac OS X中使用Cocoa、以及在Linux中使用X系统开发OpenGL程序。OpenGL是开发Mac应用的核心框架,对OpenGL有基础的了解,并且知道应用和OpenGL之间如何交互是一个Mac OS X开发者的基本技能。另外在Linux平台上,通常OpenGL是唯一可用的3D图形API。
在所有的平台中,我们已经介绍了如何查找最适合当前渲染需求的配置,也讲到了如何创建特定OpenGL版本的上下文。另外本文也介绍了如何在程序结束时情况窗口系统的状态并释放OpenGL的资源。
最后我们接触了OpenGL的嵌入式版本OpenGL ES,它几乎统治了所有的移动平台图形处理API。
6 结语
到这里历时一年多的OpenGL系列也正式结束,本系列博客仅是全面讲解了基础知识,要成长为一个优秀的图形处理方向工程师甚至专家的路还很长。最后希望能够和各位有兴趣的小伙伴,大牛🐮交流学习。谨用一句话送给自己以及和我一样希望在音视频处理、图形识别和处理方向逆风而行的小伙伴。我们的征途是星辰大海。
网友评论