OpenGL

OpenGL ES 入门之旅 -- GLSL初识着色器语言

2019-06-10  本文已影响0人  Henry_Jeannie

现代OpenGL渲染管线严重依赖着色器来处理出入的数据,如果不使用着色器,那么OpenGL可以处理的事情可能只有清除窗口了,可见着色器对OpenGL的重要性。在3.0版本(含3.0)以前,如果用到了兼容模式环境,OpenGL还包含一个固定渲染管线,可以在不使用着色器的情况下处理几何与像素数据。自从3.1版本开始,固定渲染管线从核心模式中去除,因此必须使用着色器来完成工作。

GLSL简介

OpenGL着色语言(OpenGL Shading Language)是用来在OpenGL中着色编程的语言,也即开发人员写的短小的自定义程序,他们是在图形卡的GPU (Graphic Processor Unit图形处理单元)上执行的,代替了固定的渲染管线的一部分,使渲染管线中不同层次具有可编程性。比如:视图转换、投影转换等。GLSL(GL Shading Language)的着色器代码分成2个部分:Vertex Shader(顶点着色器)和Fragment(片断着色器),有时还会有Geometry Shader(几何着色器)。负责运行顶点着色的是顶点着色器。它可以得到当前OpenGL 中的状态,GLSL内置变量进行传递。GLSL其使用C语言作为基础高阶着色语言,避免了使用汇编语言或硬件规格语言的复杂性。----GLSL百度百科.

GLSL是OpenGL使用的着色器语言,它是一个C-like语言,语法和C极为相似,区别在于:

  1. 渲染用的着色器是由两部分组成,用于控制顶点变换的顶点着色器和用于控制像素着色的片元着色器.
  2. 使用#version XXX来声明版本,比如#version 120为使用1.2版GLSL规范.
  3. 数据类型有限,没有指针,在早期版本(比如1.1,1.2)中标量只有float,int和bool,后期版本加入了uint,double.
  4. 由于没有内存分配和指针,所有数组必须是定长数组.
  5. 有矢量数据类型,比如vec2、vec3和vec4分别是二三四维float向量,mat2、mat3和mat4分别是2x2,3x3,4x4 float矩阵,matNxM为N*M矩阵,最大为4x4.对于向量类型,可以通过.xyzw、.rgba或.stpq来访问分量,这三种写法没有实质差别,比如对一个vec4变量somevec,somevec.xyz和somevec.rgb都是抽取前三个分量作为一个vec3,somevec.w和somevec.a都是取最后一个分量作为float变量,这三种写法纯粹是供人们阅读方便的,但是不可以混合使用,比如somevec.xgpw就是不友善的写法.
  6. 有采样源类型,比如sampler2D就是一个2D纹理.
  7. 有特殊的变量修饰符,比如attribute定点变量,在顶点着色器中可用,每个顶点均有它独特的值;uniform一致变量,任何着色器均可用,用于存储常量,在一次渲染(即一次DrawCall)中uniform可以视为常量;varying可插值传递变量,由顶点着色器传递给片元着色器的变量,当一个三角形上不同顶点间的输出值不同时,会被插值然后传递给其上的像素.
  8. 特殊的形参修饰符,in输入参数,也是什么都不写时的默认选项,只可读不可写(wiki上说的是可写但结果不会影响实参,就像C那样,但在我这里有时可以这样做,有时却不行...闹不明白);out输出参数,函数可以向这个参数输出值,结果会被反馈给实参,以此可以实现多输出函数;inout引用参数,即可写又可读,结果会被反馈给实参.
  9. 对int和相关的整数操作与位运算的支持到1.3才出现.
  10. 没有字符串.

共同点也是有不少的:

  1. 几乎一摸一样的语法,除了没有字符串和指针以外.
  2. 同样要求函数声明必须在调用之前.
  3. 都支持预处理器,格式相同,包括让编译器程序员吐血的多行预处理.
  4. 支持结构体.

