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呢,这是因为在这里有一个的概念,我们声明的变量,分别放在了不同的里,下图是对这个概念的示意。


我们在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。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,588评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,456评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,146评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,387评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,481评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,510评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,522评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,296评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,745评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,039评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,202评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,901评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,538评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,165评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,415评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,081评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,085评论 2 352

推荐阅读更多精彩内容