http://blog.csdn.net/kesalin/article/details/8223649

罗朝辉 (http://blog.csdn.net/kesalin)

本文遵循“署名-非商业用途-保持一致”创作公用协议

前言

在前文《[OpenGL ES 01]OpenGL ES之初体验》中我们学习了如何在 iOS 平台上设置OpenGL ES 环境,主要是设置 CAEAGLLayer 属性,创建 EAGLContext,创建和使用 renderbuffer 和 framebuffer,并知道如何清屏。但实际上并没有真正描绘点什么。在本文中,我们将学习OpenGL ES 渲染管线,顶点着色器和片元着色器相关知识,然后使用可编程管线在屏幕上描绘一个简单三角形。

一,渲染管线

在 OpenGL ES 1.0 版本中,支持固定管线,而 OpenGL ES 2.0 版本不再支持固定管线,只支持可编程管线。什么是管线?什么又是固定管线和可编程管线?管线(pipeline)也称渲染管线,因为 OpenGL ES在渲染处理过程中会顺序执行一系列操作,这一系列相关的处理阶段就被称为OpenGL ES 渲染管线。pipeline 来源于福特汽车生产车间的流水线作业,在OpenGL ES 渲染过程中也是一样,一个操作接着一个操作进行,就如流水线作业一样,这样的实现极大地提供了渲染的效率。整个渲染管线如下图所示:

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

再回到上图,这张图就是 OpenGL ES 的“架构图”,学习OpenGL ES 就是学习这张图中的每一个部分,在这里先粗略地介绍一下。

Vertex Array/Buffer objects:顶点数据来源,这时渲染管线的顶点输入,通常使用 Buffer objects效率更好。在今天的示例中,简单起见,使用的是 Vertex Array;

Vertex Shader:顶点着色器通过可编程的方式实现对顶点的操作,如进行坐标空间转换,计算 per-vertex color以及纹理坐标;

Primitive Assembly:图元装配,经过着色器处理之后的顶点在图片装配阶段被装配为基本图元。OpenGL ES 支持三种基本图元:点,线和三角形,它们是可被 OpenGL ES 渲染的。接着对装配好的图元进行裁剪(clip):保留完全在视锥体中的图元,丢弃完全不在视锥体中的图元,对一半在一半不在的图元进行裁剪;接着再对在视锥体中的图元进行剔除处理cull):这个过程可编码来决定是剔除正面,背面还是全部剔除。

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

Fragment Shader:片元着色器通过可编程的方式实现对片元的操作。在这一阶段它接受光栅化处理之后的fragment,color,深度值,模版值作为输入。

Per-Fragment Operation:在这一阶段对片元着色器输出的每一个片元进行一系列测试与处理,从而决定最终用于渲染的像素。这一系列处理过程如下:

Pixel ownership test:该测试决定像素在 framebuffer 中的位置是不是为当前 OpenGL ES 所有。也就是说测试某个像素是否对用户可见或者被重叠窗口所阻挡;

Scissor Test:剪裁测试,判断像素是否在由 glScissor 定义的剪裁矩形内,不在该剪裁区域内的像素就会被剪裁掉;

Stencil Test:模版测试,将模版缓存中的值与一个参考值进行比较,从而进行相应的处理;

Depth Test:深度测试,比较下一个片段与帧缓冲区中的片段的深度,从而决定哪一个像素在前面,哪一个像素被遮挡;

Blending:混合,混合是将片段的颜色和帧缓冲区中已有的颜色值进行混合,并将混合所得的新值写入帧缓冲;

Dithering:抖动,抖动是使用有限的色彩让你看到比实际图象更多色彩的显示方式,以缓解表示颜色的值的精度不够大而导致的颜色剧变的问题。

Framebuffer:这是流水线的最后一个阶段,Framebuffer 中存储这可以用于渲染到屏幕或纹理中的像素值,也可以从Framebuffer 中读回像素值,但不能读取其他值(如深度值,模版值等)。

二,顶点着色器

下面来仔细看看顶点着色器:

顶点着色器接收的输入:

Attributes:由 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 可用于顶点着色器和片元着色器。

Shader program:由 main 申明的一段程序源码,描述在顶点上执行的操作:如坐标变换,计算光照公式来产生 per-vertex 颜色或计算纹理坐标。

顶点着色器的输出:

Varying:varying 变量用于存储顶点着色器的输出数据,当然也存储片元着色器的输入数据,varying 变量最终会在光栅化处理阶段被线性插值。顶点着色器如果声明了 varying 变量,它必须被传递到片元着色器中才能进一步传递到下一阶段,因此顶点着色器中声明的 varying 变量都应在片元着色器中重新声明同名同类型的 varying 变量。OpenGL ES 2.0 也规定了所有实现应该支持的最大 varying 变量个数不能少于 8 个。

在顶点着色器阶段至少应输出位置信息-即内建变量:gl_Position,其它两个可选的变量为:gl_FrontFacing 和 gl_PointSize。

