iOS 开发 OpenGL 新手入门
一、搭建开发环境
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, ¶ms);
如果返回的“params”为1,则说明一切正常。
另外,上面代码中“_program”为新添加进去的公共变量
4、为着色器提供参数
顶点着色程序需要一个属性参数:position(表示顶点的位置)
5、在“glkView”方法后追加
渲染结果为:
屏幕上出现两个矩形,有图案的是用“effct”渲染的,上面红色的是用“_program”自定义着色器渲染的。
十、增加着色器显示纹理
上述着色器是一个超级简单的着色器(几乎没实现什么功能,仅是简单地着为红色)。
下面逐渐增加它的功能。
1、修改着色器
给顶点着色器,增加纹理坐标属性“TexCoord”和该坐标的输出“coord”(此输出将在片段着色器中使用)。
给片段着色器,增加纹理坐标输入“coord”,以及统一的“sampler2D”变量。
片段的颜色改为由“texture2D”函数计算出来,实际上就是按纹理坐标从纹理像素中取样。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的矩阵变换是放在一个矩阵堆栈中的(后进先出),代码中矩阵变换的次序,决定了堆栈中矩阵的变换顺序。所以,上述矩阵的变换实际上是倒过来进行的:先做平移,再做旋转,这样它就围绕屏幕中心旋转了。
把上面的代码中“平移”和“旋转”交换一下次序即可:
(完)