iOS开发OpenGL ES - Hello Triangle

在第一个项目学习之前我们需要先了解一下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,如下:

这里我们了解一下OpenGL的渲染流程:
iOS开发OpenGL ES - Hello Triangle_第1张图片

从图中可以看出,最开始的输入是顶点数据。比如三角形,就是三个点。每个顶点数据可以包含任意数量的信息,最基本的有位置,颜色。后面介绍贴图时还会包含UV信息。经过各种处理,最终放入FrameBuffer。

第一步定义顶点坐标数据:

// 定义C结构体用来保存GLKVector3类型的成员position
typedef struct {
    GLKVector3 position;
} Vertex;

// C数组存储三角形顶点坐标,每个GLKVector3变量代表一个坐标点的X、Y和Z
static const Vertex vertices[] = {
    {{  0.0,  0.5, 0.0}},
    {{ -0.5, -0.5, 0.0}},
    {{  0.5, -0.5, 0.0}},
};

第二步初始化和设置当前视图的EAGLContext对象:

// 将当前view强制转换为GLKView
GLKView *view = (GLKView *)self.view;
    
// 断言,检测用storyboard加载的视图是否是GLKView
NSAssert([view isKindOfClass:[GLKView class]], @"View controller's View is not a GLKView");
    
// 初始化context为OpenGL ES 2.0
view.context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
    
// 在任何其他的OpenGL ES配置或者渲染发生前,应用的GLKView实例的上下文属性都需要设置为当前
[EAGLContext setCurrentContext:view.context];
还需要初始化baseEffect属性,
/*
GLKBaseEffect 是GLKit提供的另一个内建类。GLKBaseEffect的存在是为了简化OpenGL ES的很多常用操作。
在OpenGL ES 2.0中,如果没有GLKit和GLKBaseEffect类,完成这个简单的例子需要用着色器语言编写一个GPU程序。
*/
@property (nonatomic, strong) GLKBaseEffect *baseEffect;

self.baseEffect = [[GLKBaseEffect alloc] init];
self.baseEffect.useConstantColor = GL_TRUE;
    
self.baseEffect.constantColor = GLKVector4Make(1.0,  // red
                                               0.0,  // green
                                               0.0,  // blue
                                               0.0); // alpha
// 设置当前OpenGL ES的上下文"清除颜色",用于在上下文的帧缓存被清除时初始化每个想色的颜色值
glClearColor(0.0, 0.0, 0.0, 1.0);

GLKBaseEffect类提供了不依赖所使用的OpenGL ES版本的控制OpenGL ES渲染方法,会在需要的时候自动地构建GPU程序并极大地简化本例。控制渲染像素颜色的方式有很多中,这个应用的GLKBaseEffect使用恒定红色来渲染三角形,所以三角形中的每一个像素都是相同的颜色。上面的constantColor定义为C数据结构体GLKVector4Make设置的4个颜色元素值。

前面已经了解过OpenGL的渲染流程,OpenGL需要接受顶点数据,然后经过图形管线处理,最终才能在屏幕上绘制出来。 现在我们在应用程序中定义好了图形的坐标数据。用于定义要绘制的三角形的顶点位置数据必须要发送到GPU来渲染。创建并使用一个用于保存顶点数据的顶点属性数组缓存。

1.为缓存生成一个独一无二的标识符
2.为接下来的运算绑定缓存
3.复制数据到缓存中
下面的这三步的带啊实现:

// 声明GLuint类型变量,用于存放本例中顶点数据的缓存标识符
GLuint vertexBufferID;

/*
生成标识符并保存在vertexBufferID变量中
glGenBuffers() 函数的第一个参数用于指定要生成的缓存标识符的数量,第二个参数是一个指针,指向生成的标识符的内存保存位置
*/
glGenBuffers(1, &vertexBufferID);

/*
glBindBuffer函数绑定用于指定标识符的缓存数据到当前缓存,
OpenGL ES保存不同类型的缓存标识符到当前OpenGL ES上下文的不同部位。
但是,在任意时刻每种类型只能绑定一个缓存。
如果在这个例子中使用了两个顶点属性数组缓存,那么在同一时刻它们不能都被绑定。

glBindBuffer() 的第一个参数是一个常量,用于指定要绑定哪一种类型的缓存。
OpenGL ES 2.0对于glBindBuffer() 的实现只支持两种类型的缓存,GL_ARRAY_BUFFER和GL_ELEMENT_ARRAY_BUFFER(后面会详细解释)。
GL_ARRAY_BUFFER类型用于指定一个顶点属性数组,例如本例中三角形顶点的位置。glBindBuffer()的第二个参数是要绑定的缓存的标识符。
*/ 
glBindBuffer(GL_ARRAY_BUFFER, vertexBufferID);