三,片元着色器

接下来仔细看看片元着色器:

片元管理器接受如下输入:

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。

四,顶点着色与片元着色在编程上的差异

1,精度上的差异

着色语言定了三种级别的精度:lowp, mediump, highp。我们可以在 glsl 脚本文件的开头定义默认的精度。如下代码定义在 float 类型默认使用 highp 级别的精度

precision highp float;

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

2,attribute 修饰符只可用于顶点着色。这个前面已经说过了。

3,或由于精度的不同,或因为编译优化的原因,在顶点着色和片元着色阶段同样的计算可能会得到不同的结果,这会导致一些问题(z-fighting)。因此 glsl 引入了 invariant 修饰符来修饰在两个着色阶段的同一变量,确保同样的计算会得到相同的值。

五,使用顶点着色器与片元着色器

好了,理论知识讲得足够多了,下面我们来看看如何在代码中添加顶点着色器与片元着色器。我们在前一篇文章《[OpenGL ES 01]OpenGL ES之初体验》代码的基础上进行编码。在前面提到可编程管线通过用 shader 语言编写脚本文件实现的,这些脚本文件相当于 C 源码,有源码就需要编译链接,因此需要对应的编译器与链接器,shader 对象与 program 对象就相当于编译器与链接器。shader 对象载入源码,然后编译成 object 形式(就像C源码编译成 .obj文件)。经过编译的 shader 就可以装配到 program 对象中,每个 program对象必须装配两个 shader 对象:一个顶点 shader,一个片元 shader,然后 program 对象被连接成“可执行文件”,这样就可以在 render 中是由该“可执行文件”了。

1,创建,装载和编译 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:NSUTF8StringEncodingerror:&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
{   // Create the shader objectGLuint shader = glCreateShader(type);if (shader == 0) {NSLog(@"Error: failed to create shader.");return 0;}// Load the shader sourceconst char * shaderStringUTF8 = [shaderString UTF8String];glShaderSource(shader, 1, &shaderStringUTF8, NULL);// Compile the shaderglCompileShader(shader);// Check the compile statusGLint 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,然后装载它,编译它。下面详细介绍每个步骤。

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_STATUS,GL_SHADER_SOURCE_LENGTH 和 GL_SHADER_TYPE。在这里我们查询编译情况,如果返回 0,表示编译出错了,错误信息会写入 info 日志中,我们可以查询该 info 日志,从而获得错误信息。

2,编写着色脚本

GLESUtils 提供的接口让我们可以使用两种方式:脚本字符串或脚本文件来提供 shader 源码,通常使用脚本文件方式有更大的灵活性。(Cocos2D 源码中倒是提供了不少脚本字符串应对一些常见的情况,有兴趣的同学可以查看下)。在这里,我们使用脚本文件方式。

1),添加顶点着色脚本

右击 Supporting Files 目录,New File->Other->Empty,输入名称:VertexShader.glsl,去除 target Tutorial02 中的勾选。后缀glsl 表示 GL Shader Language。

编辑其内容如下:

attribute vec4 vPosition; void main(void)
{gl_Position = vPosition;
}

然后选择 Tutorial02,在 Build Phases -> Copy Bundle Sources 中添加 VertexShader.glsl。

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

2),添加片元着色脚本

用于添加顶点着色脚本同样的方式添加名为 FragmentShader.glsl 的文件,编辑其内容如下:

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

不用忘记在 Build Phases -> Copy Bundle Sources 中添加 FragmentShader.glsl。

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

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

1),创建 program

在 OpenGLView.h 的 OpenGLView 类声明中添加两个成员:

    GLuint _programHandle;GLuint _positionSlot;

然后依然在 OpenGLView.m 中的匿名 category 中添加成员方法:

- (void)setupProgram;

在 - (void)render 方法前,添加其实现:

