OpenGL ES渲染管线(渲染流程)

渲染管线

苹果提供了两种OpenGL ES的可视化模型,一种是客户端—服务端的架构模型,另一种就是管线的模型。

OpenGL ES client-server architecture

客户端—服务端架构:应用程序状态一旦更改,纹理、顶点数据以及渲染命令都将传递给OpenGL ES客户端。客户端将这些数据转换成图形硬件可以理解的格式,发送给GPU。这个过程增加了app图形性能的开销。为了获得好的性能需要管理好这种开销。一个好的设计需要减少OpenGL ES调用的频率,使用适合硬件的数据格式来减少转换成本,并且管理好自身与OpenGL ES的数据流。
管线架构:另外就是本文将要说的管线架构,由于管线中每个独立阶段都依赖于上一阶段的产出,所以任何阶段的工作量太大或者运行太慢,其他阶段都会被迫闲置以等待前一阶段的完成。好的设计会根据硬件功能平衡每个阶段的执行工作。
OpenGL ES graphics pipeline

在 OpenGL ES 1.0 版本中,支持固定管线(fixed-function pipeline),有一系列固定的函数用来在屏幕上渲染对象,而不是创建一个单独的程序来指导 GPU 的行为。这样有很大的局限性,你不能做出任何特殊的效果。如果想知道着色器在工程中可以造成怎样的不同,看看这篇 Brad Larson 写的他用着色器替代固定函数重构 Molecules 应用的博客
而 OpenGL ES 2.0 版本不再支持固定管线,只支持可编程管线。

什么是管线?什么又是固定管线和可编程管线?

管线(pipeline):也称渲染管线,因为 OpenGL ES在渲染处理过程中会顺序执行一系列操作,这一系列相关的处理阶段就被称为OpenGL ES 渲染管线。OpenGL ES 渲染过程就如流水线作业一样,这样的实现极大地提高了渲染的效率。如图就是 OpenGL ES 的管线图,学习OpenGL ES 就是学习这张图中的每一个部分。

图中阴影部分的 Vertex Shader 和 Fragment Shader 是可编程管线。可编程管线就是说这个操作可以动态编程而不必写死在代码中。可动态编程实现这一功能一般都是脚本提供的,在OpenGL ES 中也一样,编写这样脚本的能力是由着色语言GLSL提供的。那可编程管线有什么好处呢?方便我们动态修改渲染过程,而无需重写编译代码,当然也和很多脚本语言一样,调试起来不太方便。

渲染管线中的各个模块

Vertex Shader

由图可见,顶点着色器分输入输出两部分。顶点着色器定义了在 2D 或者 3D 场景中几何图形是如何处理的,实现了顶点操作的通用可编程方法。一个顶点指的是 2D 或者 3D 空间中的一个点。在图像处理中,有 4 个顶点:每一个顶点代表图像的一个角。顶点着色器设置顶点的位置,并且把位置和纹理坐标这样的参数发送到片段着色器。
输入:

  • 着色器程序(Shader Program,图中没有画出):由 main 申明的一段程序源码或可执行文件,描述在顶点上执行的操作:如坐标变换、计算光照公式产生每个顶点颜色、计算纹理坐标。
  • 属性(Attribute):由 vertext array 提供的顶点数据,如空间位置,法向量,纹理坐标以及顶点颜色,属性可以理解为针对每一个顶点的输入数据。属性只在顶点着色器中才有,片元着色器中没有属性。OpenGL ES 2.0 规定了所有实现应该支持的最大属性个数不能少于 8 个。
  • 常量(Uniforms): Uniforms保存由应用程序传递给着色器的只读常量数据。在顶点着色器中,这些数据通常是变换矩阵,光照参数,颜色等。由 uniform 修饰符修饰的变量属于全局变量,该全局性对顶点着色器与片元着色器均可见,也就是说,这两个着色器如果被连接到同一个应用程序中,它们共享同一份 uniform 全局变量集。因此如果在这两个着色器中都声明了同名的 uniform 变量,要保证这对同名变量完全相同:同名+同类型,因为它们实际是同一个变量。此外,uniform 变量存储在常量存储区,因此限制了 uniform 变量的个数,OpenGL ES 2.0 也规定了所有实现应该支持的最大顶点着色器 uniform 变量个数不能少于 128 个,最大的片元着色器 uniform 变量个数不能少于 16 个。
  • 采样器(Samplers): 一种特殊的 uniform,用于呈现纹理。sampler 可用于顶点着色器和片元着色器。
    输出:
  • 可变变量(Varying):varying 变量用于存储顶点着色器的输出数据,也存储片元着色器的输入数据。varying 变量会在光栅化处理阶段被线性插值。顶点着色器如果声明了 varying 变量,它必须被传递到片元着色器中才能进一步传递到下一阶段,因此顶点着色器中声明的 varying 变量都应在片元着色器中重新声明为同名同类型的 varying 变量。OpenGL ES 2.0 也规定了所有实现应该支持的最大 varying 变量个数不能少于 8 个。
  • 在顶点着色器阶段至少应输出位置信息-即内建变量:gl_Position,其它两个可选的变量为:gl_FrontFacing 和 gl_PointSize。

