OpenGL ES零基础入门----(2)绘制三角形

前言

在前文<OpenGL ES 创建窗口>中我们学习了如何在 iOS 平台上设置OpenGL ES 环境,主要是设置 CAEAGLLayer 属性,创建 EAGLContext,创建和使用 renderbuffer和framebuffer,并知道如何清屏。但实际上并没有真正描绘点什么。在本文中,我们将学习OpenGL ES 渲染管线,顶点着色器和片元着色器相关知识,然后使用可编程管线在屏幕上描绘一个简单三角形。
学习参考网址:http://learnopengl-cn.readthedocs.io/zh/latest/

现在我们先简单的学习一下渲染管线着色器,以便我们接下来的绘制的三角形以及深入学习往下的内容

在OpenGL中,任何事物都在3D空间中,而屏幕和窗口却是2D像素数组,这导致OpenGL的大部分工作都是关于把3D坐标转变为适应你屏幕的2D像素。3D坐标转为2D坐标的处理过程是由OpenGL的图形渲染管线(Graphics Pipeline,大多译为管线,实际上指的是一堆原始图形数据途经一个输送管道,期间经过各种变化处理最终出现在屏幕的过程)管理的。图形渲染管线可以被划分为两个主要部分:第一部分把你的3D坐标转换为2D坐标,第二部分是把2D坐标转变为实际的有颜色的像素。这个教程里,我们会简单地讨论一下图形渲染管线,以及如何利用它创建一些漂亮的像素。

图形渲染管线接受一组3D坐标,然后把它们转变为你屏幕上的有色2D像素输出。图形渲染管线可以被划分为几个阶段,每个阶段将会把前一个阶段的输出作为输入。所有这些阶段都是高度专门化的(它们都有一个特定的函数),并且很容易并行执行。正是由于它们具有并行执行的特性,当今大多数显卡都有成千上万的小处理核心,它们在GPU上为每一个(渲染管线)阶段运行各自的小程序,从而在图形渲染管线中快速处理你的数据。这些小程序叫做着色器(Shader)。
下图是一个非常简化框图流水线的各个阶段,并在他们之间传播的数据。


1

(1) 顶点变换:
在这里,一个顶点的属性,如在空间的位置,以及它的颜色,法线,纹理坐标,其中包括一组。这个阶段的输入的各个顶点的属性。由固定的功能所执行的操作,主要完成一下工作:
  1. 顶点位置变换
  2. 计算顶点观照
  3. 纹理坐标变换
(2) 图元装配:
这个阶段的输入的变换后的顶点,以及连通性信息。这后者的一块数据告诉顶点如何连接,以形成一种原始的绘制数据,这个阶段还负责对视锥裁剪操作,背面剔除。光栅扫描确定的片段,和原始的像素位置。

在此上下文中的片段是一块的数据,将用于更新的像素在帧缓冲区中,在特定的位置。片段包含不仅是颜色,也法线和纹理坐标,其中包括可能的属性,被用来计算新像素的颜色。上面的顶点变换阶段,计算出的值与顶点连接信息相结合,允许当前阶段来计算相应的属性的片段。比如,每个顶点的变换位置。当考虑到顶点,使一个原语,是可以计算的原始片段的位置。另一个例子是,使用的颜色。如果一个三角形,然后用不同的颜色有其顶点的颜色的三角形内的片段片段的相对的顶点的距离加权的三角形的顶点的颜色插值的方式获得。
(3)纹理映射、着色:
插值片段信息是这一阶段的输入。彩色已经在前一阶段的计算是通过内插法,在这里,它可以结合例如一个纹理像素(纹理元素)。
纹理坐标也已经在前一阶段内插。雾也适用于在这个阶段。每个片段的本阶段的共同的最终结果是一个颜色值和深度的片段。

(4)最后阶段的管道上的片段,进行了一系列的测试:
1. 裁减测试
2. Alpha(透明度)测试
3. 模板测试
4. 深度测试
通过测试的片段信息,然后用于更新的像素的值,根据当前的混合模式。(请注意:混合只发生在这个阶段,因为片段纹理和着色阶段,有没有访问帧缓冲区。帧缓冲区是唯一能够在这个阶段。)
详细可参考:http://blog.csdn.net/kesalin/article/details/8223649 介绍的渲染管线和着色器

现在大家都已经大概了解了渲染管线和着色器,接下来我们来做实际的代码操作。
根据《OpenGL ES 创建窗口》的代码,按照创建窗口的步骤,完成前面三个步骤

一、配置OpenGL ES渲染的上下文EAGLContext(context)
二、配置渲染的图层layer(rendering layer)
三、配置渲染缓冲区和帧缓冲区(renderBuffer and frameBuffer )
四、着色器着色(user vertextShader and framentShader)

