一、什么是shader?
shader是一段GLSL(openGL着色语言)小程序,运行在GPU(图形处理器),而非CPU使用GLSL语言编写,看上去像c或c++,但却是另外一种不同的语言。使用shader就像写个普通程序一样,写代码-->编译-->链接在一起才能生成最终的程序。
着色器类似一个函数调用的方式--数据传输进来,经过处理,然后再传输出去。每个着色器看起来像一个完整的c程序,它的输入点就是一个名为main()的函数,但与c不同的是,GLSL的main函数没有任何参数,也没有返回值。
着色器的建立:
1>创建着色器(顶点和片元)在程序启示的位置,总是要用#version来声名所使用的版本。
glCreatShader(GLenum type),type为GL_VERTEX_SHADER或者是GL_FRAGMENT_SHADER
unsigned int vertexShader; vertexShader = glCreateShader(GL_VERTEX_SHADER);
我们把需要创建的着色器类型以参数形式提供给glCreateShader。由于我们正在创建一个顶点着色器,传递的参数是GL_VERTEX_SHADER。
下一步我们把这个着色器源码附加到着色器对象上,然后编译它:void glShaderSource(GLuint shader,GLsizei count,const GLchar * const *string,const GLint *length);
顶点着色器源码(储存在一个C的字符串中),所以第二个参数为1.第三个参数是顶点着色器真正的源码。
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
glCompileShader(vertexShader);
2>编译着色器
glCompileShader(Gluint shader)。结果查询使用glGetShaderiv(),例如:glGetShaderiv(Gluint shader,GL_COMPILE_STATUS,&compiled)
1 int success; 2 char infoLog[512]; 3 glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
首先我们定义了一个整型变量来表示是否成功编译,还定义了一个存储错误消息的容器(如果有的话)。然后用glGetShaderiv检查是否编译成功。如果编译失败,用glGetShaderInfoLog获取错误消息,然后打印出来。
1 if(!success) 2 { 3 glGetShaderInfoLog(vertexShader, 512, NULL, infoLog); 4 std::cout << "顶点着色器编译错误\n" << infoLog << std::endl; 5 }
3>把着色器添加到程序中
GLuint glCreatPogram();创建一个空的程序
void glAttachShader(GLuint program,GLuint shader);把着色器加到程序中
4>链接你的程序
链接程序的时候会把顶点着色器的输出作为片段着色器的输入。
void glLinkProgram(GLuint program);
void glGetProgramiv(program,GL_LINK_STATUS,&linked);
5>使用程序
void glUseProgram(GLuint program)
那shader到底干了什么?这取决于是哪种shader.
二、Vertex Shader
顶点着色器主要用来将点(x,y,z)变换成不同的点。顶点只是几何形状中的一个点,一个点叫vectex,多个点叫vertices。在本教程中,我们的三角形需要三个顶点(vertices)组成。
Vertex Shader的GLSL代码如下:
1 #version 330 core
2 layout(location = 0) in vec3 vert;
3 void main() {
4 // does not alter the vertices at all
5 gl_Position = vec4(vert, 1.0);//或者是gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);
6 }
第一行#version 330
告诉OpenGL这个shader使用GLSL版本3.3核心版本
第二行 layout(location = 0)设定输入变量的位置值,之后会有用到
in vec3 vert;
告诉shader有一个输入变量(in)vert
第三行定义函数main
,这是shader运行入口。这看上去像C,(区别:)但GLSL中main
不需要带任何参数,并且不用返回,用void。
第四行gl_Position = vec4(vert, 1);
将输入的顶点直接输出,变量gl_Position
是OpenGL定义的全局变量,用来存储vertex shader的输出。所有vertex shaders都需要对gl_Position
进行赋值。
gl_Position
是4D坐标(vec4),但vert
是3D坐标(vec3),所以我们需要将vert
转换为4D坐标vec4(vert, 1)
。第二个的参数1
是赋值给第四维坐标。(因为矩阵变换均是利用齐次坐标)
Vertex Shader在本文中没有做任何事,后续我们会修改它来处理动画,摄像机和其它东西。
顶点着色器源码(储存在一个C的字符串中)
三、Fragment Shader
片元着色器的主要功能是计算每个需要绘制的像素点的颜色。
一个”fragment”基本上就是一个像素,所以你可以认为片段着色器(fragment shader)就是像素着色器(pixel shader)。在本文中每个片段都是一像素,但这并不总是这样的。你可以更改某个OpenGL设置,以便得到比像素更小的片段。
#version 330 core
out vec4 FragColor;
void main() {
FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);//橘黄色
}
创建并编译:
1 unsigned int fragmentShader; 2 fragmentShader = glCreateShader(GL_FRAGMENT_SHADER); 3 glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL); 4 glCompileShader(fragmentShader);
现在两个着色器都编译过了,剩下的事情是把两个着色器对象链接到一个用来渲染的着色器程序中。(shader program)
四、着色器程序
着色器程序对象是多个着色器合并之后最终链接完成的,若要使用刚才编译的着色器,我们必须把它们链接为一个着色器程序对象,然后再渲染对象的时候激活这个着色器程序。
1 //创建 2 unsigned int shaderProgram; 3 shaderProgram = glCreateProgram(); 4 //依附并链接 5 glAttachShader(shaderProgram, vertexShader); 6 glAttachShader(shaderProgram, fragmentShader); 7 glLinkProgram(shaderProgram);
得到的结果就是一个程序对象,我们可以调用glUseProgram函数,用刚创建的程序对象作为它的参数,以激活这个程序对象。
glUseProgram(shaderProgram);
就像着色器的编译一样,我们也可以检测链接着色器程序是否失败,并获取相应的日志
1 glGetProgramiv(shaderProgram,GL_LINK_STATUS,&success); 2 if (!success) { 3 glGetShaderInfoLog(shaderProgram, 512, NULL, infoLog); 4 cout << "着色器程序链接出错" << infoLog << endl; 5 }
在把着色器对象链接到程序对象以后,记得删除着色器对象,我们不再需要它们了:
1 glDeleteShader(vertexShader); 2 glDeleteShader(fragmentShader);
现在,我们已经把输入顶点数据发送给了GPU,并指示了GPU如何在顶点和片段着色器中处理它。就快要完成了,但还没结束,OpenGL还不知道它该如何解释内存中的顶点数据,以及它该如何将顶点数据链接到顶点着色器的属性上。我们需要告诉OpenGL怎么做。
五、链接顶点属性
使用glVertexAttribPointer函数告诉OpenGL该如何解析顶点数据.
1 glVertexAttribPointer(0,3,GL_FLOAT,GL_FALSE,3*sizeof(float),0); 2 glEnableVertexAttribArray(0);
第一个参数指定我们要配置的顶点属性.之前我们在顶点着色器中使用layout(location = 0)
定义了position顶点属性的位置值(Location)。我们希望把数据传递到这一个顶点属性中,所以这里我们传入0
。
第二个参数指定顶点属性的大小。顶点属性是一个vec3
,它由3个值组成,所以大小是3。
第三个参数指定数据的类型,这里是GL_FLOAT(GLSL中vec*
都是由浮点数值组成的)。
第四个参数定义我们是否希望数据被标准化(Normalize)。如果我们设置为GL_TRUE,所有数据都会被映射到0(对于有符号型signed数据是-1)到1之间。我们把它设置为GL_FALSE。
第五个参数叫做步长(Stride),它告诉我们在连续的顶点属性组之间的间隔。由于下个组位置数据在3个float
之后,我们把步长设置为3 * sizeof(float)
。要注意的是由于我们知道这个数组是紧密排列的(在两个顶点属性之间没有空隙)
最后一个参数的类型是void*
,所以需要我们进行这个奇怪的强制类型转换。表示偏移量,设为0.