Primitive Assembly图元装配(还有啥好的翻译?)

图元(Primitive):OpenGL ES 支持三种基本图元:点,线和三角形,它们是可被 OpenGL ES 渲染的。经过着色器处理之后的顶点在这一阶段被装配为基本图元。对于每个图元,必须确定图元是否位于视锥体(屏幕上可见的3D空间区域)内,保留完全在视锥体中的图元,丢弃完全不在视锥体中的图元,对一半在一半不在的图元进行裁剪。裁剪之后,顶点位置就被转换成了屏幕坐标。也可以再对在视锥体中的图元进行剔除(cull):这个过程可编码来决定是剔除正面,背面还是全部剔除。裁剪和剔除之后,图元便准备传递给管线的下一个阶段——光栅化阶段。

Rasterization

在光栅化阶段,基本图元被转换为一组二维的片元(fragment),fragment 表示可以被渲染到屏幕上的像素,它包含位置,颜色,纹理坐标等信息,这些值是由图元的顶点信息进行插值计算得到的。这些片元接着被送到片元着色器中处理。这是从顶点数据到可渲染在显示设备上的像素的质变过程。

Fragment Shader

GPU 使用片元着色器在对象或者图片的每一个像素上进行计算,处理由光栅化阶段生成的每个片元,最终计算出每个像素的最终颜色。图片,归根结底,实际上仅仅是数据的集合。图片的文档包含每一个像素的各个颜色分量和像素透明度的值。因为对每一个像素,算式是相同的,GPU 可以流水线作业这个过程,从而更加有效的进行处理。使用正确优化过的着色器,在 GPU 上进行处理,将获得百倍于在 CPU 上用同样的过程进行图像处理的效率。

  • 可变变量(Varyings):这个在前面已经讲过了,顶点着色器阶段输出的 varying 变量在光栅化阶段被线性插值计算之后输出到片元着色器中作为它的输入,即上图中的 gl_FragCoord,gl_FrontFacing 和 gl_PointCoord。OpenGL ES 2.0 也规定了所有实现应该支持的最大 varying 变量个数不能少于 8 个。
  • 常量(Uniforms):前面也已经讲过,这里是用于片元着色器的常量,如雾化参数,纹理参数等;OpenGL ES 2.0 也规定了所有实现应该支持的最大的片元着色器 uniform 变量个数不能少于 16 个。
  • 采样器(Samples):一种特殊的 uniform,用于呈现纹理。
  • 着色器程序(Shader program):由 main 申明的一段程序源码,描述在片元上执行的操作。
  • 在顶点着色器阶段只有唯一的 varying 输出变量-即内建变量:gl_FragColor。