OpenGL ES 创建窗口》代码的基础上进行编码。在前面提到可编程管线通过用 shader 语言编写脚本文件实现的,这些脚本文件相当于 C 源码,有源码就需要编译链接,因此需要对应的编译器与链接器,shader 对象与 program 对象就相当于编译器与链接器。shader 对象载入源码,然后编译成 object 形式(就像C源码编译成 .obj文件)。经过编译的 shader 就可以装配到 program 对象中,每个 program对象必须装配两个 shader 对象:一个顶点 shader,一个片元 shader,然后 program 对象被连接成“可执行文件”,这样就可以在 render 中是由该“可执行文件”了。

2

3

完成图2,3配置以后,开始创建顶点着色器和片段着色器,并创建着色器程序链接顶点和片段着色器

4
1、首先,我们向工程中添加新的类 OpenGLESUtils,让它继承自 NSObject

OpenGLESUtils.h文件内容为下图


5

OpenGLESUtils.m文件内容为:

//
//  OpenGLESUtils.m
//  OpenGL_ Triangle
//
//  Created by Mr_zhang on 17/4/7.
//  Copyright © 2017年 Mr_zhang. All rights reserved.
//

#import "OpenGLESUtils.h"

@implementation OpenGLESUtils
+ (GLuint)loadShaderProgram:(GLenum)type withFilepath:(NSString *)shaderFilepath
{
    NSError *error;
    NSString *shaderString = [NSString stringWithContentsOfFile:shaderFilepath encoding:NSUTF8StringEncoding error:&error];
    if (!shaderString)
    {
        NSLog(@"Error: loading shader file:%@  %@",shaderFilepath,error.localizedDescription);
        return 0;
    }
    return [self loadShader:type withString:shaderString];
}

+ (GLuint)loadShader:(GLenum)type withString:(NSString *)shaderString
{
    GLuint shader = glCreateShader(type);
    if (shader == 0)
    {
        NSLog(@"Error: failed to create shader.");
        return 0;
    }
    // Load the shader soure (加载着色器源码)
    const char *shaderStringUTF8 = [shaderString UTF8String];
    // 要编译的着色器对象作为第一个参数,第二个参数指定了传递的源码字符串数量,第三个着色器是顶点的真正的源码,第四个设置为NULL;
    glShaderSource(shader, 1, &shaderStringUTF8, NULL);
    // 编译着色器
    glCompileShader(shader);
    
    // 检查编译是否成功
    GLint success;
    GLchar infoLog[512];
    glGetShaderiv(shader, GL_COMPILE_STATUS, &success);
    if (!success)
    {
        GLint infolen;
        glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &infolen);
        
        if (infolen > 1)
        {
            char *infolog = malloc(sizeof(char) * infolen);
            glGetShaderInfoLog(shader, infolen, NULL, infoLog);
            NSLog(@"compile faile,error:%s",infoLog);
            free(infolog);
        }
       
        glDeleteShader(shader);
        return 0;
    }
    return shader;
}

/**
 创建顶点着色器和片段着色器

 @param vertexShaderFilepath 顶点着色器路径
 @param fragmentShaderFilepath 片段着色器路径
 @return 链接成功后的着色器程序
 */
+ (GLuint)loadProgram:(NSString *)vertexShaderFilepath withFragmentShaderFilepath:(NSString *)fragmentShaderFilepath
{
    // Create vertexShader (创建顶点着色器)
    GLuint vertexShader = [self loadShaderProgram:GL_VERTEX_SHADER withFilepath:vertexShaderFilepath];
    if (vertexShader == 0)
        return 0;
    
    // Create fragmentShader (创建片段着色器)
    GLuint fragmentShader = [self loadShaderProgram:GL_FRAGMENT_SHADER withFilepath:fragmentShaderFilepath];
    if (fragmentShader == 0)
    {
        glDeleteShader(vertexShader);
        return 0;
    }
    
    // Create the program object (创建着色器程序)
    GLuint shaderProgram = glCreateProgram();
    if (shaderProgram == 0)
        return 0;
    glAttachShader(shaderProgram, vertexShader);
    glAttachShader(shaderProgram, fragmentShader);
    // Link the program (链接着色器程序)
    glLinkProgram(shaderProgram);
    
    // Check the link status (检查是否链接成功)
    GLint linked;
    GLchar infoLog[512];
    glGetProgramiv(shaderProgram, GL_LINK_STATUS, &linked);
    if (!linked)
    {
        glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);
        glDeleteProgram(shaderProgram);
        NSLog(@"Link shaderProgram failed");
        return 0;
    }

    
    // Free up no longer needed shader resources (释放不再需要的着色器资源)
    glDeleteShader(vertexShader);
    glDeleteShader(fragmentShader);
    
    return shaderProgram;
}
@end

