在绘制之前,我们需要了解下面的知识:
一、渲染管线
下图中展示整个OpenGL ES 2.0可编程渲染管线
图中Vertex Shader和Fragment Shader 是可编程管线;
1).Vertex Array/Buffer objects
顶点数据来源,这时渲染管线的顶点输入,通常使用 Buffer objects效率更好。
2).Vertex Shader
顶点着色器通过矩阵变换位置、计算照明公式来生成逐顶点颜色已经生成或变换纹理坐标等基于顶点的操作。
3).Primitive Assembly
图元装配经过着色器处理之后的顶点在图片装配阶段被装配为基本图元。OpenGL ES 支持三种基本图元:点,线和三角形,它们是可被 OpenGL ES 渲染的。接着对装配好的图元进行裁剪(clip):保留完全在视锥体中的图元,丢弃完全不在视锥体中的图元,对一半在一半不在的图元进行裁剪;接着再对在视锥体中的图元进行剔除处理(cull):这个过程可编码来决定是剔除正面,背面还是全部剔除。
4).Rasterization
光栅化。在光栅化阶段,基本图元被转换为二维的片元(fragment),fragment 表示可以被渲染到屏幕上的像素,它包含位置,颜色,纹理坐标等信息,这些值是由图元的顶点信息进行插值计算得到的。这些片元接着被送到片元着色器中处理。这是从顶点数据到可渲染在显示设备上的像素的质变过程。
5).Fragment Shader
片元着色器通过可编程的方式实现对每个片元的操作。在这一阶段它接受光栅化处理之后的fragment,color,深度值,模版值作为输入,片元着色器可以抛弃片元,也可以生成一个或多个颜色值作为输出。
6).逐片段操作
1.像素归属测试(Pixel Ownership Test):这一步骤由OpenGL ES内部进行,不由开发人员控制;测试确定帧缓冲区的位置的像素是否归属当前OpenGL ES所有,如不属于或被另一个窗口遮挡,从而完全不显示这些像素。
2.裁剪测试(Scissor Test):判断像素是否在由 glScissor 定义的剪裁矩形内,不在该剪裁区域内的像素就会被剪裁掉;
3.模板和深度测试(Stencil And Depth Test):测试输入片段的模板和深度值上进行,以确定片段是否应该被拒绝;深度测试比较下一个片段与帧缓冲区中的片段的深度,从而决定哪一个像素在前面,哪一个像素被遮挡;
4.混合(Blending):是将片段的颜色和帧缓冲区中已有的颜色值进行混合,并将混合所得的新值写入帧缓冲;
5.抖动(Dithering):可用于最小化因为使用有限精度在帧缓冲区中保存颜色值而产生的伪像。
6.To Framebuffer:这是流水线的最后一个阶段,Framebuffer 中存储这可以用于渲染到屏幕或纹理中的像素值,也可以从Framebuffer 中读回像素值,但不能读取其他值(如深度值,模版值等)。
注:以上渲染管线资料来自http://www.cnblogs.com/edisongz/p/6918428.html
二、顶点着色器 Vertex Shader
下面来仔细看看顶点着色器:
顶点着色器接收的输入:
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。
三、片元着色器 Fragment Shader
接下来仔细看看片元着色器:
片元管理器接受如下输入:
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 修饰符来修饰在两个着色阶段的同一变量,确保同样的计算会得到相同的值。
注:以上关于顶点着色器和片元着色器资料来自 http://www.cnblogs.com/kesalin/archive/2012/11/25/opengl_es_tutorial_02.html
五,使用顶点着色器与片元着色器
好了,理论知识讲得足够多了,下面我们来看看如何在代码中添加顶点着色器与片元着色器。我们在前一篇文章《OpenGLES-01 渲染第一步》代码的基础上进行编码。在前面提到可编程管线通过用 shader 语言编写脚本文件实现的,这些脚本文件相当于 C 源码,有源码就需要编译链接,因此需要对应的编译器与链接器,shader 对象与 program 对象就相当于编译器与链接器。shader 对象载入源码,然后编译成 object 形式(就像C源码编译成 .obj文件)。经过编译的 shader 就可以装配到 program 对象中,每个 program对象必须装配两个 shader 对象:一个顶点 shader,一个片元 shader,然后 program 对象被连接成“可执行文件”,这样就可以在 render 中是由该“可执行文件”了。
1.首先,我们创建顶点着色器脚本文件
然后命名为:VertexShader.glsl ,(glsl:gl shader language)话说这样命名才能有代码提示和校验,然而我没体验到提示和校验。
编辑文件内容如下:
attribute vec4 vPosition;
void main(void)
{
gl_Position = vPosition;
}
顶点着色脚本的源码很简单,如果你仔细阅读了前面的介绍,就一目了然。 attribute 属性 vPosition 表示从应用程序输入的类型为 vec4 的位置信息,输出内建 vary 变量 vPosition。留意:这里使用了默认的精度(highp)。
2.创建片元着色器脚本文件
创建方式如1,命名为FragmentShader.glsl,然后编辑其内容如下:
precision mediump float;
void main()
{
gl_FragColor = vec4(0.0, 1.0, 0.0, 1.0);
}
片元着色脚本源码也很简单,前面说过片元着色要么自己定义默认精度,要么在每个变量前添加精度描述符,在这里自定义 float 的精度为 mediump。然后为内建输出变量 gl_FragColor 指定为绿色。 (故意亮瞎你的眼,请自行修改,此外,提下人对颜色的敏感度,人对绿色的敏感度比红和蓝都要高,所以16位的颜色数据里,红绿蓝占比为5:6:5)。
3.编写工具类GLESUtils文件来使用shader脚本文件
首先创建一个GLESUtils类集成NSObject,修改.h为:
#import <Foundation/Foundation.h>
#include <OpenGLES/ES3/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
然后在.m中添加如下函数:
+(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
{
// Create the shader object
GLuint shader = glCreateShader(type);
if (shader == 0) {
NSLog(@"Error: failed to create shader.");
return 0;
}
// Load the shader source
const char * shaderStringUTF8 = [shaderString UTF8String];
glShaderSource(shader, 1, &shaderStringUTF8, NULL);
// Compile the shader
glCompileShader(shader);
// Check the compile status
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;
}
工具类GLESUtils中用两个类方法实现对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 日志,从而获得错误信息。
六、准备绘制
回到我们的MyGLView,添加下面两个成员变量:
GLuint _programHandle;
GLuint _positionSlot;
再添加如下函数配置program
- (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 进行操作。
接下来我们在initWithFrame方法里调用此方法(在render方法前):
-(instancetype)initWithFrame:(CGRect)frame{
if (self==[super initWithFrame:frame]) {
[self setupLayer];
[self setupContext];
[self setupRenderBuffer];
[self setupFrameBuffer];
[self setupProgram]; //配置program
[self render];
}
return self;
}
以上都是绘制前的准备工作,接下来开始绘制:
七、开始绘制
修改render方法里的代码:
-(void)render
{
//设置清屏颜色,默认是黑色,如果你的运行结果是黑色,问题就可能在这儿
glClearColor(0.3, 0.5, 0.8, 1.0);
/*
glClear指定清除的buffer
共可设置三个选项GL_COLOR_BUFFER_BIT,GL_DEPTH_BUFFER_BIT和GL_STENCIL_BUFFER_BIT
也可组合如:glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
这里我们只用了color buffer,所以只需清除GL_COLOR_BUFFER_BIT
*/
glClear(GL_COLOR_BUFFER_BIT);
// Setup viewport
glViewport(0, 0, self.frame.size.width, self.frame.size.height);
//Draw Point
GLfloat pointVertices[] = {
0.0f, 0.8f, 0.0f,
0.1f, 0.8f, 0.0f,
0.2f, 0.8f, 0.0f,
0.2f, 0.7f, 0.0f
};
//loadData
glVertexAttribPointer(_positionSlot, 3, GL_FLOAT, GL_FALSE, 0, pointVertices );
glEnableVertexAttribArray(_positionSlot);
glDrawArrays(GL_POINTS, 0, 4); //draw
//Draw Line
GLfloat lineVertices[] = {
0.0f, 0.6f, 0.0f,
-0.1f, 0.6f, 0.0f,
0.2f, 0.6f, 0.0f,
0.2f, 0.5f, 0.0f
};
//loadData
glVertexAttribPointer(_positionSlot, 3, GL_FLOAT, GL_FALSE, 0, lineVertices );
glEnableVertexAttribArray(_positionSlot);
glDrawArrays(GL_LINES, 0, 4); //draw
// glDrawArrays(GL_LINE_LOOP, 0, 4);
// glDrawArrays(GL_LINE_STRIP, 0, 4);
// Draw triangle
GLfloat triangleVertices[] = {
-0.5f, 0.4f, 0.0f,
0.5f, 0.4f, 0.0f,
-0.5f, -0.4f, 0.0f,
0.5f, -0.4f, 0.0f
};
//loadData
glVertexAttribPointer(_positionSlot, 3, GL_FLOAT, GL_FALSE, 0, triangleVertices );
glEnableVertexAttribArray(_positionSlot);
// glDrawArrays(GL_TRIANGLES, 0, 4); //这里若想画出2个三角形,还得添加2个顶点,请自行添加(012,345)。
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
// glDrawArrays(GL_TRIANGLE_FAN, 0, 4);
[_context presentRenderbuffer:_renderBuffer];
}
⊙⊙⊙先来介绍下相关函数:
void glVertexAttribPointer (GLuint index, GLint size, GLenum type, GLboolean normalized, GLsizei stride, const GLvoid* ptr);
参数 index :为顶点数据(如顶点,颜色,法线,纹理或点精灵大小)在着色器程序中的槽位;
参数 size :指定每一种数据的组成大小,比如顶点由 x, y, z 3个组成部分,纹理由 u, v 2个组成部分;
参数 type :表示每一个组成部分的数据格式;
参数 normalized : 表示当数据为法线数据时,是否需要将法线规范化为单位长度,对于其他顶点数据设置为 GL_FALSE 即可。如果法线向量已经为单位长度设置为 GL_FALSE 即可,这样可免去不必要的计算,提升效率;
stride : 表示上一个数据到下一个数据之间的间隔(同样是以字节为单位),OpenGL ES根据该间隔来从由多个顶点数据混合而成的数据块中跳跃地读取相应的顶点数据;
ptr :值得注意,这个参数是个多面手(后面再提)。这里它指向 CPU 内存中的顶点数据数组。
glEnableVertexAttribArray(); 允许使用顶点数据
1.glViewport 表示渲染 surface 将在屏幕上的哪个区域呈现出来,请自己修改其参数运行以便理解。
2.我们构造了点、线、三角形的顶点数据(vertices),然后绘制出来。
3.关于绘制点,若就以上图代码,绘制出来的点会很小,可能你会看不见,这时,我们在顶点着色器中添加:
gl_PointSize = 10.0; //只能是float
就会让点变大。
4.关于绘制线,绘制线有3种选项,分别为GL_LINES、GL_LINE_LOOP、GL_LINE_STRIP。
Line Strip , 指首尾相接的线段,第一条线和最后一条线没有连接在一起;
Line Loops, 指首尾相接的线段,第一条线和最后一条线连接在一起,即闭合的曲线;
5.关于绘制三角形,绘制三角形也有三种选项,分别为GL_TRIANGLES、GL_TRIANGLE_STRIP、GL_TRIANGLE_FAN。
Triangle Strip, 指条带,相互连接的三角形
Triangle Fan, 指扇面,相互连接的三角形
最后运行结果如下:
所有教程代码在此 : https://github.com/qingmomo/iOS-OpenGLES-