顶点着色器与片元着色器的编程区别

  • 精度上的差异
    着色语言定了三种级别的精度:lowp, mediump, highp。我们可以在 glsl 脚本文件的开头定义默认的精度。

    precision highp float;
    
    • 在顶点着色阶段,如果没有自定义默认精度,那么 int 和 float 都默认为 highp 级别;
    • 在片元着色阶段,如果没有自定义默认精度,就真的没有默认精度了.我们必须在每个变量前放置精度描述符。此外,OpenGL ES 2.0 标准也没有强制要求所有实现在片元阶段都支持 highp 精度。我们可以通过查看是否定义 GL_FRAGMENT_PRECISION_HIGH 来判断具体实现是否在片元着色器阶段支持 highp 精度,从而编写出可移植的代码。通常不需要在片元着色器阶段使用 highp 级别的精度,推荐先使用 mediump 级别的精度,只有在效果不够好的情况下再考虑 highp 精度。
  • attribute 修饰符只可用于顶点着色。

使用顶点着色器和片元着色器

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

  • 创建,装载和编译 shader

    • 向工程中添加新的类 GLESUtils,让它继承自 NSObject。GLESUtils.h 为
    #import <Foundation/Foundation.h>
    #include <OpenGLES/ES2/gl.h>
    
    @interface GLESUtils : NSObject
    
    // Create a shader object, load the shader source string, and compile the shader.
    //
    +(GLuint)loadShader:(GLenum)type withString:(NSString *)shaderString;
    
    +(GLuint)loadShader:(GLenum)type withFilepath:(NSString *)shaderFilepath;
    
    @end
    
    • GLESUtils.m
    #import "GLESUtils.h"
    
    @implementation GLESUtils
    
    +(GLuint)loadShader:(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 {   
        //创建shader
        GLuint shader = glCreateShader(type);
        if (shader == 0) {
            NSLog(@"Error: failed to create shader.");
            return 0;
        }
    
        //装载shader
        const char * shaderStringUTF8 = [shaderString UTF8String];
        glShaderSource(shader, 1, &shaderStringUTF8, NULL);
    
        //编译shader
        glCompileShader(shader);
    
        //查询变异状态
        GLint compiled = 0;
        glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
    
        if (!compiled) {
            GLint infoLen = 0;
            glGetShaderiv ( shader, GL_INFO_LOG_LENGTH, &infoLen );
        
            if (infoLen > 1) {
                char * infoLog = malloc(sizeof(char) * infoLen);
                glGetShaderInfoLog (shader, infoLen, NULL, infoLog);
                NSLog(@"Error compiling shader:\n%s\n", infoLog );            
            
                free(infoLog);
            }
        
            glDeleteShader(shader);
            return 0;
        }
    
        return shader;
    }
    
    @end
    

    辅助类 GLESUtils 中有两个类方法用来跟进 shader 脚本字符串或 shader 脚本文件创建 shader,然后装载它,编译它。下面详细介绍每个步骤。

    • 创建/删除 shader
      函数 glCreateShader 用来创建 shader,参数 GLenum type 表示我们要处理的 shader 类型,它可以是 GL_VERTEX_SHADER 或 GL_FRAGMENT_SHADER,分别表示顶点 shader 或 片元 shader。它返回一个句柄指向创建好的 shader 对象。
      函数 glDeleteShader 用来销毁 shader,参数为 glCreateShader 返回的 shader 对象句柄。

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

    • 编译 shader
      函数 glCompileShader 用来编译指定的 shader 对象,这将编译存储在 shader 对象中的源码。

    • 查询shader对象信息
      函数 glGetShaderiv 来查询 shader 对象的信息,如本例中查询编译情况。此外还可以查询 GL_DELETE_STATUS,GL_INFO_LOG_STATUS,GL_SHADER_SOURCE_LENGTH 和 GL_SHADER_TYPE。在这里我们查询编译情况,如果返回 0,表示编译出错了,错误信息会写入 info 日志中,我们可以查询该 info 日志,从而获得错误信息。

  • 编写着色脚本
    添加VertexShader.glsl和FragmentShader.glsl文件

    • VertexShader.glsl
    attribute vec4 vPosition; 
    
    void main(void) {
        gl_Position = vPosition;
    }
    

    attribute 属性 vPosition 表示从应用程序输入的类型为 vec4 的位置信息,输出内建 vary 变量 vPosition。注意:这里使用了默认的精度。

    • FragmentShader.glsl
    precision mediump float;
    
    void main() {
        gl_FragColor = vec4(1.0, 1.0, 0.0, 1.0);//RGBA,此处为黄色
    }
    

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

  • 创建 program,装配 shader,链接 program,使用 program

    • OpenGLView.h中添加两个成员
    GLuint _programHandle;
    GLuint _positionSlot;
    
    • 在 OpenGLView.m 中的匿名 category 中添加成员方法:
    - (void)setupProgram {
        // Load shaders
        NSString * vertexShaderPath = [[NSBundle mainBundle] pathForResource:@"VertexShader"
                                                                  ofType:@"glsl"];
        NSString * fragmentShaderPath = [[NSBundle mainBundle] pathForResource:@"FragmentShader"
                                                                    ofType:@"glsl"];
        GLuint vertexShader = [GLESUtils loadShader:GL_VERTEX_SHADER
                                   withFilepath:vertexShaderPath]; 
        GLuint fragmentShader = [GLESUtils loadShader:GL_FRAGMENT_SHADER
                                     withFilepath:fragmentShaderPath];
    
        // Create program, attach shaders.
        _programHandle = glCreateProgram();
        if (!_programHandle) {
            NSLog(@"Failed to create program.");
           return;
        }
    
        glAttachShader(_programHandle, vertexShader);
        glAttachShader(_programHandle, fragmentShader);
    
       // Link program
    
        glLinkProgram(_programHandle);
    
        // Check the link status
        GLint linked;
        glGetProgramiv(_programHandle, GL_LINK_STATUS, &linked );
        if (!linked) {
            GLint infoLen = 0;
            glGetProgramiv (_programHandle, GL_INFO_LOG_LENGTH, &infoLen );
        
            if (infoLen > 1) {
                char * infoLog = malloc(sizeof(char) * infoLen);
                glGetProgramInfoLog (_programHandle, infoLen, NULL, infoLog );
                NSLog(@"Error linking program:\n%s\n", infoLog );            
            
                free (infoLog );
            }
        
            glDeleteProgram(_programHandle);
            _programHandle = 0;
            return;
        }
    
        glUseProgram(_programHandle);
    
        // Get attribute slot from program
        _positionSlot = glGetAttribLocation(_programHandle, "vPosition");
    }  
    

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

  • 使用示例
    在 - (void)layoutSubviews 中调用 render 方法之前,插入对 setupProgram 的调用

    [self setupProgram];
    [self render];
    

render方法:

- (void)render {
    glClearColor(0.5, 1.0, 0.5, 1.0);
    glClear(GL_COLOR_BUFFER_BIT);

    // Setup viewport
    glViewport(0, 0, self.frame.size.width, self.frame.size.height);

    GLfloat vertices[] = {
        0.0f,  0.5f, 0.0f, 
        -0.5f, -0.5f, 0.0f,
        0.5f,  -0.5f, 0.0f };

    // Load the vertex data
    glVertexAttribPointer(_positionSlot, 3, GL_FLOAT, GL_FALSE, 0, vertices );
    glEnableVertexAttribArray(_positionSlot);

    // Draw triangle
    glDrawArrays(GL_TRIANGLES, 0, 3);

    [_context presentRenderbuffer:GL_RENDERBUFFER];
}

在新增的代码中,第一句 glViewport 表示渲染 surface 将在屏幕上的哪个区域呈现出来,然后我们创建一个三角形顶点数组,通过 glVertexAttribPointer 将三角形顶点数据装载到 OpenGL ES 中并与 vPositon 关联起来,最后通过 glDrawArrays 将三角形图元渲染出来。


绘制三角形

代码存放在绘制一个三角形中New Group文件夹

OpenGL ES渲染管线与着色器
OpenGL ES 3.0编程指南
Apple文档——OpenGL ES Design Guidelines
Notes on OpenGL ES Graphics Pipeline
GPU 加速下的图像处理

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

推荐阅读更多精彩内容