- (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_SHADERwithFilepath:vertexShaderPath]; GLuint fragmentShader = [GLESUtils loadShader:GL_FRAGMENT_SHADERwithFilepath: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 statusGLint 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 进行操作。

4,使用示例

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

    [self setupProgram];[self render];

然后改写 render 方法:

- (void)render
{glClearColor(0, 1.0, 0, 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 将三角形图元渲染出来。

5,编译运行

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

六,总结

在前文《[OpenGL ES 01]OpenGL ES之初体验》和本文中,我们详细了解了如何在 iPhone 中使用 OpenGL ES 的整个过程,包括设置 CAEAGLLayer 属性,创建 EAGLContext,创建和使用 renderbuffer 和 framebuffer,了解OpenGL ES 渲染管线,创建和使用 shader,创建和实现 program,使用顶点数组进行描绘。流程已经走通,接下来让我们进入 OpenGL ES 各个具体的技术领域。

本文源码可以在这里获得:https://github.com/kesalin/OpenGLES/tree/master/Tutorial02

七,Refference

OpenGL ES 2.0 Programming Guide

OpenGL ES Programming Guide for iOS

[OpenGL ES 02]OpenGL ES渲染管线与着色器相关推荐

  1. OpenGL ES渲染管线与着色器

    转自:http://blog.csdn.net/kesalin/article/details/8223649 [OpenGL ES 02]OpenGL ES渲染管线与着色器 罗朝辉 (http:// ...

  2. OpenGL程序管道,可分离程序和着色器子例程的基本用法

    OpenGL程序管道,可分离程序和着色器子例程的基本用法 先上图,再解答. 完整主要的源代码 源代码剖析 先上图,再解答. 完整主要的源代码 #include <stdio.h> #inc ...

  3. OpenGL中的曲面细分和几何着色器

    [摘要]本文我们先介绍OpenGL中的曲面细分的一些基本概念,然后给两个例子说明不得不用这项技术的理由. 曲面细分是OpenGL 4.0之后才定义的功能,使用之前请确认你的显卡驱动支持OpenGL4. ...

  4. 可编程渲染管线与着色器语言

    Programming pipeline & shading language 大家好,今天想给大家介绍一下可编程渲染管线和着色器语言的相关基础知识,使想上手SHADER编程的童鞋们可以快速揭 ...

  5. OpenGL渲染管线,着色器,光栅化等概念理解

    卧槽,前些日子看这几个概念就十分想吐槽,这么难理解的概念窃以为纯属翻译的不够接地气. ---- 首先,光栅化(Rasterize/rasteriztion). 这个词儿Adobe官方翻译成栅格化或者像 ...

  6. 【我的OpenGL学习进阶之旅】解决着色器编译错误:#version directive must occur on the first line of the shader

    目录 一.问题描述 二.分析错误 三.解决问题 三.总结 一.问题描述 今天编写一个OpenGL ES的demo,发现没有任何图元输出. 查看日志,发现报了如下错误: 2021-11-15 15:09 ...

  7. Learn OpenGL(五)——定义自己的着色器

    定义自己的着色器 编写.编译.管理着色器是件麻烦事.在着色器的最后主题里,我们会写一个类来让我们的生活轻松一点,这个类从硬盘读着色器,然后编译和链接它们,对它们进行错误检测,这就变得很好用了.这也会给 ...

  8. OpenGL学习笔记(十)-几何着色器-实例化

    参考网址:LearnOpenGL 中文版 4.7 几何着色器 4.7.1 基本概念 1.顶点和片段着色器之间有一个可选的几何着色器,几何着色器的输入是一个图元(如点或三角形)的一组顶点,顶点发送到下一 ...

  9. 【OpenGL】笔记二十七、几何着色器

    1. 流程 在顶点和片段着色器之间有一个可选的几何着色器(Geometry Shader),几何着色器的输入是一个图元(如点或三角形)的一组顶点.几何着色器可以在顶点发送到下一着色器阶段之前对它们随意 ...

最新文章

  1. Latex中设置字体颜色
  2. 神经网络架构搜索(NAS)综述 | 附AutoML资料推荐
  3. 30.jvm.gc(GC之详解CMS收集过程和日志分析)
  4. workbook加载文件路径_通过Workbook.XML 修复Excel自定义名称
  5. python代码比例_Python如何输出百分比
  6. 28.课时28.【Django模块】with标签使用详解(Av61533158,P28)
  7. Ruby中的Profiling工具
  8. 大话数据结构第一章理解
  9. CSS轮廓 边距 填充 分组和嵌套
  10. Oracle BRM处理逻辑
  11. 数据之路 - Python爬虫 - PyQuery库
  12. 设置jupyter notebook软件的字体样式
  13. Vue 动态加载子组件
  14. Python全栈:Django模板
  15. Oracle创建数据链路
  16. ps保存图片时为了可以发送到微信中(微信大于25M的图片不能发送) 应该这样保存图片!!!...
  17. 中标麒麟龙芯桌面版重置root密码
  18. python怎样创建列表_如何创建Python列表(list)和添加元素
  19. 西门子水处理1200PLC程序+触摸屏程序
  20. XML语法以及DTD的详解

热门文章

  1. springBoot+mybaits+达梦数据库
  2. java innodb存储引擎_InnoDB存储引擎简介
  3. java 面试700问_JAVA面试700问(一) | 并发编程网
  4. ideal如何快速导入import_【MAC版】pr预设安装目录?pr如何快速批量导入lut
  5. LoRa、LoRaWAN及网关相关技术介绍
  6. 无限极评论怎么删除php,TP5 无限极评论回复
  7. 三次握手和四次挥手图解_详解 TCP 连接的“三次握手”与“四次挥手”
  8. 软件测试常见笔试面试题(一)
  9. 浏览器皮肤_和平精英返场皮肤投票时间是什么时候?投票地址入口介绍-手游资讯...
  10. android 加载大长图,android加载长图片的方法