一、搭建开发环境

1、打开XCODE,新建一个工程

选择:IOS-->ApplicationàSingle View Application模板。

取名为“HelloOpenGL”,勾选“UseStoryboards”,然后创建。




 


  

2、添加必要的框架

在“Build Phases”栏,添加进三个框架:




3、修改viewController.h

添加“#import <GLKit/glkit.h>”,并将它修改为继承“GLKViewController”。



  

4、修改“view”的类

双击“MainStoryboard.storyboard”展开,选择“view”。



 


  

然后,在其“Identity Inspector”中,将它的类改为“GLKView”。



 


 至此,OpenGL环境已基本搭建出来了。运行,应该不会报错,尽管它目前仍是黑屏。


二、开始堆代码

基本上,所有的代码都在“ViewController.m”中写。

1、添加全局属性声明



 


当然,还得在实现部分补足“@synthesize context;”和“@synthesize effect;”。



2、添加一组顶点数据

这是一个正方形顶点的数组,实际上它是由二个三角形接合而成的。



  每行顶点数据的排列含义是:

顶点X、顶点Y,顶点Z、法线X、法线Y、法线Z、纹理S、纹理T。

在后面解析此数组时,将参考此规则。

顶点位置用于确定在什么地方显示,法线用于光照模型计算,纹理则用在贴图中。

一般约定为“顶点以逆时针次序出现在屏幕上的面”为“正面”。

世界坐标是OpenGL中用来描述场景的坐标,Z+轴垂直屏幕向外,X+从左到右,Y+轴从下到上,是右手笛卡尔坐标系统。我们用这个坐标系来描述物体及光源的位置。


三、初始化OpenGL环境

1、基本的初始化代码

在“viewDidLoad”方法内,补充初始化代码:





第一部分:使用“ES2”创建一个“EAGLContext”实例。

第二部分:将“view”的context设置为这个“EAGLContext”实例的引用。并且设置颜色格式和深度格式。

第三部分:将此“EAGLContext”实例设置为OpenGL的“当前激活”的“Context”。这样,以后所有“GL”的指令均作用在这个“Context”上。随后,发送第一个“GL”指令:激活“深度检测”。

第四部分:创建一个GLK内置的“着色效果”,并给它提供一个光源,光的颜色为绿色。

 

2、运行

现在应该是粉红色屏幕了(目前场景仍是空的),说明初始化过程没问题。


四、将顶点数据写入通用的顶点属性存储区

1、写入过程

首先将数据保存进GUP的一个缓冲区中,然后再按一定规则,将数据取出,复制到各个通用顶点属性中。

注:如果顶点数据只有一种类型(如单纯的位置坐标),换言之,在读数据时,不需要确定第一个数据的内存位置(总是从0开始),则不必事先保存进缓冲区。


2、顶点数组保存进缓冲区



 

这几行代码表示的含义是:声明一个缓冲区的标识(GLuint类型)à让OpenGL自动分配一个缓冲区并且返回这个标识的值à绑定这个缓冲区到当前“Context”à最后,将我们前面预先定义的顶点数据“squareVertexData”复制进这个缓冲区中。

注:参数“GL_STATIC_DRAW”,它表示此缓冲区内容只能被修改一次,但可以无限次读取。


3、将缓冲区的数据复制进通用顶点属性中



  

首先,激活顶点属性(默认它的关闭的)。“GLKVertexAttribPosition”是顶点属性集中“位置Position”属性的索引。

顶点属性集中包含五种属性:位置、法线、颜色、纹理0,纹理1。

它们的索引值是0到4。

激活后,接下来使用“glVertexAttribPointer”方法填充数据。

参数含义分别为:

顶点属性索引(这里是位置)、3个分量的矢量、类型是浮点(GL_FLOAT)、填充时不需要单位化(GL_FALSE)、在数据数组中每行的跨度是32个字节(4*8=32。从预定义的数组中可看出,每行有8个GL_FLOAT浮点值,而GL_FLOAT占4个字节,因此每一行的跨度是4*8)。

最后一个参数是一个偏移量的指针,用来确定“第一个数据”将从内存数据块的什么地方开始。


4、继续复制其他数据

在前面预定义的顶点数据数组中,还包含了法线和纹理坐标,所以参照上面的方法,将剩余的数据分别复制进通用顶点属性中。



 

原则上,必须先“激活”某个索引,才能将数据复制进这个索引表示的内存中。

因为纹理坐标只有两个(S和T),所以上面参数是“2”。


五、执行渲染循环

万事具备,现在可以让OpenGL显示一些东西了。

在GLKit框架中,尽管OpenGL的行为,是由“GLKViewController”和“GLKView”联合控制的,但实际上“GLKView”类中完全不需要写任何自己的代码,因为,“GLKView”类中每帧触发的两个方法“update”和“glkView”,都转交给“GLKViewController”代理执行了。


1、添加代理方法