/*
glBufferData() 函数复制应用的顶点数据到当前上下文所绑定的顶点缓存中
第一个参数:指定要更新当前上下文中所绑定的是哪一个缓存
第二个参数:指定要复制这个缓存的字节数
第三个参数:要复制的字节地址
第四个参数:设置缓存数据在未来的运算中可能将会被怎样使用,这个信息可以帮助OpenGL ES优化内存使用
    使用GL_STATIC_DRAW:提示上下文缓存中的数据适合复制到GPU内存,因为很少对其进行修改
    使用GL_DYNAMIC_DRAW:提示上下文,数据内容会周期性改动,同时提示OpenGL ES以不同的方式来处理缓存的存储
    使用GL_STREAM_DRAW:提示上下文,缓存区数据会频繁改动
*/
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

通过上面三步,我们已经把应用中的顶点数据发送到GPU的内存缓存区了。

接下来就需要通过CPU执行代码告诉GPU如果使用这些数据了。

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

- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect {
    
    /*
     iOS的OpenGL中里有2个着色器, 一个是GLKBaseEffect,为了方便OpenGL ES 1.0转移到2.0的通用着色器。 
一个是OpenGL ES 2.0新添加的可编程着色器,使用跨平台的着色语言。
实例化基础效果实例,如果没有GLKit与GLKBaseEffect类,就需要为这个简单的例子编写一个小的GPU程序,使用2.0的Shading Language,
而GLKBaseEffect会在需要的时候自动的构建GPU程序。这里使用GLKBaseEffect来做着色器
     
“prepareToDraw”方法,是让“效果Effect”针对当前“Context”的状态进行一些配置,
它始终把“GL_TEXTURE_PROGRAM”状态定位到“Effect”对象的着色器上。
此外,如果Effect使用了纹理,它也会修改“GL_TEXTURE_BINDING_2D”。
     */
    
    [self.baseEffect prepareToDraw];
    
    // 前两行为渲染前的“清除”操作,清除颜色缓冲区和深度缓冲区中的内容。
    glClear(GL_COLOR_BUFFER_BIT);
    
    // 启动顶点缓存渲染操作
    glEnableVertexAttribArray(GLKVertexAttribPosition);
    
    /*
     告诉OpenGL ES每个顶点数据如何使用。
     第一个参数:指示当前绑定的缓存包含每个顶点的位置信息,也就是给哪个顶点属性赋值
     第二个参数:指定每个位置又3个数据部分
     第三个参数:告诉OpenGL ES每个部分都保存为一个浮点类型的值
     第四个参数:告诉OpenGL ES小数点固定数据是否可以被改变,本例中没有使用小数点固定的数据,因此赋值为GL_FALSE
     第五个参数:可以称为"步幅",指定了没哥顶点的保存需要多少个字节。简单点就是指定了GPU从一个顶点的内存碍事转到下一个顶点的内存开始位置需要跳过多少字节
     第六个参数:告诉OpenGL ES可以从当前绑定的顶点缓存的开始位置访问顶点数据
     */
    glVertexAttribPointer(GLKVertexAttribPosition, 3, GL_FLOAT, GL_FALSE, sizeof(SceneVertex), NULL);
    
    /*
     告诉OpenGL ES如何使用缓存数据之后就可以调用glDrawArrays()函数通过这些数据来绘制图形了
     第一个参数:告诉OpenGL ES怎么处理在绑定的顶点缓存内的顶点数据。本例中屎指示OpenGL ES去渲染三角形
     第二个参数:指定缓存内的需要渲染的第一个顶点的位置
     第三个参数:需要渲染的顶点数量
     */
    glDrawArrays(GL_TRIANGLES, 0, 3);
}

最后运行程序,可以看到我们想要的三角形了:
iOS开发OpenGL ES - Hello Triangle_第2张图片
Triangle

放上本例完整代码,以方便大家参考:

#import "ViewController.h"

@interface ViewController () {
    
    // 声明GLuint类型变量,用于存放本例中顶点数据的缓存标识符
    GLuint vertexBufferID;
}

@property (nonatomic, strong) GLKBaseEffect *baseEffect;

@end

typedef struct {
    GLKVector3 positionCoords;
    
} SceneVertex;

