原文链接:http://blog.db-in.com/all-about-opengl-es-2-x-part-1/
大家好!
欢迎再次来到新一期的系列教程。这次让我们进入充满魔力的3D世界。下面就来谈谈OpenGL。最近五个月的时间,我全身心的投入到3D世界中,即将完成我的新3D引擎(我觉得这是我做过的最棒的事情了)。现在是时候把我所了解的内容分享给大家了,其中包含所有的引用来源,所有的书籍、教程等等,当然,我会从大家的回复中学到更多。
本系列分为3大部分:
- Part 1 - 有关3D世界和OpenGL的基本概念(入门)
- Part 2 - 深入理解OpenGL ES 2.0(进阶)
- Part 3 - OpenGL ES 2.0和2D图形编程的高级技巧(高级)
所有,如果你对于如何书写代码更感兴趣的话,可以直接跳到第二部分,因为这部分我将只涉及基本概念,不会有其他的内容。
好的,下面就让我们进入正题。
引言
是否有人没有听说过OpenGL?OpenGL全称为Open Grahics Library(开源图形库),在今天的计算机语言中被广泛使用。OpenGL作用在CPU(开发者运行应用程序的场所)与GPU(每个显卡都存在的图形处理器)之间。所以OpenGL需要被显卡(比如NVidia)和操作系统供应商(比如苹果的MacOS和iOS)所支持,而最终,OpenGL给予开发者一个统一的API标准。这样的API是跨平台的,即“Language Free”,不受编程语言的限制。这是非常令人兴奋的事情,因为无论你使用C、C++、Objective-C、Perl、C#或者JavaScript,API总是等价的,并且呈现相同的行为,拥有相同的函数,相同的命令,而本系列教程也会说明这一点!开发者可以尽情使用OpenGL的API。
在开始谈论OpenGL的API之前,我们需要了解3D世界。计算机语言的3D历史包含了OpenGL的历史,所以,就让我们简单看看这段历史。
一个简短的故事
大约在20年前,硅谷图像公司(SGI)研发了一种技术,能够呈现现实世界的视觉体验。在当时到处都是2D图像的世界中,它竟然可以通过模拟人眼的透视和深度来显示3D图像。这种技术被称为IrisGL(可能因为它在试图模仿人眼的虹膜)。
严格来讲,这种技术是第一个优秀图像库。但它很快就被淘汰了,因为需要开发者同时控制计算机的多种器件,如显卡、窗口系统、Basic语言,甚至是前端。这都让仅仅一个公司来管理显然是不可能的。所以SGI开始代为处理一些事情,比如“创建显卡”、“管理窗口”、“生成前端”,而其他公司专注于图形库更重要的部分。在1992年启动了第一版OpenGL的开发。
在1995年微软发布Direct3D,成为了OpenGL的主要竞争者。而仅仅在1997年,OpenGL就发布了1.1版本。但是我真正开始关注OpenGL是在2004年,此时OpenGL发布了2.0并做了大幅度的改进。Shaders,可编程管线,我真是爱死它了!
最终在2007年我们见到了OpenGL ES 2.0,将Shader和可编程管线技术带到了嵌入式系统中。
今天我们能在很多游戏、3D、2D应用和很多图形软件中看到OpenGL(或者OpenGL ES)的商标。OpenGL ES现在被PlayStation,Android,任天堂的3DS,诺基亚,三星,塞班,当然还有苹果的MacOS和iOS所使用。
OpenGL最大的挑战
下面我们谈谈微软的Windows系统(真是不爽!!)
你还记得我说过OpenGL在1992第一次启动研发吗?在那个时候,微软拥有了他们闪亮的Windows3.1。好吧,就像微软所坚信的那样“好东西不是被创造的,而且被复制来的”,微软开始在他们所谓的DirectX技术中尝试抄袭OpenGL,并在1995年Windows95发布时首次介绍DirectX。
一年后的1996年,微软介绍了Direct3D,一个OpenGL的完整复制版。而微软支配了主要市场长达数年,DirectX(或者叫Direct3D)像瘟疫一样在很多电脑上传播渗透,当微软开始将他们的系统应用于手机和视频游戏上,DirectX也跟着一起被使用。
现在的DirectX在结构上与OpenGL非常相似:比如Shader Language,拥有可编程管线,也有固定管线,甚至在API的命名上都很相似。不同之处在于OpenGL是开源的,而DirectX是闭源的。OpenGL使用于iOS,MacOS以及Linux系统上,而DirectX是应用于微软自己的系统上。
好的,现在让我们来开始真正的3D旅程!
3D world
眼睛
从我记事开始,我就对3D世界和3D游戏充满了兴趣。 我们人类都很清楚能在3D视觉中模拟真实世界的仅仅来自于一个地方:我们的眼睛。
眼睛是3D世界一切事物的基础。我们所做的一切努力都是在模拟人眼神奇而美丽的能力。我不是医生,也不想在教程中过多讨论眼睛的事情,但是如果你了解一切基本概念:如视野,双眼和单眼视觉,眼睛的透镜,凹凸镜,和很多这类的东西,这对于了解后面的概念很有帮助。
我们在3D世界中所做的就是通过人眼重建视觉:透视,盲点,扭曲,景深,焦点,视野,所有内容都是在模拟相应的视觉。
三维
也许这看上去很蠢,但是仍然有必要谈论一下,3D世界之所以是3D的,因为有3个维度。“WTF,这很明显啊!”,冷静下来,我之所以说这些有着很重要的原因,比起2D世界多出的那一维会给我们带来非常多的麻烦。
在2D世界中当我们需要旋转一个正方形非常简单,45º总可以让正方形做出一个特定的旋转,但是在3D世界,旋转一个简单的正方形需要X,Y,Z三种旋转。根据我们首先旋转的维度,最终的结果会完全不同。当我们做连续旋转时会变得更糟糕,举个例子,旋转x=25、y=20是一个结果,但是旋转x=10、y=20然后再旋转x=10是完全不同的新结果。
好吧,这里要说明的重点是增加的那一维度使我们要做的工作非常恼人的翻了几番。
不是3D…通常是4D
WTF!又一个维度?
是的,这是我要说的最后一点。我们通常不仅仅只在3D世界中操作,我们拥有第四个维度:时间。3D世界的物体是相互作用的,需要移动、加速、碰撞、改变惯性。就像我之前说过的,在3D世界中做连续改变会产生成倍的结果。
到现在为止,我们可以为3D虚拟世界做一个定义:“对人眼的模拟并且所有的物体都在移动”。
3D世界中的OpenGL
现在本教程开始变得有趣了。让我们开始讨论伟大的引擎OpenGL。首先我们需要感谢伟大的数学家,如Leonhard Euler,William Rowan,Hamilton,Pythagoras和很多人。感谢他们,在3D空间我们有了如此多的公式与技术。OpenGL运用了所有这些知识并在我们面前构架出了一个3D世界。每秒钟会执行上千甚至百万次操作才能模拟人眼中看到的美丽世界。
OpenGL是一个庞大的状态机(这意味着,整个OpenGL都是在状态模式下运行的)。为了解释清OpenGL是什么,让我们想象一台港口里有很多的集装箱,每一个里面都装满了木箱。而OpenGL就像整个港口:
- 所有的集装箱就是OpenGL的操作对象(材质,着色器,网格和很多类似的属性)
- 集装箱里面的木箱就是我们用OpenGL在应用中创建的实例。
- 起重机对应的就是我们可以访问的OpenGL的API。
所以当我们执行一个OpenGL函数时,就好比给了起重机一条命令。起重机将港口中的集装箱提起并保持一段时间,在集装箱内部进行处理,最终将集装箱放回原先的位置。你不能直接进入港口,也无法看到或者改变集装箱内部的事物, 当然也无法重组它,总之你无法直接对集装箱做任何事。你所能做的就是给予起重机指令。记住这点!到现在为止要开始的是OpenGL最重要的部分。起重机是港口唯一可以管理集装箱的器材。
好吧,从这种方式上看OpenGL会觉得API是受限的,但实际并非如此。OpenGL的起重机非常强大,它能够每秒重复提起、放下集装箱几千甚至几倍玩次。而OpenGL的另一个优势是,使用状态机模式意味着我们不需要保持任何实例,也不需要直接创建任何对象,我们仅仅需要持有id或者插入的语句,以及集装箱的唯一句柄。
OpenGL是如何工作的
深入OpenGL的核心就会发现所有计算都直接的使用硬件加速,由GPU进行浮点运算。CPU是电脑或电子装置的处理器。GPU是电脑或电子装置的图像显示卡。之所以显卡可以有效缓解处理器的压力,是因为在将内容显示于屏幕前显卡会对图像进行大量的计算。所以深入来讲,就是由GPU来代替CPU进行大量的计算。 GPU比CPU在处理浮点运算时要快得多。这就是一款游戏在一台拥有强大显卡的电脑上可以运行更快的真正原因,也是专业3D软件提供“软件渲染”(CPU处理)和“图形卡渲染”(GPU处理)选项的原因。还有另外一些软件也能为你提供“OpenGL”的选项,现在你该知道它的含义吧,就是用GPU进行处理!所以,有人会猜测OpenGL直接工作在GPU吗?其实并不是这样,GPU所做的只是一些图像处理和其余一些事情。OpenGL给予我们很多方便的接口,可以用一种优化的格式来储存图像、数据或者信息。这些优化过的数据将会直接由GPU进行处理。所以,又会有人猜测OpenGL是依赖于硬件吗? 很不幸,是这样的!如果硬件(显卡)不支持OpenGL,我们就无法使用。新的OpenGL版本经常需要新的GPU特性支持。以上这些是我们应该知道的,但是没有必要为此担心。因为OpenGL需要供应商来集成,我们开发者只需要等到设备准备就绪就可以在新的OpenGL版本下工作了。实际上,今天的所有显卡芯片都集成了OpenGL。所以,你们可以在各种设备和各种编程语言下使用OpenGL,甚至是微软的Windows系统下。
OpenGL的逻辑实现
OpenGL图形库是非常简洁和专注的。你在专业3D软件中看到的是使用OpenGL进行的非常复杂的操作。因为在底层,OpenGL的内部逻辑包含如下的内容:
- 基本体
- 缓冲区
- 光栅化
只有这三样吗?请相信,OpenGL是围绕这三种概念来运转的。下面就让我们分别认识下每一种概念和它们在高级3D图形库中是如何参与运作的。(你也可以使用OpenGL进行2D图像处理,只是工作在Z轴为0的3D图像处理的特例而已,有关这一点我们以后再说。)
基本体
OpenGL的基本体只有三种:
- 3D空间内的点(x,y,z)
- 3D空间内的直线(有两个点组成)
- 3D空间内的三角形(有三个点组成)
3D空间内的点能被用来做空间粒子。 3D空间内的直线通常都是一条单独的线,并且被用来做为3D空间向量。 3D空间内的三角形能够做为网格成千上万个的表面。 有一些OpenGL版本也支持四边形,仅仅是三角形的旁支。 但是OpenGL ES需要实现最高的性能,因此四边形是不支持的。
缓冲区
现在让我们谈谈缓冲区。简单来说,缓冲区就是一个优化过的临时存储器。存什么?存很多的东西。 OpenGL主要包含三种类型:
- 帧缓冲区
- 渲染缓冲区
- 对象缓存
帧缓冲区是这三类中最抽象的。但你使用OpenGL进行渲染,你会把最终的图像直接传给屏幕或者帧缓冲区。所以,帧缓冲区就是临时的图像数据,是这样吗?并不准确。你可以将OpenGL渲染的输出结果图像化,这就意味着是一组图像,而不是一个。但究竟是哪类图像呢?这种图像包含了3D物体,物体在空间中深度、相交,和物体可见的部分。所以帧缓冲区就好比是图像的收集器。所有这些像素的信息都以二进制数组的形式进行存储。
渲染缓冲区是一个单独图像的临时存储器。现在你能清楚的明白实际上帧缓冲区是渲染缓冲区的集合。渲染缓冲区存在几种类型:颜色、深度和模板。
颜色渲染缓冲区存储的是由OpenGL最终生成的彩色图像。颜色渲染缓冲区实际上是一个彩色图像(包含RGB)。深度渲染缓冲区存储的是物体的Z轴深度信息。如果你熟悉3D软件,你就会知道Z轴深度图像。
它是3D空间里对象在Z轴位置的一个灰度图像,近处可见对象为纯白色,而大部分远端物体呈现纯黑色(纯黑色是不可见的)。
模板渲染缓冲注意着对象的可见部分。它就像一个覆盖在可见部分上的遮罩,其本身是一个黑白色图像。 缓冲区对象是一个存储器,在OpenGL中被称为“服务器端”(或者服务器的地址空间),它也是一个临时存储器,但是其临时性并非像其他对象一样。一个缓冲区对象可以在应用执行中持续存在。它能够持有你的3D物体的格式优化过的相关信息。这些信息存在两种形式:结构和索引。
结构就是描述你的3D物体的数组,比如顶点数组,纹理坐标数组或者任何你想要的数组。而索引更加具体,索引数组通常被用来标识你的网格面是如何被一个结构数组所构建。
是不是感觉很困惑?
好吧,就让我们来看个例子。
想象一个3D立方体。这个立方体由8个顶点构建出六个面,对吧?
六个面都是四边形,但是你是否还记得OpenGL的基本体是三角形?所以我们需要利用OpenGL将三角形转化成四边形。当我们这样处理的时候,实际上六个面变成了12个面。上面的这张图是用Modo制作的,注意看右下角,是Modo给出的关于网格的信息。就像我们可以直观看到的,一共8个顶点和12个面(GL:12)。
现在让我们思考一下。
OpenGL的三角形是由三个顶点组成。所以在构建立方体的正面时,我们需要创建{vertex 1, vertex2, vertex 3}, {vertex 1, vertex 3, vertex 4},对吧?
换句话说,我们需要在每个立方体的面上重复两条向量。更糟糕的是,如果我们的网格是五角形,则我们会有四条向量重复,同样的,六角形的话,就会重复六条向量等等。
这样结果会急剧扩张。
所以OpenGL给了我们一种更简便的处理方式,被称为索引数组。在上面的立方体例子中,我们最后能得到8个顶点的数组:{vertex 1, vertex 2, vertex 3, vertex 4, …}。为了避免重写这些信息,我们创建一个索引数组:{0,1,2,0,2,3,2,6,3,2,5,6…}。数组中每3个元素的组合(0,1,2 - 0,2,3 - 6,3,2)代表了一个三角形面。利用这种特性,我们可以一次写入顶点信息,并多次复用索引数组中的信息。
现在,我们回到缓冲区对象的内容,结构数组中的第一种类型是顶点,类似{vertex 1, vertex 2, vertex 3, vertex 4, …},第二种是索引数组,类似{0,1,2,0,2,3,2,6,3,2,5,6…}。
缓冲区对象最大的优势在于它们自身已被优化,并且会直接送入GPU进行处理,而我们不需要在应用中创建缓冲区对象后继续持有该数组。
光栅化
光栅化是OpenGL利用所有关于3D物体(坐标,顶点等等)的信息来创建2D图像,并经过多次改变并最终显示到屏幕上(通常情况下)。
但是最后一步,搭建像素信息和显示设备屏幕的桥梁,是供应商的职责。Khronos组织提供另一种API,被称为EGL,但这需要供应商来集成。而我们开发者不会直接操作Khronos EGL,但会操作供应商的修改版本。
所以,当你使用OpenGL渲染时,你可以选择直接使用供应商的EGL实现来渲染到屏幕上,或者渲染到一个帧缓冲区上。当渲染到帧缓冲区,你也同样在使用OpenGL API,但是渲染的内容并不会立刻显示到设备屏幕上。当渲染到屏幕上,你将不再使用OpenGL API,而转而使用EGL API。所以在渲染的时候,你可以选择这两种输出的任意一种。
但是,现在不需要担心这些,就像我说过的,每个渲染器都集成了各自的EGL API。例如,苹果公司并不会让你的渲染器直接渲染到设备屏幕上,你总需要渲染到一个帧缓冲区,然后再使用苹果集成的EGL实现来向屏幕呈现内容。
OpenGL的管线
我之前提到过“可编程管线”与“固定管线”。但是简单来说,可编程管线到底是什么东西?
可编程管线是图形库委托给我们开发者的,可以用来处理摄像,灯光,材料和特效相关的内容。我们能做的都要和这著名的着色器联系在一起。所以每次当你听到“可编程管线”的时候,你就要联想到着色器!
但是,什么优势着色器呢?
着色器就像一段代码片段,只是一段程序,直接在GPU运行来处理一些复杂运算。比如:使用聚光透镜T在位置P使用相机C看到的,在拥有光功率SL及Z轴入射角LA和衰减F的光照L下,一个纹理T的表面点的最终颜色,被隆起的纹理TB所修改,并使用了高光级别SL的单个高光颜色SC。
无论这意味着什么,如果让CPU来进行处理会相当复杂。所以可编程管线能够代替我们管理这类事情。
而固定管线又是什么呢?
它与可编程管线完全相反!使用固定管线,即图形库要关心所有要进行处理,并且给予我们处理摄像,灯光,材料和特效相关的API。
为了创建着色器,我们使用一种类似C的编程语言,名叫OpenGL Shader Language(GLSL)。OpenGL ES使用了更加严格的版本,被称为OpenGL ES Shader Language(也被称为GLSL ES或者ESSL)。两者的不同之处在于,你在GLSL中可以使用更多的固定函数,并且可以创建更多的变量,但是两者的语法是相同的。
但是这些着色器是如何工作的呢?
你可以在单独的滤镜或者直接在代码中创建它们,但无论怎样,最重要的事情是最终创建的包含SL的字符串会发送给OpenGL的核心,并由核心来编译再返回给你(你甚至能用预编译的二进制着色器,但这是本系列另一部分的内容)。
着色器是成对运行的:包括顶点寄存器和片段着色器。这一点需要大家多多关注,所以让我们进一步地来了解顶点和片段着色器,并理解每个着色器的工作原理。现在让我们回到刚才的立方体实例。
顶点着色器
顶点着色器,也被称为VS或者VSH,是一段执行在网格顶点的程序。回顾之前看的立方体,就像我所说的,一共需要8个顶点(现在图片中有5个顶点是可见的,很快你就会了解为什么会变少)。所以现在立方体的VSH将由GPU处理这8个顶点。
顶点着色器所做的事情就是确定顶点的最终位置。你是否还记得可编程管线可以让开发者来负责摄像?现在正是时候!
位置和照相机的镜头会影像顶点的最终位置。顶点着色器也负责处理些准备工作,并向片段着色器输出一些变量。在OpenGL中我们可以在顶底着色器中定义变量,但不能直接在片段着色器进行变量的定义,因为我们的片段着色器必须穿过顶点着色器。
但是为什么我们不能直接访问片段着色器? 让我们来看看FSH,相信你马上就会明白。
片段着色器
再次观察立方体图像。
你是否意识到只有5个顶点可见?这是因为这种特定的位置和特定的旋转角度下,我们刚刚能看到由7个顶点组成的3个面。
这就是片段着色器要做的事情!FSH会对最终图像的每个可见片段进行处理。这里你可以将每个片段理解为一个像素。但是一般情况下这种比喻并不准确,因为在OpenGL渲染和呈现最终显示在屏幕上的图像时会有拉伸。所以一个片段会小于或大于一个像素,这取决于设备和渲染器的设置。在上面的立方体中,片段着色器将处理由7个顶点构成的3个面中的每个像素。
在片段着色器内部,我们将处理网格表面相关的任何属性,比如材质,碰撞,阴影和光照,反射,折射,纹理和其他任何我们想要的效果。最终片段着色器输出的是一个像素颜色的格式化RGBA。
现在,你需要了解的最后一件事就是VSH和FSH是怎么协同工作的。一个顶点着色器对应只一个片段着色器,这是强制性规定。为了确保我们不会犯错,OpenGL拥有程序的属性。在OpenGL中的一段程序就是一对VSH和FSH编译后的结果。
总结
非常好!
以上这些就是关于OpenGL的基本概念。希望大家可以牢记。
- OpenGL的逻辑由3个简单的概念组成:基本体,缓冲区和光栅化。
- 基本体包括点、线和三角形。
- 缓冲区可以是帧缓冲区,渲染缓冲区或者对象缓存。
- 光栅化是在像素数据上进行OpenGL数学转换的处理过程。
- OpenGL是在固定或是可编程管线中运行。
- 固定管线很老旧,而且庞大而缓慢。有很多固定的函数来处理摄像,灯光,材料和特效。
- 可编程管线的使用更加容易,而且比固定管线更加快速和简洁,因为OpenGL可以让开发者使用编程的方式来处理摄像,灯光,材料和特效。
- 可编程管线是着色器的同义词:顶点着色器存在于每个网格的顶点,片段着色器存在于每个网格可见的片段。每一对顶点着色器和片段着色器都会在程序内部进行编译。
只看这3点,看上去OpenGL很容易理解和学习。是的!它确实很容易理解…但是学起来…额… 这些内容会衍生出更多的知识,了解所有的东西可能要花费数月之久。
我在本系列后面的两个部分,会把我这六个月以来深入学习OpenGL所了解到的内容展示给大家。下一章节,我会给大家展示一些用OpenGL编写的应用内部的一些基本函数和结构,与你所使用的编程语言和设备无关。
但是再次之前,我想向大家再介绍一点OpenGL相关的概念。
OpenGL异常API
OpenGL是一个庞大的状态机就像港口的起重机一样工作,而且你无法了解内部真正发生了什么。所以如果内部发生了错误,你的应用不会发生任何事,因为OpenGL是完全的外部核心。
但是,如何才能知道你的着色器出了问题?如何才能了解渲染缓冲区有没有配置正确?
为了处理这类错误,OpenGL提供了异常API。此API非常简单,拥有固定的组合函数。一个是简单的检测,返回YES或者NO,仅仅知道命令是否正确执行。另外一个用来检测异常信息。所以非常简单。首先你应进行检测,过程发生的非常迅速,如果有错误发生你会很快收到信息。
通常我们会在一些关键部位进行检测,比如着色器的编译以及缓冲区的配置,并会留意一些常见的错误。
后续
好了,现在大家应该准备好进行实际操作了。 在下一章节里,我会展示一些代码范例,大家做好准备开始编写自己的程序。
感谢大家的阅读,我们下次再会!
本文由博主Bayonetta独立完成译制,请尊重他人劳动成果,转载请注明译者信息和本文网址。