在“ViewController.m”中添加两个方法:



  

这两个方法每帧都执行一次(循环执行),一般执行频率与屏幕刷新率相同(但也可以更改)。

第一次循环时,先调用“glkView”再调用“update”。

一般,将场景数据变化放在“ update ”中,而渲染代码则放在“ glkView ”中。



2、渲染场景




  

前两行为渲染前的“清除”操作,清除颜色缓冲区和深度缓冲区中的内容,并且填充淡蓝色背景(默认背景是黑色)。

“prepareToDraw”方法,是让“效果Effect”针对当前“Context”的状态进行一些配置,它始终把“GL_TEXTURE_PROGRAM”状态定位到“Effect”对象的着色器上。此外,如果Effect使用了纹理,它也会修改“GL_TEXTURE_BINDING_2D”。

 

接下来,用“glDrawArrays”指令,让OpenGL“画出”两个三角形(拼合为一个正方形)。OpenGL会自动从通用顶点属性中取出这些数据、组装、再用“Effect”内置的着色器渲染。

 

3、结果



 

渲染内容终于呈现了,蓝色背景、还有一个绿色矩形(其实是两个三角形)。绿色并非是此物体的本色,而受是绿色灯光影响。

PS:在前面的顶点数据定义中,期望得到一个正方形,但为什么显示结果却是一个矩形?


六、正确显示正方形外观

默认,“Effect”的投影矩阵是一个单位矩阵,它不做任何变换,将场景(-1,-1,-1)到(1,1,1)的立文体范围的物体,投射到屏幕的X:-1,1,Y:-1,1。因此,当屏幕本身是非正方形时,正方形的物体将被拉伸,从而显示为矩形。

实际上,默认的“Effect”模型视图矩阵也是一个单位矩阵。

透视投影中的观察点位于原点(0,0,0),并沿着Z轴的负方向进行观察,就像是从屏幕内部看进去。

 

1、修改投影矩阵

为了正确显示,需要修改投影矩阵。在“update”方法中添加下面的代码:



 


首先计算出屏幕的纵横比(aspect),然后缩放单位矩阵的Y轴,强制将Y轴的单位刻度与X轴保持一致。

 

2、渲染观察效果



 

3、使用透视投影矩阵

把单位矩阵做拉伸,本质上仍然是一个正交投影。要模拟人眼观察世界的效果,则必须使用透视投影。

把上面的代码做一些修改:



 

使用GLKit自带的方法创建出一个透视矩阵,视角、纵横比、近平面、远平面。

渲染效果如下:



 

4、修改模型视图矩阵

上图看起来感觉像一个正方形,但似乎左右两边没显示完整。

原因是,正方形与透视视点距离太近。

有两个方法解决这个问题:一是修改原始的顶点数据(Z轴值),使之透视视点;二是通过所谓的“模型视图矩阵”,将正方形“变换”到远一点的位置。

添加以下代码:



 这样,同样再次显出一个精确的正方形。


七、使用纹理

1、准备纹理

在PS中剪切、调节纹理尺寸(512*512),并保存为Tulips.JPG。本例中使用的图像是一幅黄色的郁金香。

然后在XCODE,导入进工程中。


 

2、使用GLKTextureLoader加载纹理

在“ viewDidLoad ”方法的后面,追加下列代码:


 

首先用“NSBundle”找到资源“Tulips.jpg”的路径,然后用“GLKTextureLoader”类方法同步加载这个纹理,也可以用它的实例方法异步进行加载。

默认,此图片加载进TEXTURE0,如果需要加载进其他单元,需要先用指令“glActiveTexure(GL_TEXTUREn)”。——n为1-(CL_COMBINED_TEXTURE_IMAGE_UNITS-1)中的一个数值。

加载成功后,该纹理的信息都保存在“textureInfo”中,以后,直接使用此变量的相关属性,就可以在OpenGL中应用这个纹理了。


3、将纹理绑定到Effect

接着,继续添加后续代码:



 

4、渲染



 

与意料中的结果似乎有差距,黄色的花瓣变成了绿色?图像是上下颠倒的?



八、纹理细节调整

造成上面错误是原因是:

在最初构造Effect光照时,使用了绿色,所以整个纹理被“染”成为绿色。

图像颠倒是因为纹理的坐标原点不在左下角。

1、修改光照颜色



 

2、将纹理坐标原点改为左下角

GLKit加载纹理,默认都是把坐标设置在“左上角”。然而,OpenGL的纹理贴图坐标却是在左下角,这样刚好颠倒。

在加载纹理之前,添加一个“options”:



 

这个参数可以让系统在加载纹理后,做一些基本的处理。如预乘Alpha、创建“Mipmaps”等。

 

3、渲染,一切正常






九、使用自定义的着色器

迄今,例中只是简单地调用了“GLKit”内置的着色程序进行渲染。但是,在某些情况下,可能需要使用自己的特殊的着色器。

1、编写着色程序

