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);
}

最后运行就可以看到你想要的图形了:
iOS开发学习OpenGL ES系列 -- 第一个OpenGL ES项目_第1张图片

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

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);
}

最后效果:
iOS开发学习OpenGL ES系列 -- 第一个OpenGL ES项目_第2张图片

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

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

最后附上源码 LearningOpenGL ES GitHub

你可能感兴趣的:(iOS开发学习OpenGL ES系列 -- 第一个OpenGL ES项目)