在OpenGL中,GLSL的着色器shader使用的流程与C语言相似,每个shader类似一个C模块,首先需要单独编译(compile),然后一组编译好的shader连接(link)成一个完整程序。一般我们在创建着色器文件时的文件后缀为:.vsh和.fsh(verterx shader/ fragment shader)

在学习着色器代码之前我们先来看下三种变量修饰符:

  1. uniform修饰符

外部(客户端)程序传递到顶点着色器和片元着色器,
(1).在客户端中会提供接口.glUniform 来传递相关的数据,提供赋值功能。
(2).类似于const的作用。即被uniform修饰的变量在顶点和片元着色器中就不会被修改,只能使用。

2.attribute修饰符

attribute是用来修饰属性变量的修饰符,且只能在顶点着色器使用

  1. varying修饰符

中间传递功能,在顶点着色器和片元着色器之间传递。

下面我们来看一下相关着色器代码格式:
顶点着色器shaderv.vsh

//顶点坐标
attribute vec4 position;    // vec4四维向量
//纹理坐标
attribute vec2 textCoordinate;    // vec2 二维向量
//为了传递纹理坐标
varying lowp vec2 varyTextCoord;

void main() {
// 通过varying修饰的varyTextCoord,将纹理坐标传递到片元着色器
varyTextCoord = textCoordinate;

//给内建变量gl_Position赋值(必须赋值)
gl_Position = position;
}

片元着色器shaderf.fsh

//传递过来的纹理坐标
varying lowp vec2 varyTextCoord;
// 纹理采样器 (获取对应的纹理ID)
uniform sampler2D colorMap;

void main() {
//将纹理颜色添加到对应的像素点上
 gl_FragColor = texture2D(colorMap, varyTextCoord);
//返回值应该是一个vec4 即是RGBA--颜色值。
}

gl_FragColor GLSL内建变量 (赋值像素点颜色值)GLSL语言已经提前定义好的变量,有相应的特殊含义。
内建函数 GLSL提前封装好的函数
texture2D(纹理采样器,纹理坐标),获取对应坐标纹素(读取纹素,读取每一个像素点的颜色值)。

着色器和程序

关于顶点着色器和片元着色器的内容已经在前面的文章中介绍过,详情请看OpenGL ES顶点着色器和片元着色器.

我们需要创建两个对象才能用着色器进行渲染:着色器对象和程序对象,类似编译器和链接程序。
着色器源代码被编译成一个目标形式(类似obj文件),编译之后,着色器对象可以连接到一个程序对象,程序对象可以连接多个着色器对象。在OpenGLES中,每个程序对象必须链接一个顶点着色器和一个片元着色器。程序对象被链接为用于渲染的最后“可执行程序”。
获得连接后的着色器对象的过程一般包括6个步骤:
1.创建一个顶点着色器和一个片元着色器:
2.将源代码连接到每个着色器对象
3.编译着色器对象
4.创建一个程序对象
5.将编译后的着色器对象连接到程序对象
6.连接程序对象

1.创建着色器

GLuint glCreateShader(GLenum type);

type —创建着色器类型􏱀􏱁􏰇􏰈􏰉􏰆􏱨􏲆,GL_VERTEX_SHADER(顶点) 􏲇和GL_FRAGMENT_SHADER 􏱅􏱆􏱇 (片元)
返回值 — 􏱗􏰀􏰁􏱊􏰇􏰈􏰉􏰝􏰞􏰆􏰟􏰠 是指向新的着色器对象的句柄,可以调用􏰦􏰧􏱱glDeleteShader 􏱋􏱌删除。

注:我们可以创建多个shader,但是所有的顶点shader只能有一个main函数,片元shader也一样。

2. 删除着色器

void glDeleteShader(GLuint shader);

shader -- 􏰚􏱋􏱌􏰆􏰇􏰈􏰉􏰝􏰞􏰟􏰠􏰚􏱋􏱌􏰆􏰇􏰈􏰉􏰝􏰞􏰟􏰠要删除的着色器对象的句柄。