一个着色器由两个部分构成(可以是两个文件,也可以是硬编码嵌在程序中的两段代码字串)。

它们分别是:顶点着色程序和片段着色程序。

创建两个“Empty”文件,分别命名为“v.shader”和“f.shader”。



  

然后,两个文件分别写入这些代码:



2、加载、存储、编译、附着、链接

在OpenGL中使用自定义着色器,过程比较繁琐。

首先需要加载这个文件-->把它转换为GLChar(UTF8编码)-->保存进GUP内存-->编译内存中的字串代码-->附着给“program”对象。

上述过程要进行两次(分别为顶点程序和片段程序)。

最后,将“program”链接到当前“Context”,这样才能在OpenGL中发挥作用。

为了简化代码,可以写成两个方法,作为公共的加载方法使用:


  

3、开始加载自定义的着色器

在“viewDidLoad”方法里追加以下代码:




如何判断着色器是否能正常工作?可以用:

glGetProgramiv(_program, GL_LINK_STATUS, &params);

如果返回的“params”为1,则说明一切正常。

另外,上面代码中“_program”为新添加进去的公共变量


   

4、为着色器提供参数

顶点着色程序需要一个属性参数:position(表示顶点的位置)

 

5、在“glkView”方法后追加



渲染结果为:



屏幕上出现两个矩形,有图案的是用“effct”渲染的,上面红色的是用“_program”自定义着色器渲染的。



十、增加着色器显示纹理

上述着色器是一个超级简单的着色器(几乎没实现什么功能,仅是简单地着为红色)。

下面逐渐增加它的功能。


1、修改着色器

给顶点着色器,增加纹理坐标属性“TexCoord”和该坐标的输出“coord”(此输出将在片段着色器中使用)。



给片段着色器,增加纹理坐标输入“coord”,以及统一的“sampler2D”变量。



2、绑定着色器变量

要使着色器正常工作,必须提供它需要的参数内容。

在“viewDidload”方法后,添加“绑定”代码:


  

第一部分是绑定“position”属性到通用的的顶点属性索引“0”上,绑定“texCoord”到通用的顶点属性索引“3”上。(索引1是法线,2是顶点颜色)。

绑定后,必须调用“glLinkProgram”方法才能生效。

 

第二部分,绑定“统一的纹理sampler2D”变量,到纹理0号单元——在使用“GLKTextureLoader”加载纹理时,默认是激活了“0”号单元。当然,如果是激活其他单元(例如8),则这里就相应的改为8。

绑定之前,必须调用“glUseProgram”才起作用。


3、运行渲染





十一、着色器顶点变换矩阵

在上述着色器代码中,是直接使用:

gl_Position = position;

也就是说,顶点位置没有经过任何变换,直接使用它的原始数据(所以它的图像也被显示为一个矩形)。


1、引入变换矩阵

修改顶点着色器代码:



  

添加了一个统一的矩阵变量“modelViewProjectionMatrix”(模型、视图、投影矩阵,是这三个变换矩阵合并后(乘法),得到一个单个的矩阵)。将来,要在主程序中将矩阵值传入。


2、传入矩阵值

在“update”方法中,追加下面的代码:



  

查询到“modelViewProjectionMatrix”变量à计算合并矩阵à传给着色器。

传入着色器的值是modelViewProjectionMatrix.m,注意后面的“m”,它表示是一维数组形式的矩阵。


3、再次渲染



 

因为,“Effect”的变换矩阵与“着色器”的渲染结果相同,所以,显示为两个完全重合的正方形。


4、偏离屏幕中心

为了更便于观察,下面将“着色器”渲染的正方形偏离一下。

修改代码:




 


在合并矩阵之前,先把“modelViewMatrix”做一个平移(1.0,1.0,-1.0)。

结果为:




 


注意到两个图像的颜色略有差别,这是因为“Effect”内置的着色器使用了光源。而自定义的着色器没有光效代码,它完全照搬了纹理的“原色”。另外,后面那个正方形变小了,是因为它更远离了“相机”。


十二、动画

所谓动画,其实就是在“update”中有规律地修改一些Matrix参数,连续刷新时,即产生动画的“错觉”。

1、旋转动画

添加一些代码,如下:




  

首先要添加一个全局旋转变量“_rotation”。

然后让它每帧旋转一点点,并以此修改“modelVireMatrix”矩阵。


2、渲染动画效果



 

结果是,正方形围绕“自己”进行了旋转。

如果希望它绕屏幕中心旋转,怎么做?


3、理解矩阵堆栈

OpenGL的矩阵变换是放在一个矩阵堆栈中的(后进先出),代码中矩阵变换的次序,决定了堆栈中矩阵的变换顺序。所以,上述矩阵的变换实际上是倒过来进行的:先做平移,再做旋转,这样它就围绕屏幕中心旋转了。

把上面的代码中“平移”和“旋转”交换一下次序即可:





(完)