在前文
《OpenGL ES 创建窗口》代码的基础上进行编码。在前面提到可编程管线通过用 shader 语言编写脚本文件实现的,这些脚本文件相当于 C 源码,有源码就需要编译链接,因此需要对应的编译器与链接器,shader 对象与 program 对象就相当于编译器与链接器。shader 对象载入源码,然后编译成 object 形式(就像C源码编译成 .obj文件)。经过编译的 shader 就可以装配到 program 对象中,每个 program对象必须装配两个 shader 对象:一个顶点 shader,一个片元 shader,然后 program 对象被连接成“可执行文件”,这样就可以在 render 中是由该“可执行文件”了。
完成图2,3配置以后,开始创建顶点着色器和片段着色器,并创建着色器程序链接顶点和片段着色器
OpenGLESUtils.h文件内容为下图
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_STATUS
,GL_SHADER_SOURCE_LENGTH
和 GL_SHADER_TYPE
。在这里我们查询编译情况,如果返回 0,表示编译出错了,错误信息会写入 info 日志中,我们可以查询该 info 日志,从而获得错误信息。
OpenGLESUtils提供的接口让我们可以使用两种方式:脚本字符串或脚本文件来提供 shader 源码,通常使用脚本文件方式有更大的灵活性。(Cocos2D 源码中倒是提供了不少脚本字符串应对一些常见的情况,有兴趣的同学可以查看下)。在这里,我们使用脚本文件方式。按照以下两个步骤创建,创建顶点着色器后缀使用.vsh(即vertextShader),片段着色器后缀使用.fsh(即framentShader)。
vertextShader顶点着色器脚本内容:
attribute vec4 vPosition;
void main(void)
{
gl_Position = vPosition;
}
顶点着色脚本的源码很简单,如果你仔细阅读了前面的介绍,就一目了然。 attribute 属性 vPosition 表示从应用程序输入的类型为 vec4 的位置信息,输出内建 vary 变量 vPosition。留意:这里使用了默认的精度。
fragmentShader片段着色器脚本内容:
precision mediump float;
void main()
{
gl_FragColor = vec4(1.0, 1.0, 0.0, 1.0);
}
片元着色脚本源码也很简单,前面说过片元着色要么自己定义默认精度,要么在每个变量前添加精度描述符,在这里自定义 float 的精度为 mediump。然后为内建输出变量 gl_FragColor 指定为黄色
有了前面的介绍,上面的代码很容易理解。首先我们是由 GLESUtils 提供的辅助方法从前面创建的脚本中创建,装载和编译顶点 shader 和片元 shader;然后我们创建 program,将顶点 shader 和片元 shader 装配到 program 对象中,再使用 glLinkProgram 将装配的 shader 链接起来,这样两个 shader 就可以合作干活了。注意:链接过程会对 shader 进行可链接性检查,也就是前面说到同名变量必须同名同型以及变量个数不能超出范围等检查。我们如何检查 shader 编译情况一样,对 program 的链接情况进行检查。如果一切正确,那我们就可以调用 glUseProgram 激活 program 对象从而在 render 中使用它。通过调用 glGetAttribLocation 我们获取到 shader 中定义的变量 vPosition 在 program 的槽位,通过该槽位我们就可以对 vPosition 进行操作。
编译运行,将看到一个红色的三角形显示在屏幕中央。知道为什么是黄色的么?那是因为 program 也链接了片元着色器,在片元着色脚本文件中,我们指定 gl_FragColor 的值为黄色 vec4(1.0, 1.0, 0.0, 1.0)。
经过《OpenGL ES 创建窗口 》《OpenGL ES零基础入门—-(2)绘制三角形》两章节的学习,我们大概了解了openGL的基本使用,包括设置 CAEAGLLayer 属性,创建 EAGLContext,创建和使用 renderbuffer 和 framebuffer,了解OpenGL ES 渲染管线,创建和使用 shader,创建和实现 program,使用顶点数组进行描绘。流程已经走通,接下来让我们进入 OpenGL ES 各个具体的技术领域。
本文源码可以在这里获得:https://github.com/476455183/OpenGLES