工具类 GLESUtils 中有两个类方法用来跟进 shader 脚本字符串或 shader 脚本文件创建 shader,然后装载它,编译它。下面详细介绍每个步骤。
1)创建/删除 shader
函数 glCreateShader 用来创建 shader,参数 GLenum type 表示我们要处理的 shader 类型,它可以是 GL_VERTEX_SHADER 或 GL_FRAGMENT_SHADER,分别表示顶点 shader 或 片元 shader。它返回一个句柄指向创建好的 shader 对象。
函数 glDeleteShader 用来销毁 shader,参数为 glCreateShader 返回的 shader 对象句柄。

2)装载 shader
函数 glShaderSource 用来给指定 shader 提供 shader 源码。第一个参数是 shader 对象的句柄;第二个参数表示 shader 源码字符串的个数;第三个参数是 shader 源码字符串数组;第四个参数一个 int 数组,表示每个源码字符串应该取用的长度,如果该参数为 NULL,表示假定源码字符串是 \0 结尾的,读取该字符串的内容指定 \0 为止作为源码,如果该参数不是 NULL,则读取每个源码字符串中前 length(与每个字符串对应的 length)长度个字符作为源码。

3)编译 shader
函数glCompileShader 用来编译指定的shader对象,这将编译存储在 shader 对象中的源码。我们可以通过函数glGetShaderiv来查询shader 对象的信息,如本例中查询编译情况,此外还可以查询 GL_DELETE_STATUS,GL_INFO_LOG_STATUSGL_SHADER_SOURCE_LENGTHGL_SHADER_TYPE。在这里我们查询编译情况,如果返回 0,表示编译出错了,错误信息会写入 info 日志中,我们可以查询该 info 日志,从而获得错误信息。

2、编写着色器脚本

OpenGLESUtils提供的接口让我们可以使用两种方式:脚本字符串或脚本文件来提供 shader 源码,通常使用脚本文件方式有更大的灵活性。(Cocos2D 源码中倒是提供了不少脚本字符串应对一些常见的情况,有兴趣的同学可以查看下)。在这里,我们使用脚本文件方式。按照以下两个步骤创建,创建顶点着色器后缀使用.vsh(即vertextShader),片段着色器后缀使用.fsh(即framentShader)。

6
7
1)添加顶点着色器脚本vertextShader.vsh

vertextShader顶点着色器脚本内容:

attribute vec4 vPosition; 

void main(void)
{
    gl_Position = vPosition;
}

顶点着色脚本的源码很简单,如果你仔细阅读了前面的介绍,就一目了然。 attribute 属性 vPosition 表示从应用程序输入的类型为 vec4 的位置信息,输出内建 vary 变量 vPosition。留意:这里使用了默认的精度。

2) 添加片段着色器脚本fragmentShader.fsh

fragmentShader片段着色器脚本内容:

precision mediump float;

void main()
{
    gl_FragColor = vec4(1.0, 1.0, 0.0, 1.0);
}

片元着色脚本源码也很简单,前面说过片元着色要么自己定义默认精度,要么在每个变量前添加精度描述符,在这里自定义 float 的精度为 mediump。然后为内建输出变量 gl_FragColor 指定为黄色

3、创建着色器程序 program,装配 shader,链接 program,使用 program
8

有了前面的介绍,上面的代码很容易理解。首先我们是由 GLESUtils 提供的辅助方法从前面创建的脚本中创建,装载和编译顶点 shader 和片元 shader;然后我们创建 program,将顶点 shader 和片元 shader 装配到 program 对象中,再使用 glLinkProgram 将装配的 shader 链接起来,这样两个 shader 就可以合作干活了。注意:链接过程会对 shader 进行可链接性检查,也就是前面说到同名变量必须同名同型以及变量个数不能超出范围等检查。我们如何检查 shader 编译情况一样,对 program 的链接情况进行检查。如果一切正确,那我们就可以调用 glUseProgram 激活 program 对象从而在 render 中使用它。通过调用 glGetAttribLocation 我们获取到 shader 中定义的变量 vPosition 在 program 的槽位,通过该槽位我们就可以对 vPosition 进行操作。

五、渲染
9
六、编译运行

编译运行,将看到一个红色的三角形显示在屏幕中央。知道为什么是黄色的么?那是因为 program 也链接了片元着色器,在片元着色脚本文件中,我们指定 gl_FragColor 的值为黄色 vec4(1.0, 1.0, 0.0, 1.0)。

10

总结

经过《OpenGL ES 创建窗口 》《OpenGL ES零基础入门----(2)绘制三角形》两章节的学习,我们大概了解了openGL的基本使用,包括设置 CAEAGLLayer 属性,创建 EAGLContext,创建和使用 renderbuffer 和 framebuffer,了解OpenGL ES 渲染管线,创建和使用 shader,创建和实现 program,使用顶点数组进行描绘。流程已经走通,接下来让我们进入 OpenGL ES 各个具体的技术领域。
本文源码可以在这里获得:https://github.com/476455183/OpenGLES

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

推荐阅读更多精彩内容