static const SceneVertex vertices[] = {
    
    {{  0.0,  0.5, 0.0}},
    {{ -0.5, -0.5, 0.0}},
    {{  0.5, -0.5, 0.0}},
};

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 将当前view强制转换为GLKView
    GLKView *view = (GLKView *)self.view;
    
    // 断言,检测用storyboard加载的视图是否是GLKView
    NSAssert([view isKindOfClass:[GLKView class]], @"View controller's View is not a GLKView");
    
    // 初始化context为OpenGL ES 2.0
    view.context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
    
    // 在任何其他的OpenGL ES配置或者渲染发生前,应用的GLKView实例的上下文属性都需要设置为当前
    [EAGLContext setCurrentContext:view.context];
    
    /*
     初始化baseEffect
     GLKBaseEffect 是GLKit提供的另一个内建类。GLKBaseEffect的存在是为了简化OpenGL ES的很多常用操作。GLKBaseEffect隐藏了iOS设备支持的多个OpenGL ES版本之间的差异。
     
     在OpenGL ES 2.0中,如果没有GLKit和GLKBaseEffect类,完成这个简单的例子需要用着色器语言编写一个GPU程序。
     GLKBaseEffect会在需要的时候自动地构建GPU程序并极大地简化本书中的例子
     */
    self.baseEffect = [[GLKBaseEffect alloc] init];
    self.baseEffect.useConstantColor = GL_TRUE;
    
    self.baseEffect.constantColor = GLKVector4Make(1.0,  // red
                                                   0.0,  // green
                                                   0.0,  // blue
                                                   0.0); // alpha
    
    glClearColor(0.0, 0.0, 0.0, 1.0);
    
    glGenBuffers(1, &vertexBufferID);
    glBindBuffer(GL_ARRAY_BUFFER, vertexBufferID);
    
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
    
}

- (void)update{

}

- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect {
    
  /*
     iOS的OpenGL中里有2个着色器, 一个是GLKBaseEffect,为了方便OpenGL ES 1.0转移到2.0的通用着色器。 
一个是OpenGL ES 2.0新添加的可编程着色器,使用跨平台的着色语言。
实例化基础效果实例,如果没有GLKit与GLKBaseEffect类,就需要为这个简单的例子编写一个小的GPU程序,使用2.0的Shading Language,
而GLKBaseEffect会在需要的时候自动的构建GPU程序。这里使用GLKBaseEffect来做着色器
     
“prepareToDraw”方法,是让“效果Effect”针对当前“Context”的状态进行一些配置,
它始终把“GL_TEXTURE_PROGRAM”状态定位到“Effect”对象的着色器上。
此外,如果Effect使用了纹理,它也会修改“GL_TEXTURE_BINDING_2D”。
     */
    
    [self.baseEffect prepareToDraw];
    
    // 前两行为渲染前的“清除”操作,清除颜色缓冲区和深度缓冲区中的内容。
    glClear(GL_COLOR_BUFFER_BIT);
    
    // 启动顶点缓存渲染操作
    glEnableVertexAttribArray(GLKVertexAttribPosition);
    
    /*
     告诉OpenGL ES每个顶点数据如何使用。
     第一个参数:指示当前绑定的缓存包含每个顶点的位置信息,也就是给哪个顶点属性赋值
     第二个参数:指定每个位置又3个数据部分
     第三个参数:告诉OpenGL ES每个部分都保存为一个浮点类型的值
     第四个参数:告诉OpenGL ES小数点固定数据是否可以被改变,本例中没有使用小数点固定的数据,因此赋值为GL_FALSE
     第五个参数:可以称为"步幅",指定了没哥顶点的保存需要多少个字节。简单点就是指定了GPU从一个顶点的内存碍事转到下一个顶点的内存开始位置需要跳过多少字节
     第六个参数:告诉OpenGL ES可以从当前绑定的顶点缓存的开始位置访问顶点数据
     */
    glVertexAttribPointer(GLKVertexAttribPosition, 3, GL_FLOAT, GL_FALSE, sizeof(SceneVertex), NULL);
    
    /*
     告诉OpenGL ES如何使用缓存数据之后就可以调用glDrawArrays()函数通过这些数据来绘制图形了
     第一个参数:告诉OpenGL ES怎么处理在绑定的顶点缓存内的顶点数据。本例中屎指示OpenGL ES去渲染三角形
     第二个参数:指定缓存内的需要渲染的第一个顶点的位置
     第三个参数:需要渲染的顶点数量
     */
    glDrawArrays(GL_TRIANGLES, 0, 3);
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

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

代码已上传至GitHubLearnOpenGL ES

你可能感兴趣的:(iOS开发OpenGL ES - Hello Triangle)