在前一节,我们对OpenGL进行概述和其中一些专有名词。或许有些人刚开始对于接触这些名词,会有一头雾水的感觉,这是正常。
现在我们通过创建第一个项目,在屏幕中绘制出一个三角形,来理解这些名词。
我使用的iOS框架中的GLKViewController来绘制三角形,这个Controller已经将OpenGL所需要的环境和变量进行了一次封装,可以方便开发者使用。如果需要用复杂的并且可以控制的,也可以选择用普通的View来转化为OpenGL需要使用的环境,自己控制每一过程。
构建OpenGL环境
OpenGL es有和我们使用图像绘制一样有个管理上下文的类,叫:EAGLContext。创建时候会让我们选择OpenGL的Api版本,我们这里选择OpenGLES2
self.context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
if (!self.context) {
NSLog(@"初始化失败");
exit(1);
}
设置视图环境和编码
每个视图在处理不同的图片的时候都有着不同的视图编码,先进行设置,避免后面会有错误
GLKView *view = (GLKView *)self.view;
[view setContext:self.context];
view.drawableDepthFormat = GLKViewDrawableColorFormatRGBA8888;
//检查当前上下文是否是我们穿件的 Context
if (![EAGLContext setCurrentContext:self.context]) {
NSLog(@"Failed to set current OpenGL context");
exit(1);
}
这里我们就完成了对于OpenGL的环境的搭建和设置,接下来就要开始看看如何创建三角形,创建三角形都需要什么?
顶点输入
开始绘制图形之前,我们必须给OpenGL输入一些顶点数据,也就是这个三角形需要在屏幕的什么位置。
OpenGL是一个3D图形库,所以我们在OpenGL中指定的所有坐标都是3D坐标(x、y、z)。OpenGL不是简单地把所有的3D坐标变换为屏幕上的2D像素;OpenGL仅当3D坐标在3个轴(x、y和z)上都为-1.0到1.0的范围内时才处理它。
由于我们要创建一个2D的三角形,我们要给三个顶点坐标,每个顶点都表示一个在3D空间的位置。我们会将它们以标准化设备坐标的形式(OpenGL的可见区域)定义为一个float数组。
//顶点数据
const GLfloat vertices[] = {
0.5, -0.5, 0.0f, 1.0f, 0.0f, 0.0f, //右下(x,y,z坐标 + rgb颜色)
-0.5, 0.5, 0.0f, 0.0f, 1.0f, 0.0f, //左上
-0.5, -0.5, 0.0f, 0.0f, 0.0f, 1.0f, //左下
};
因为我们要创建的是一个2D的三角形,所以这个三角形的深度都是一样的,我们都给0.0f。
顶点坐标.png通常深度可以理解为z坐标,它代表一个像素在空间中和你的距离,如果离你远就可能被别的像素遮挡,你就看不到它了,它会被丢弃,以节省资源。
另外我们可以创建一个索引坐标,用来控制三角形绘制顺序。也可以不需要,不需要的话,在最后的绘制方法需要有所变化。这里我们使用索引来控制。和顶点坐标一样需要创建一个数组:
const GLuint indices[] = {
0,1,2
};
定义这样的顶点数据以后,我们会把它作为输入发送给图形渲染管线的第一个处理阶段:顶点着色器。它会在GPU上创建内存用于储存我们的顶点数据,还要配置OpenGL如何解释这些内存,并且指定其如何发送给显卡。
我们通过顶点缓冲对象 (Vertex Buffer Objects, VBO)管理这个内存,它会在GPU内存(通常被称为显存)中储存大量顶点。使用这些缓冲对象的好处是我们可以一次性的发送一大批数据到显卡上,而不是每个顶点发送一次。从CPU把数据发送到显卡相对较慢,所以只要可能我们都要尝试尽量一次性发送尽可能多的数据。当数据发送至显卡的内存中后,顶点着色器几乎能立即访问顶点,这是个非常快的过程。
绑定顶点数据
顶点缓冲对象是中第一个出现的OpenGL对象。就像OpenGL中的其它对象一样,这个缓冲有一个独一无二的ID:
//用于跟踪每个顶点信息的
GLuint verticesBuffer;
glGenBuffers(1, &verticesBuffer);//创建一个缓存对象
OpenGL有很多缓冲对象类型,顶点缓冲对象的缓冲类型是GL_ARRAY_BUFFER。OpenGL允许我们同时绑定多个缓冲,只要它们是不同的缓冲类型。我们可以使用glBindBuffer函数把新创建的缓冲绑定到GL_ARRAY_BUFFER目标上:
//激活缓冲对象. OpenGL有很多缓冲对象类型,顶点缓冲对象的缓冲类型
glBindBuffer(GL_ARRAY_BUFFER, verticesBuffer);
从这一刻起,我们使用的任何(在GL_ARRAY_BUFFER目标上的)缓冲调用都会用来配置当前绑定的缓冲(顶点数据VBO)。然后我们可以调用glBufferData函数,它会把之前定义的顶点数据复制到缓冲的内存中:
/**
GL_STATIC_DRAW :数据不会或几乎不会改变。
GL_DYNAMIC_DRAW:数据会被改变很多。
GL_STREAM_DRAW :数据每次绘制时都会改变。
*/
//调用glBufferData函数,它会把之前定义的顶点数据复制到缓冲的内存中
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glBufferData是一个专门用来把用户定义的数据复制到当前绑定缓冲的函数。
- 第一个参数是目标缓冲的类型:顶点缓冲对象当前绑定到GL_ARRAY_BUFFER目标上。
- 第二个参数指定传输数据的大小(以字节为单位);用一个简单的sizeof计算出顶点数据大小就行。
- 第三个参数是我们希望发送的实际数据。
- 第四个参数指定了我们希望显卡如何管理给定的数据。它有三种形式:
- GL_STATIC_DRAW :数据不会或几乎不会改变。
- GL_DYNAMIC_DRAW:数据会被改变很多。
- GL_STREAM_DRAW :数据每次绘制时都会改变。
三角形的位置数据不会改变,每次渲染调用时都保持原样,所以它的使用类型最好是GL_STATIC_DRAW。
索引的使用流程和顶点数据的流程一样,同样需要绑定,当是索引绑定的类型不同
//用于跟踪组成每个三角形的索引信息
GLuint indicesBuffer;
glGenBuffers(1, &indicesBuffer);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indicesBuffer);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
链接顶点属性(启动顶点数据)
顶点着色器允许我们指定任何以顶点属性为形式的输入。这使其具有很强的灵活性的同时,它还的确意味着我们必须手动指定输入数据的哪一个部分对应顶点着色器的哪一个顶点属性。所以,我们必须在渲染前指定OpenGL该如何解释顶点数据。
我们的顶点缓冲数据会被解析为下面这样子:
image.png
- 位置数据被储存为32位(4字节)浮点值。
- 每个位置包含3个这样的值。
- 在这3个值之间没有空隙(或其他值)。这几个值在数组中紧密排列(Tightly Packed)。
- 数据中第一个值在缓冲开始的位置。
有了这些信息我们就可以使用glVertexAttribPointer函数告诉OpenGL该如何解析顶点数据(应用到逐个顶点属性上)了:
//开启对应的顶点属性(坐标,纹理坐标,颜色等)
glEnableVertexAttribArray(GLKVertexAttribPosition);
glEnableVertexAttribArray(GLKVertexAttribColor);
/**
为顶点属性(坐标,纹理坐标,颜色等)配置合适的值
参数1:GLuint indx 声明这个属性的名称
参数2:GLint size定义这个属性由多少个值组成。譬如说position是由3个GLfloat组成
参数3: GLenum type声明每一个值是什么类型。我们都用了GL_FLOAT
参数4:GLboolean normalized , GL_FALSE就好了
参数5:GLsizei stride stride的大小,描述每个vertex数据的大小
参数6:const GLvoid* ptr , 数据结构的偏移量。从这个结构中哪里开始获取值。
Position的值在前面,所以传(GLfloat *)NULL + 0进去就可以了。
而TexCoord是紧接着位置的数据,而position的大小是3个float的大小,所以是从(GLfloat *)NULL + 3开始的
*/
glVertexAttribPointer(GLKVertexAttribPosition, 3, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 6, (GLfloat *)NULL + 0);
glVertexAttribPointer(GLKVertexAttribColor, 3, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 6, (GLfloat *)NULL + 3);
这过程的完整代码
- (void)setupVBOs {
//用于跟踪每个顶点信息的
GLuint verticesBuffer;
glGenBuffers(1, &verticesBuffer);//创建一个缓存对象
glBindBuffer(GL_ARRAY_BUFFER, verticesBuffer);//激活缓冲对象. OpenGL有很多缓冲对象类型,顶点缓冲对象的缓冲类型是GL_ARRAY_BUFFER
/**
GL_STATIC_DRAW :数据不会或几乎不会改变。
GL_DYNAMIC_DRAW:数据会被改变很多。
GL_STREAM_DRAW :数据每次绘制时都会改变。
*/
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);//调用glBufferData函数,它会把之前定义的顶点数据复制到缓冲的内存中
//用于跟踪组成每个三角形的索引信息
GLuint indicesBuffer;
glGenBuffers(1, &indicesBuffer);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indicesBuffer);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
//开启对应的顶点属性(坐标,纹理坐标,颜色等)
glEnableVertexAttribArray(GLKVertexAttribPosition);
glEnableVertexAttribArray(GLKVertexAttribColor);
/**
为顶点属性(坐标,纹理坐标,颜色等)配置合适的值
参数1:GLuint indx 声明这个属性的名称
参数2:GLint size定义这个属性由多少个值组成。譬如说position是由3个GLfloat组成
参数3: GLenum type声明每一个值是什么类型。我们都用了GL_FLOAT
参数4:GLboolean normalized , GL_FALSE就好了
参数5:GLsizei stride stride的大小,描述每个vertex数据的大小
参数6:const GLvoid* ptr , 数据结构的偏移量。从这个结构中哪里开始获取值。
Position的值在前面,所以传(GLfloat *)NULL + 0进去就可以了。
而TexCoord是紧接着位置的数据,而position的大小是3个float的大小,所以是从(GLfloat *)NULL + 3开始的
*/
glVertexAttribPointer(GLKVertexAttribPosition, 3, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 6, (GLfloat *)NULL + 0);
glVertexAttribPointer(GLKVertexAttribColor, 3, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 6, (GLfloat *)NULL + 3);
}
顶点着色器
顶点着色器(Vertex Shader)是几个可编程着色器中的一个。如果我们打算做渲染的话,现代OpenGL需要我们至少设置一个顶点和一个片段着色器。
片段着色器
片段着色器(Fragment Shader)是第二个也是最后一个我们打算创建的用于渲染三角形的着色器。片段着色器所做的是计算像素最后的颜色输出。
这里我们使用iOS系统框架中已经构建好的编译类来完成
- (void)setupBaseEffect {
self.mEffect = [[GLKBaseEffect alloc] init];
//启动着色器
[self.mEffect prepareToDraw];
}
在初始化这个类的时候,系统内部就进行了对着色器的编译。我们只要对它进行启用就好。
在iOS的GLKit中有多种基础的着色器类
-
- 提供基于着色器的OpenGL渲染效果的对象的标准界面。
-
- 用于基于着色器的OpenGL渲染中的一个简单的照明和着色系统。
-
- 用于基于着色器的OpenGL渲染的支持反射映射的照明和着色系统。
-
- 用于基于着色器的OpenGL渲染的一个简单的天空盒视觉效果。
绘制三角形
终于到最后一步开始绘制了。
因为这类是继承于GLKViewController,它的会自动执行一个代理函数GLKViewDelegate,我们在那进行绘制三角形。
官方文档对于这个方法的描述是该方法的语义与drawRect:方法相同,用来绘制view的内容的。
#pragma mark - GLKViewDelegate
- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect {
glClearColor(0.3f, 0.6f, 1.0f, 1.0f);//设置清除颜色
//把窗口清除为当前颜色 和 清除深度缓冲区
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glDrawElements(GL_TRIANGLES, 3, GL_UNSIGNED_INT, 0);
}
重点说下glDrawElements(GL_TRIANGLES, 3, GL_UNSIGNED_INT, 0);
- 第一个参数:表示绘制的基本图元类型 GL_POINTS, GL_LINE_STRIP等。
- 第二个参数:参数count表示使用的EBO(索引缓冲区对象)中索引元素的个数
- 第三个参数:参数type 表示索引数据的数据类型。必须取 GL_UNSIGNED_BYTE, GL_UNSIGNED_SHORT, 或者 GL_UNSIGNED_INT 三者之一。
- 第四个参数:indices 表示EBO中索引的偏移量。
还有需要注意的是千万不要在绘图方法中去调用display方法,什么是绘图方法,比如说UIView的drawRect:(CGRect)rect
方法还有glkView:(GLKView *)view drawInRect:(CGRect)rect
。因为会循环调用,循环调用导致崩溃
完成
三角形.png如果不使用索引来构建三角形?提示:glDrawArrays(GL_TRIANGLES, 0, 3);
如果把三角形变成一个矩形呢?提示:两个三角形组成一个矩形
不用索引来构建一个矩形?
网友评论