注:如意一个着色器对象调用了glDeleteShader之后还链接在程序中,则说明这个着色器并没有被真正删除,当调用glDetachShader,将着色器从程序中分离之后,才会被真正删除,所以在调用glDeleteShader之前应该先调用glDetachShader。

3. 链接源代码到着色器对象

void glShaderSource(GLuint shader , GLSizei count ,const GLChar * con st *string, const GLint *length);

shader -- 指向着色器对象的句柄。
count -- 着色器源字符串的数量,着色器可以由多个源字符串组成,但是每个着色器只有一个main函数。
string -- 指向保存数量的count的着色器源字符串的数组指针。
length -- 指向保存每个着色器源字符串大小且元素数量为count的整数数组指针。

4. 编译着色器对象

void glCompileShader(GLuint shader);

shader -- 需要编译的着色器对象句柄

5. 获取编译结果

void glGetShaderiv(GLuint shader , GLenum pname , GLint *params );

获得编译阶段着色器的状态
shader -- 需要编译的着色器对象句柄。
pname -- 获取的信息参数􏰦􏰧􏰗 可以为:GL_COMPILE_STATUS
GL_DELETE_STATUS
GL_INFO_LOG_LENGTH GL_SHADER_SOURCE_LENGTH GL_SHADER_TYPE
params -- 指向查询结果的整数存储位置的指针。

6. 获取编译错误日志

void glGetShaderInfolog(GLuint shader , GLSizei maxLength, GLSizei *l ength , GLChar *infoLog);

获得链接阶段着色器的状态,如果发生错误,这个日志保存的是最后一次操作的信息。
shader -- 需要获取信息日志的着色器对象句柄。
maxLength -- 保存信息日志的缓存区大小。
length -- 写入的信息日志的长度(减去null 终止符);如果不需要知道长度,这个参数可以为Null。
infoLog -- 指向保存信息日志的字符缓存区的指针。

7. 创建程序对象

GLUint glCreateProgram( )

返回值 -- 返回一个执行新程序对象的句柄。

注:也可以创建多个程序对象,在渲染时,可以在不同程序切换。

8. 删除一个程序对象

void glDeleteProgram( GLuint program )

program -- 指向需要删除的程序对象句柄。

9.链接(附着)着色器和程序

void glAttachShader( GLuint program , GLuint shader );

program -- 指向程序对象的句柄。
shader -- 指向程序链接的着色器对象句柄。

注:如果同时有顶点着色器和片元着色器,需要把它们都链接到同一个程序中。就如同一个C程序有多个功能模块一样,我们可以把很多个相同类型的顶点着色器或者片元着色器链接到一个程序中,但是它们只有一个main函数。当然,也可以把一个着色器对象链接到不同的程序中。

10. 断开着色器与程序链接

void glDetachShader(GLuint program, GLuint shader);

即是 将着色器从程序中分离
program -- 指向程序对象的句柄。
shader -- 指向程序链接的着色器对象句柄。

11.连接程序对象

void glLinkProgram(GLuint program)

program -- 指向程序对象的句柄。

12.检查链接是否成功

void glGetProgramiv (GLuint program,GLenum pname, GLint *params);

检查链接是否成功,即检查链接状态。
program -- 指向需要获取信息的程序对象的句柄。
pname -- 获取信息的参数
可以为:
GL_ACTIVE_ATTRIBUTES GL_ACTIVE_ATTRIBUTES_MAX_LENGTH GL_ACTIVE_UNIFORM_BLOCK GL_ACTIVE_UNIFORM_BLOCK_MAX_LENGTH GL_ACTIVE_UNIFROMS GL_ACTIVE_UNIFORM_MAX_LENGTH GL_ATTACHED_SHADERS GL_DELETE_STATUS GL_INFO_LOG_LENGTH
GL_LINK_STATUS GL_PROGRAM_BINARY_RETRIEVABLE_HINT GL_TRANSFORM_FEEDBACK_BUFFER_MODE GL_TRANSFORM_FEEDBACK_VARYINGS GL_TRANSFORM_FEEDBACK_VARYING_MAX_LENGTH GL_VALIDATE_STATUS
params -- 指向查询结果整数存储位置的指针。

