前言
上一篇文章我们认识并编写了一个最简单的Shader,这一篇文章我们将会把第一个图形真正的画出来,本文是系列最关键的部分,如果你读懂了本文,以后学习OpenGL都会很容易理解。
本文目标
了解GPU程序编译过程,并且绘制出第一个三角形。
正文
1.创建VBO
从上一篇文章,我们了解到第一步需要准备数据,即Shader需要从哪里进行数据读取。
VBO全称Vertex Buffer Object,就是给Shader提供数据的地方,我们先创建一个C语言数组用来保存顶点的位置。
float positions[] =
{
-0.5f,-0.5f,0.0f,
0.5f,-0.5f,0.0f,
0.0f,0.5f,0.0f,
};
这个数组现在有9个数据,其中每一行都代表一个顶点(Vertex)的XYZ坐标,因为我们要画的是一个三角形,所以这里有3组数据。
创建完数据之后,我们需要使用这些数据,也就是把它导入到GPU里面去,因为OpenGL是一个状态机,所以我们在设置之前需要先启用状态,我们设置什么状态的属性,就要先启用什么状态。
GLuint vbo; // 创建一个vbo对象
glGenBuffers(1, &vbo); //生成vbo
glBindBuffer(GL_ARRAY_BUFFER, vbo); //开启设置当前vbo对象的状态
glBufferData(GL_ARRAY_BUFFER, sizeof(float) * 3 * 3, positions, GL_STATIC_DRAW); // 给当前的vbo设置数据
glBindBuffer(GL_ARRAY_BUFFER, 0); //一个良好的习惯,设置回默认状态
每一句注释的说法可能不是那么精确,当大家了解了以后自然会理解,这里只是采用一些便于理解的说法。
接下来解释一下代码以及参数的含义。
1.首先创建一个vbo
对象,这个对象是Gluint
类型。
2.生成vbo
,第一个参数代表需要生成多少个vbo
,第二个传递vbo
对象的地址,如果生成多个,第一个参数可以写具体生成的个数,第二个传递vbo
数组。
3.�绑定vbo
对象,第一个参数GL_ARRAY_BUFFER
用于为顶点数组传值。
4.给我们生成的vbo
对象设置数据,第二个参数是当前数据的个数,我们当前一共9个数据,所以传9,第三个参数传递我们的顶点坐标数组,第四个参数GL_STATIC_DRAW
意味着是把这些数据基本不会改变,它还有另外一个参数是GL_DYNAMIC_DRAW
,意味着这个数据会被频繁的改变,GL_STREAM_DRAW
意味着数据每帧都不同,系统会根据这个参数来为缓冲区对象分配最佳的存储位置。
5.因为OpenGL是一个状态机,为了防止"误伤",我们要养成一个良好的习惯,就是在设置完一个数据之后,把状态设置回去。
2.编译Shader
一个C语言的编译过程大致包括预编译->编译->汇编->链接,GLSL跟它很相像,这一步我们先把Vertex Shader编译出来。
GLuint shader = glCreateShader(GL_VERTEX_SHADER); // 创建Shader
const GLchar *shaderCode = [self getShaderCodeWithPath:shaderPath]; // 读取我们写好的Shader代码
glShaderSource(shader, 1, &shaderCode, NULL); // 把代码设置给创建的Shader
glCompileShader(shader); // 编译Shader
// 以下为获取Shader编译时错误的代码
GLint compileStatus = GL_TRUE;
glGetShaderiv(shader, GL_COMPILE_STATUS, &compileStatus); // 获取是否有错误
if (compileStatus == GL_FALSE) { // 如果有错误
NSLog(@"compile shader error, error code is %s",shaderCode);
GLchar infoLog[1024] = {0}; // 创建储存错误信息的字符数组
GLsizei len = 0; // 出错信息的长度
glGetShaderInfoLog(shader, 1024, &len, infoLog); // 获取错误信息
NSLog(@"error log is %s",infoLog);
glDeleteShader(shader); // 把出错的Shader删掉
}
1.创建一个Shader,这里有两个参数,分别是GL_VERTEX_SHADER
和GL_FRAGMENT_SHADER
,对应两个不同的Shader类型。
2.glShaderSource
把我们写好的代码设置到创建出来的Shader,这一步类似于我们在Xcode里复制别人的代码写代码,第一个参数是我们设置的Shader,第二个参数是有多少个数据源,我们这里只有一个,所以传1,第三个是我们写好的代码,这里跟glGenBuffers
同理,第二个参数写了多个,第三个传一个字符串的数组,第四个参数如果我们的整个Shader文件不在同一个字符串里,则这里需要传一个对应长度的数组,比如第一句的长度,第二句的长度,因为我们这里是在一个字符串里,所以传NULL。
3.获取出错信息的代码注释已经写的很清楚了,大家看注释就好,这段代码是通用的。
Fragment Shader跟Vertex Shader编译代码相同,你只需要把第一句代码的参数改成GL_FRAGMENT_SHADER
即可。
3.创建连接GPU程序
这一步我们创建一个GPU程序,我们前面创建的Shader大家可以想象成是一个"库",这一步我们需要创建一个程序,把"库"放到程序里,然后连接这些"库",如果大家了解编译原理这里会很容易理解。
下面的代码就是完成上述的操作。
GLuint gpuProgram = glCreateProgram(); // 创建GPU程序
GLuint vsShader = [self compileVertexShaderWithPath:vshPath]; // 获取编译过的VertexShader"库"
GLuint fsShader = [self compileFragmentShaderWithPath:fshPath]; // 获取编译过的FragmentShader"库"
glAttachShader(gpuProgram, vsShader); // 将VertexShader"库"添加到程序
glAttachShader(gpuProgram, fsShader); // 将FragmentShader"库"添加到程序
glLinkProgram(gpuProgram); // 链接
glDetachShader(gpuProgram, vsShader); // 链接过后可以分离掉他们
glDetachShader(gpuProgram, fsShader);
// 下面是获取连接时错误的代码
GLint linkStatus = GL_TRUE;
glGetProgramiv(gpuProgram, GL_LINK_STATUS, &linkStatus);
if (linkStatus == GL_FALSE) {
NSLog(@"link error!");
GLchar infoLog[1024] = {0};
GLsizei len = 0;
glGetProgramInfoLog(gpuProgram, 1024, &len, infoLog);
NSLog(@"error is %s",infoLog);
glDeleteProgram(gpuProgram);
}
return gpuProgram;
同样的这里的"库"也是方便大家理解的一种说法,这里的代码参数较少,大家看的也比较轻松,就不加解释了,看注释应该就可以理解,注意这里的获取错误的方法跟编译Shader时是不同的,但是流程是相同的,都是先获得有没有编译错误,然后才获取错误信息。
4.获取Shader里变量的"位置"
在这里大家先思考一个问题,我们已经编译好了Shader,GPU程序也创建连接了,回想一下我们在上一篇编写Shader时,里面的attribute
这样的变量,我们又该怎么传入数据?假如我有10个attribute
,我们如何区分到底数据传递给了谁?
带着这样的疑问,我们来看一下OpenGL获取Shader里变量的代码。
GLint posLocation = glGetAttribLocation(_gpuProgram, "position");
它返回的是一个GLint类型,第一个参数传的是我们创建好的GPU程序,第二个是Shader里变量的名字,看到这里,大家可能犹如尿壶灌顶貌似已经明白了,但是我们仔细看一下这个函数的名字glGetAttribLocation
,如果是我们想的那样,它为什么不直接叫glGetAttribute
呢,不知道大家有没有看过一本叫沙僧日记
的书,书里最后封唐三藏为金刚罗汉果
,于是唐三藏就问了,佛祖,金刚罗汉就金刚罗汉,为什么还要加个果?因为这表明你已经修成正果。
同样的,为什么要加个Location呢,这是因为在这里有一个槽的概念,我们声明的变量,分别放在了不同的槽里,下图是对这个概念的示意。
我们在Shader声明的position变量就被放在了这些 槽里,所以这里需要获取的实际是变量的 "位置",被相同类型的修饰符修饰的变量 "槽"会放在一起,不同类型的变量会被放在不同的地方,他们不会冲突,这个在以后遇到别的变量类型修饰符时会进行解释
5.绘制图形
在上面我们已经创建好了GPU程序,这一小节我们就要使用它了。
在- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect
这个方法里,我们已经清除了颜色缓冲区,现在我们启用创建好的GPU程序。
glUseProgram(_gpuProgram); // 参数就是创建好的GPU程序
启用了程序之后,我们要给它设置数据源,也就是我们的vbo
。
glBindBuffer(GL_ARRAY_BUFFER, _vbo);
接下来,我们要启用我们的槽,也就是启用position
变量。
glEnableVertexAttribArray(_posLocation);
之后我们要把vbo的数据传递给position
变量,这也是我们最关键的一步,要实现数据的传递。
glVertexAttribPointer(_posLocation, 3, GL_FLOAT, GL_FALSE, sizeof(float) * 3, 0);
它的第一个参数是我们的position
变量的"位置",第二个参数是每个点由几个元素组成,我们的坐标是XYZ,所以传3,第三个参数是每个元素的类型,第四个参数是是否启用类型转换,因为GPU只支持浮点数,这里的意思是否把别的类型如int
转换成float
,因为我们本来就是float
类型,所以无需转换,第五个参数是点跟点之间的距离,这里可能需要懂一点内存的知识,为什么需要这个参数呢,我们回想一下上一篇文章说过GPU是并行的,它会把能容纳的所有的点同时加载,所以需要给出间隔,它就知道隔多少个点取几个点,这里是间隔3个float
类型,也就是距离3个元素的位置,因为我们XYZ是三个点。第六个参数是从第几个点开始。
数据传递好了,就可以进行绘制了。
glDrawArrays(GL_TRIANGLES, 0, 3);
第一个参数是绘制的类型,这里是三角形,第二个参数是从第几个点开始,第三个参数是绘制多少个点。
下面这一小节的完整代码
glUseProgram(_gpuProgram);
glBindBuffer(GL_ARRAY_BUFFER, _vbo);
glEnableVertexAttribArray(_posLocation);
glVertexAttribPointer(_posLocation, 3, GL_FLOAT, GL_FALSE, sizeof(float) * 3, 0);
glBindBuffer(GL_ARRAY_BUFFER, 0); // 不要忘记设置回去
glDrawArrays(GL_TRIANGLES, 0, 3);
glUseProgram(0); // 不要忘记设置回去
完成这一步,就可以点击运行看到我们的三角形了。
可能有朋友看到结果后会问,我们写的坐标明明很小,为什么这个三角形看起来这么大,这是因为在OpenGLES里,坐标是以屏幕的中心点为(0,0),中心点到屏幕边为1,具体如下图。
结束
在本文中我们已经把OpenGL的整个流程走完,大家也大致了解了我们如何创建一个GPU程序,编译Shader,以及数据的传递,看懂了本文,以后我们学习起来就会比较容易了,顺便在这里说一下,这个系列我现在初步想法是更侧重于制作图片跟视频滤镜的方向,因为具体iOS开发过程中,除了游戏很少会涉及到,这个系列也只是带大家入门,想要深入可以自行学习其他方面的知识,当然,除了滤镜以外的我也许也会写一些,不过应该不是重点。
本文Demo
我会尽量的在需要提供Demo的时候提供一下Demo,但是里面一般没有注释,文章里已经写的足够明白了,大家只需要稍微对比一下就可以看明白,最后在这里你可以下载到本文的Demo。