iOS开发-最接地气的OpenGL ES入门教程03-完成第一个三角形

前言

上一篇文章我们认识并编写了一个最简单的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_SHADERGL_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呢,这是因为在这里有一个的概念,我们声明的变量,分别放在了不同的里,下图是对这个概念的示意。

iOS开发-最接地气的OpenGL ES入门教程03-完成第一个三角形_第1张图片

我们在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); // 不要忘记设置回去

完成这一步,就可以点击运行看到我们的三角形了。

iOS开发-最接地气的OpenGL ES入门教程03-完成第一个三角形_第2张图片

可能有朋友看到结果后会问,我们写的坐标明明很小,为什么这个三角形看起来这么大,这是因为在OpenGLES里,坐标是以屏幕的中心点为(0,0),中心点到屏幕边为1,具体如下图。

iOS开发-最接地气的OpenGL ES入门教程03-完成第一个三角形_第3张图片

结束

在本文中我们已经把OpenGL的整个流程走完,大家也大致了解了我们如何创建一个GPU程序,编译Shader,以及数据的传递,看懂了本文,以后我们学习起来就会比较容易了,顺便在这里说一下,这个系列我现在初步想法是更侧重于制作图片跟视频滤镜的方向,因为具体iOS开发过程中,除了游戏很少会涉及到,这个系列也只是带大家入门,想要深入可以自行学习其他方面的知识,当然,除了滤镜以外的我也许也会写一些,不过应该不是重点。

本文Demo

我会尽量的在需要提供Demo的时候提供一下Demo,但是里面一般没有注释,文章里已经写的足够明白了,大家只需要稍微对比一下就可以看明白,最后在这里你可以下载到本文的Demo。

你可能感兴趣的:(iOS开发-最接地气的OpenGL ES入门教程03-完成第一个三角形)