13. 获取程序信息日志

void glGetPorgramInfoLog( GLuint program ,GLSizei maxLength, GLSize i *length , GLChar *infoLog )

program -- 指向需要获取信息日志的程序对象句柄。
maxLength -- 存储信息日志的缓存区大小。
length -- 写入的信息日志的长度(减去null 终止符);如果不需要知道长度,这个参数可以为Null。
infoLog -- 指向保存信息日志的字符缓存区的指针。

14. 使用程序对象

void glUseProgram(GLuint program)

program -- 在程序对象链接成功之后,设置为活动程序的程序对象句柄,即成功之后使用程序对象。

注:如果一个程序被使用后,如果被再次链接,则程序被自动替代并使用,不需要再次调用使用函数。
如果使用的参数是0,则表示使用固定功能流水线。

下面我们根据上述函数绘制一个着色器和程序的流程图: Shader-Program.png

下面我们用一份代码来看一下这个创建和链接的过程(代码比较简陋,不足之处望见谅指正):

void setShaders()  {

    NSString *vert = [[NSBundle mainBundle]pathForResource:@"shaderv" ofType:@"vsh"];
    NSString *frag = [[NSBundle mainBundle]pathForResource:@"shaderf" ofType:@"fsh"];


    NSString* vertcontent = [NSString stringWithContentsOfFile:vert encoding:NSUTF8StringEncoding error:nil];
    const GLchar* vertsource = (GLchar *)[vertcontent UTF8String];
    NSString* fragcontent = [NSString stringWithContentsOfFile:frag encoding:NSUTF8StringEncoding error:nil];
    const GLchar* fragsource = (GLchar *)[fragcontent UTF8String];
    
    //定义2个零时着色器对象
    GLuint verShader, fragShader;

    verShader = glCreateShader(GL_VERTEX_SHADER);
    fragShader = glCreateShader(GL_FRAGMENT_SHADER);
    glShaderSource(verShader, 1, &vertsource, NULL);  
    glShaderSource(fragShader, 1, &fragcontent, NULL);  
    glCompileShader(verShader);  
    glCompileShader(fragShader);  

  //3.创建program
    GLint program = glCreateProgram();
  //4.创建最终的程序
    glAttachShader(program, verShader);
    glAttachShader(program, fragShader);
    
    //5.释放不需要的shader
    glDeleteShader(verShader);
    glDeleteShader(fragShader);

   //6.链接
    glLinkProgram(self.myPrograme);
    GLint linkStatus;
    //获取链接状态
    glGetProgramiv(self.myPrograme, GL_LINK_STATUS, &linkStatus);
    if (linkStatus == GL_FALSE) {
        GLchar message[512];
        glGetProgramInfoLog(self.myPrograme, sizeof(message), 0, &message[0]);
        NSString *messageString = [NSString stringWithUTF8String:message];
        NSLog(@"Program Link Error:%@",messageString);
        return;
    }
    
    NSLog(@"Program Link Success!");
    //7.使用program
    glUseProgram(self.myPrograme);

}

补充内容:
OpenGL ES 错误处理
如果不正确使用OpenGL ES 命令,应用程序就会产生一个错误编码,这个错误编码将会被记录,可以调用glGetError查询,在应用程序调用glGetError查询第一个错误代码之前,不会记录其他错误代码,一旦查询到错误代码,当前错误代码便会复位为GL_NO_ERROR.

GLenum glGetError(void);
错误代码与描述.png
上一篇下一篇

猜你喜欢

热点阅读