iOS开发学习OpenGL ES系列 -- 第一个OpenGL ES项目

在第一个项目学习之前我们需要先了解一下iOS开发中管理OpenGL ES渲染的视图控制器 - GLKViewController

先看一下苹果官方概述:

A GLKViewController object works in conjunction with a GLKView object to display frames of animation in the view, and also provides standard view controller functionality.
To use this class, allocate and initialize a new GLKViewController subclass and set its view property to point to a GLKView object. Then, configure the view controller’s preferredFramesPerSecond property to the desired frame rate your application requires. You can set a delegate or configure other properties on the view controller, such as whether the animation loop is automatically paused or resumed when the application moves into the background.

一个GLKViewController对象与GLKView对象在视图中显示动画帧作品的同时,也提供了标准视图控制器的功能。使用这个类,分配和初始化一个新的GLKViewController类并设置其视图(View)属性指向一个GLKView对象。然后,配置视图控制器的preferredframespersecond属性设置应用程序要求所需的帧速率。您可以在视图控制器上设置委托或配置其他属性,例如当应用程序移动到后台时是否自动暂?;蚧指炊?。

When you set the view property to point to a GLKView object, if the view does not already have a delegate, then the view controller is automatically set as the view’s delegate.

如果你设置view属性指向一个GLKView对象,并且view没有设置代理,那么当前控制器会自动成为view的代理

When active, rendering loop automatically updates the view’s contents each time a new frame must be displayed. Each frame is rendered by the view controller using these steps:

  • The view controller calls its delegate’s glkViewControllerUpdate: method. Your delegate should update frame data that does not involve rendering the results to the screen.
  • The view controller calls its view’s display method. Your view should redraw the frame.

当运行时,渲染循环每次更新一个新的帧时会自动更新视图的内容。每个帧使用这些步骤由视图控制器呈现:
视图控制器调用代理方法glkViewControllerUpdate:,代理应该更新不需要将结果呈现到屏幕上的数据。
视图控制器调用视图的disolay方法。视图重新绘制。

Subclassing Notes
Your application should subclass GLKViewController and override the viewDidLoad and viewDidUnload methods. Your viewDidLoad method should set up your context and any drawable properties and can perform other resource allocation and initialization. Similarly, your class’s viewDidUnload method should delete the drawable object and free any unneeded resources.
As an alternative to implementing a glkViewControllerUpdate: method in a delegate, your subclass can provide an update method instead. The method must have the following signature:

子类注意:
你的应用程序应该集成自GLKViewController,并且重写viewDidLoad和viewDidUnload方法。在viewDidLoad中设置视图的上下文和初始化任何绘画可执行属性。同样,在viewDidUnload方法中可以删除绘制对象和释放不必要的资源。
glkViewControllerUpdate作为一个可供选择实现的方法,在代理方法中,子类可以提供一个更新的方法代替,方法必须具备以前特征: - (void)update

属性:
preferredFramesPerSecond: 设置视图更新内容的速率, 默认值为 30

framesPerSecond: 视图更新内容实际速率,只读属性

delegate: 设置代理对象

paused:bool值,设置暂停

pauseOnWillResignActive: bool值,表示当控制器挂起状态时是否自动暂停渲染循环,默认YES

resumeOnDidBecomeActive: bool值,表示当控制器激活时是否自动恢复渲染循环,默认YES

获取关于视图更新的信息(可读属性)
framesDisplayed:视图控制器自创建以来已发送的帧更新数。
timeSinceFirstResume:自第一次视图控制器恢复发送更新事件以来所经过的时间量。
timeSinceLastResume:自上一次视图控制器恢复发送更新事件以来所经过的时间量。
timeSinceLastUpdate:自从上次视图控制器调用代理方法glkviewcontrollerupdate经过的时间量
timeSinceLastDraw:自上次视图控制器调用视图的display方法以来所经过的时间量。

OpenGL ES渲染视图
GLKView:使用opengl绘制内容的默认实现视图

剩下两个代理属性:GLKViewDelegate和GLKViewControllerDelegate

关于GLKViewController就说这么多,接下来的OpenGL ES学习我们都是基于GLKViewController环境实现的,因为苹果的封装我们可以省掉一些基本的配置,比如生成默认的FrameBufferObject

开始项目:
在新建工程完毕,我们需要做几处修改。首先导入GLKit,修改当前控制器集成自GLKViewController,如下:

然后修改Main.storyboard中控制器view的class为GLKView,如下:

接下来在viewDidLoad中将它的View属性转为GLKView类型。OpenGL ES的上下文不仅会保存OpenGL ES的状态,还会控制GPU去执行渲染运算。所以需要初始化一个内建的EAGLContext类的实例对象,这个对象会封装一个特定于某个平台的OpenGL ES上下文。另外在任何其他的OpenGL ES配置或者渲染发生之前,应用的GLKView实例的上下文属性都需要设置为当前。

下面是代码实现:

// 将当前view强制转换为GLKView
GLKView *view = (GLKView *)self.view;

// 初始化当前视图上下文
view.context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];

// 在任何其他的OpenGL ES配置或者渲染发生前,应用的GLKView实例的上下文属性都需要设置为当前
[EAGLContext setCurrentContext:view.context];

环境配置完毕,接下来需要实例化一个GLKBaseEffect对象:

// 定义全局属性
@property (nonatomic, strong) GLKBaseEffect *baseEffect;

// 初始化
self.baseEffect = [[GLKBaseEffect alloc] init];

// 启用constantColor属性
self.baseEffect.useConstantColor = GL_TRUE;

// 设置渲染颜色(红色)
self.baseEffect.constantColor = GLKVector4Make(1.0, 0.0, 0.0, 1.0); 

GLKBaseEffect类提供了不依赖所使用的OpenGL ES版本的控制OpenGL ES渲染方法,会在需要的时候自动地构建GPU程序并极大地简化本例。也就是说,如果我们定义数据渲染图形,那么就需要定义两个着色器程序来处理数据才能渲染出图形来。而这里并没有写着色器程序,用的是GLKit框架提供的GLKBaseEffect类来自动生成着色器程序。

每当一个GLKView实例需要被重绘时,它都会让保存在视图的上下文属性中的OpenGL ES的上下文成为当前上下文。如果需要的话,GLKView实例会绑定与一个Core Animation层分享的帧缓存,执行其他的标准OpenGL ES配置,并发送一个消息来调用glkView:(GLKView *)view drawInRect:(CGRect)rect方法。

- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect {
    
    // 定义顶点坐标
    static GLfloat vertices[] = {
        0.0f,  0.5f,  0,
       -0.5f, -0.5f,  0,
        0.5f, -0.5f,  0,
    };
    
    /*
     “prepareToDraw”方法,是让“效果Effect”针对当前“Context”的状态进行一些配置,
     它始终把“GL_TEXTURE_PROGRAM”状态定位到“Effect”对象的着色器上。
     */
    [self.baseEffect prepareToDraw];
    
    // 前两行为渲染前的“清除”操作,清除颜色缓冲区和深度缓冲区中的内容。
    glClear(GL_COLOR_BUFFER_BIT);
    glClearColor(0.0, 0.0, 0.0, 1.0);
    
    // 启动顶点缓存渲染操作
    glEnableVertexAttribArray(GLKVertexAttribPosition);
    
    /*
     发送顶点数据到GPU内存
     第一个参数:指示当前绑定的缓存包含每个顶点的位置信息,也就是给哪个顶点属性赋值
     第二个参数:指定每个位置又3个数据部分
     第三个参数:告诉OpenGL ES每个部分都保存为一个浮点类型的值
     第四个参数:告诉OpenGL ES小数点固定数据是否可以被改变,本例中没有使用小数点固定的数据,因此赋值为GL_FALSE
     第五个参数:可以称为"步幅",指定了没哥顶点的保存需要多少个字节。简单点就是指定了GPU从一个顶点的内存碍事转到下一个顶点的内存开始位置需要跳过多少字节
     第六个参数:告诉OpenGL ES可以从当前绑定的顶点缓存的开始位置访问顶点数据
     */
    glVertexAttribPointer(GLKVertexAttribPosition, 3, GL_FLOAT, GL_TRUE, 6 * sizeof(GLfloat), (char *)vertices);
    
    /*
     告诉OpenGL ES如何使用缓存数据之后就可以调用glDrawArrays()函数通过这些数据来绘制图形了
     第一个参数:告诉OpenGL ES怎么处理在绑定的顶点缓存内的顶点数据。本例中屎指示OpenGL ES去渲染三角形
     第二个参数:指定缓存内的需要渲染的第一个顶点的位置
     第三个参数:需要渲染的顶点数量
     */
    glDrawArrays(GL_TRIANGLES, 0, 3);
}

最后运行就可以看到你想要的图形了:

背景色为我们设置的黑色:

glClearColor(0.0, 0.0, 0.0, 1.0);

三角形的颜色为红色:

self.baseEffect.constantColor = GLKVector4Make(1.0, 0.0, 0.0, 1.0); 

上面我们是设置baseEffect对象的constantColor属性来设置图形的渲染颜色。下面我们换一种方式来设置图形颜色,既然坐标可以通过顶点数组发送,那么颜色值也可以,修改顶点坐标数组:

static GLfloat vertices[] = {
        0.0,   0.5f,  0,  1,  0,  0, 
       -0.5f, -0.5f,  0,  0,  1,  0,
        0.5f, -0.5f,  0,  0,  0,  1,
};
// 每个坐标点的颜色都不一样,等会可以看看最终渲染出来是什么样的

激活GLKVertexAttribColor属性和赋值

glEnableVertexAttribArray(GLKVertexAttribColor);
glVertexAttribPointer(GLKVertexAttribColor, 3, GL_FLOAT, GL_TRUE, 6 * sizeof(GLfloat), (char *)vertices + 3 * sizeof(GLfloat));

glVertexAttribPointer函数在上面已经对每个参数做了解释,现在每个顶点不止X、Y、Z三个坐标值,还加了R、G、B三个颜色值。所以第五个参数为6 * sizeof(GLfloat)

每个顶点数据前面三个为坐标值,如果要取颜色值,就需要往后跳过3个位置,所以第六个参数为(char *)vertices + 3 * sizeof(GLfloat),从每个顶点的开始位置跳过3个sizeof(GLfloat)的位置再取就是颜色值了。

所以现在glkView:(GLKView *)view drawInRect方法为:

- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect {

    [self.baseEffect prepareToDraw];
    
    glClear(GL_COLOR_BUFFER_BIT);
    glClearColor(1.0, 1.0, 1.0, 1.0);
    
    static GLfloat vertices[] = {
        0.0,   0.5f,  0,  1,  0,  0, 
       -0.5f, -0.5f,  0,  0,  1,  0,
        0.5f, -0.5f,  0,  0,  0,  1,
    };
    
    glEnableVertexAttribArray(GLKVertexAttribPosition);
    glVertexAttribPointer(GLKVertexAttribPosition, 3, GL_FLOAT, GL_TRUE, 6 * sizeof(GLfloat), (char *)vertices);
    
    glEnableVertexAttribArray(GLKVertexAttribColor);
    glVertexAttribPointer(GLKVertexAttribColor, 3, GL_FLOAT, GL_TRUE, 6 * sizeof(GLfloat), (char *)vertices + 3 * sizeof(GLfloat));
    
    glDrawArrays(GL_TRIANGLES, 0, 3);
}

最后效果:

这个图片可能不是你所期望的那种,因为我们只提供了3个颜色,而不是我们现在看到的大调色板。这是在片段着色器中进行的所谓片段插值(Fragment Interpolation)的结果。当渲染一个三角形时,光栅化(Rasterization)阶段通常会造成比原指定顶点更多的片段。光栅会根据每个片段在三角形形状上所处相对位置决定这些片段的位置。
基于这些位置,它会插值(Interpolate)所有片段着色器的输入变量。比如说,我们有一个线段,上面的端点是绿色的,下面的端点是蓝色的。如果一个片段着色器在线段的70%的位置运行,它的颜色输入属性就会是一个绿色和蓝色的线性结合;更精确地说就是30%蓝 + 70%绿。
这正是在这个三角形中发生了什么。我们有3个顶点,和相应的3个颜色,从这个三角形的像素来看它可能包含50000左右的片段,片段着色器为这些像素进行插值颜色。如果你仔细看这些颜色就应该能明白了:红首先变成到紫再变为蓝色。片段插值会被应用到片段着色器的所有输入属性上。(这一段是在学习OpenGL ES资料上查到的,如果想要深入了解插值的可以自行搜索相关资料了)

看完这篇,你应该可以绘制出这个三角形了。如果还有不懂的欢迎留言讨论。近期在对OpenGL ES的学习之后才写下记录文章,如果有讲解不到位或者是错误的地方还请大家留言指正,谢谢!

最后附上源码 LearningOpenGL ES GitHub

最后编辑于
?著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,100评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,308评论 3 388
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,718评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,275评论 1 287
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,376评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,454评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,464评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,248评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,686评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,974评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,150评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,817评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,484评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,140评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,374评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,012评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,041评论 2 351

推荐阅读更多精彩内容