OpenGL - 使用着色器

上一部分,我们使用了着色器,但没有进行解释说明,这里将介绍着色器的使用。对于OpenGL来说,Vertex Arrays、Buffer Objects、纹理提供了绘制的原始材料。但如果没有着色器,他们只是无效的buffer。

在OpenGL里有两种着色器 Vertex Shader (顶点着色器) 和 Fragment Shader (片段着色器),前者处理顶点在屏幕空间的映射,后者对生成的三角形的光栅化片元进行着色。

顶点着色器

顶点着色器主要处理本地坐标系到剪裁坐标系的转化,因为GL并不识别本地坐标系。所以,在顶点着色器中,要对本地坐标系执行模型视图变化,将本地坐标系转化为剪裁坐标系(Clip Coordinate)坐标值。

[顶点着色器输出(本地坐标系)]=>[视锥体剪裁(剪裁坐标系)]=>...=>屏幕

在编写时,OpenGL中使用GLSL语言,它跟C类似:

//在使用时,GLSL版本要与OpenGL版本对应
#version 330

in vec3 position;
in vec3 color;
out vec3 ver_color;

void main(void)
{
    vec3 pos = position;
    gl_Position = vec4(pos, 1.0);
    ver_color = color;
}

#version定义GLSL版本,这里我们的版本是330GLSL版本和OpenGL版本绑定的很紧;3.30是对应于OpenGL3.30。参见:OpenGL和GLSL版本对应

在上一部分中,我们提到GPU将我们的顶点位置、颜色、法线等顶点属性解释为location值,我们通过硬编码的方式与将其与对应的VBO关联

//在shader里
layout (location = 0) in vec3 position;

//设置VAO时的处理
glVertexAttribPointer(VERTEX_LOCATION_POSITION,3,GL_FLOAT,GL_FALSE,0,0);
glEnableVertexAttribArray(VERTEX_LOCATION_POSITION);

现在,我们改为第二种方式,通过glGetAttribLocation获取着色器属性,并将VBO与着色器上的相应对象进行关联:

首先,在色器里的简单声明一下属性(注意这里没有设置location属性):

in vec3 position;

在代码内部通过函数 glGetAttribLocation 获取着色器属性的解释位置:

GLuint locationPosition =  glGetAttribLocation (shader.getProgramID(), "position");
GLuint locationColor =  glGetAttribLocation (shader.getProgramID(), "color");

然后进行关联和并开启着色器属性的访问

glVertexAttribPointer(locationVertexPosition,3,GL_FLOAT,GL_FALSE,0,0);
glEnableVertexAttribArray(locationVertexPosition);

变量 in 和 out 是GLSL 130版本之后,用于替换attribute、varying变量。前者标识只能在Vertex Shader使用的变量。后者表示该变量是在Vertex Shader和Fragment Shader之间做数据传递用的,并且在Vertex Shader和Fragment Shader要声明一致。

out vec3 ver_color;

ver_color是我们的输出给 Fragment Shader的顶点颜色。

gl_Position 是GLSL提供的一个预定义变量,用来表示顶点在裁剪坐标系中的位置,其格式是vec4,前两位表示剪裁平面的二维坐标,第三、四位表示深度和透视投影。在OpenGL中,[-1,-1]和[1,1]分别对应在剪裁平面空间中左下角和右上角的位置,坐标[0,0]对应剪裁平面的中心点。

跟C类似,GLSL着色器从main函数开始执行,GLSL中main函数不接受参数并返回void。GLSL借用了C的预处理关键字用于它的指令。

GLSL去掉了指针和大多数的C中的各种大小的数值类型,只保留了常用的bool,int和float类型,但是它添加了一系列的向量和矩阵类型,长度最多为4个单元大小。这里你看到的vec3和vec4类型分别是三元素和四元素的float向量。类型名也可以作为这些类型的构造函数使用;你可以使用单值构造一个向量,构成的向量的每个元素都将是这个值,或者从向量和单值的混合构造,它们会绑到一起成为一个更大的向量。GLSL的数学操作和一些内置函数是定义在这些向量类型之上的,可以执行元素级的计算。除了数值类型,GLSL还提供特殊的sampler数据类型用于纹理取样。

设置着色器

#version 330

in vec3 ver_color;
out vec4 frag_color;

void main(void)
{
    frag_color = vec4(ver_color,1.0);
}

在Fragment Shader里,我们接收从Vertex Shader传入颜色值ver_color,并且对于这个变量,每个Frament Shader都接收到一个光栅化的Vertex Shader的输出。

编译着色器

OpenGL 从 GLSL 源代码编译着色器并保存生成的GPU机器码。没有一个标准的方式来将GLSL程序编译成一个二进制,你必须每次都从源代码编译着色器。

一,编译vertex shader和fragment shader.

着色器和程序对象脱离了缓冲和纹理所使用的那套glGen和glBind协议。不像缓冲和纹理函数,操作着色器和程序的函数直接使用对象的整数名作为参数,对象不需要绑定到任何目标。

这里,我们对过调用glCreateShader创建一个着色器对象,着色器参数可以是GL_VERTEX_SHADER或者GL_FRAGMENT_SHADER。然后我们提供一个源代码的字符串指针给glShaderSource,并告诉OpenGL去使用glCompileShader编译着色器。这一步跟C的编译处理过程很类型;编译的着色器对象也是类型一个.o或者.obj文件。

正如C项目中一样,任意多的顶点着色器和片元着色器可以被链接到一起形成一个工作的程序,每个着色器对象引用到其它同类型着色器对象中定义的函数,只要被引用函数全部可以被解析并且顶点着色器和片元着色器的main函数都提供了。

同样正如C程序,一个着色器的代码块可能会由于语法错误,引用不存在的函数,或者类型不匹配而链接失败。OpenGL对每个着色器对象维护一个由GLSL编译器发出的错误或警告信息记录。在编译着色器之后,我们需要使用glGetShaderiv检查它的GLCOMPILESTATUS。如果编译失败了,我们使用showinfolog函数显示信息记录并放弃。

如果着色器对象是GLSL编译过的对象文件,那么程序对象在完成时是可执行的。我们使用glCreateProgram创建一个程序对象,使用glAttachShader附上着色器对象跟它进行链接,最后使用glLinkProgram调用链接过程。

代码:

https://github.com/sbxfc/OpenGLShaderDemo

Comments