由于这里的知识点很细碎又不是很多,所以我边学OpenGl一边把需要用到的GLSL知识写上去。
0.概念和初始化:
着色器分为顶点着色器(Vertex Shader)和片元着色器(Fragment Shader),语法类似C++,OpenGL对每一个顶点都执行一次顶点着色器,对所有片元执行片元着色器(片元可以狭隘的理解为像素)。
初始化:
In C++:
GLuint programID = LoadShaders( "TransformVertexShader.vertexshader", "ColorFragmentShader.fragmentshader" );
获得到的是链接的着色器句柄。但是LoadShaders却需要我们自己编写。
GLuint LoadShaders(const char * vertex_file_path,const char * fragment_file_path){ // Create the shaders GLuint VertexShaderID = glCreateShader(GL_VERTEX_SHADER); GLuint FragmentShaderID = glCreateShader(GL_FRAGMENT_SHADER); // Read the Vertex Shader code from the file std::string VertexShaderCode; std::ifstream VertexShaderStream(vertex_file_path, std::ios::in); if(VertexShaderStream.is_open()){ std::stringstream sstr; sstr << VertexShaderStream.rdbuf(); VertexShaderCode = sstr.str(); VertexShaderStream.close(); }else{ printf("Impossible to open %s. Are you in the right directory ? Don't forget to read the FAQ ! ", vertex_file_path); getchar(); return 0; } // Read the Fragment Shader code from the file std::string FragmentShaderCode; std::ifstream FragmentShaderStream(fragment_file_path, std::ios::in); if(FragmentShaderStream.is_open()){ std::stringstream sstr; sstr << FragmentShaderStream.rdbuf(); FragmentShaderCode = sstr.str(); FragmentShaderStream.close(); } GLint Result = GL_FALSE; int InfoLogLength; // Compile Vertex Shader printf("Compiling shader : %s ", vertex_file_path); char const * VertexSourcePointer = VertexShaderCode.c_str(); glShaderSource(VertexShaderID, 1, &VertexSourcePointer , NULL); glCompileShader(VertexShaderID); // Check Vertex Shader glGetShaderiv(VertexShaderID, GL_COMPILE_STATUS, &Result); glGetShaderiv(VertexShaderID, GL_INFO_LOG_LENGTH, &InfoLogLength); if ( InfoLogLength > 0 ){ std::vector<char> VertexShaderErrorMessage(InfoLogLength+1); glGetShaderInfoLog(VertexShaderID, InfoLogLength, NULL, &VertexShaderErrorMessage[0]); printf("%s ", &VertexShaderErrorMessage[0]); } // Compile Fragment Shader printf("Compiling shader : %s ", fragment_file_path); char const * FragmentSourcePointer = FragmentShaderCode.c_str(); glShaderSource(FragmentShaderID, 1, &FragmentSourcePointer , NULL); glCompileShader(FragmentShaderID); // Check Fragment Shader glGetShaderiv(FragmentShaderID, GL_COMPILE_STATUS, &Result); glGetShaderiv(FragmentShaderID, GL_INFO_LOG_LENGTH, &InfoLogLength); if ( InfoLogLength > 0 ){ std::vector<char> FragmentShaderErrorMessage(InfoLogLength+1); glGetShaderInfoLog(FragmentShaderID, InfoLogLength, NULL, &FragmentShaderErrorMessage[0]); printf("%s ", &FragmentShaderErrorMessage[0]); } // Link the program printf("Linking program "); GLuint ProgramID = glCreateProgram(); glAttachShader(ProgramID, VertexShaderID); glAttachShader(ProgramID, FragmentShaderID); glLinkProgram(ProgramID); // Check the program glGetProgramiv(ProgramID, GL_LINK_STATUS, &Result); glGetProgramiv(ProgramID, GL_INFO_LOG_LENGTH, &InfoLogLength); if ( InfoLogLength > 0 ){ std::vector<char> ProgramErrorMessage(InfoLogLength+1); glGetProgramInfoLog(ProgramID, InfoLogLength, NULL, &ProgramErrorMessage[0]); printf("%s ", &ProgramErrorMessage[0]); } glDetachShader(ProgramID, VertexShaderID); glDetachShader(ProgramID, FragmentShaderID); glDeleteShader(VertexShaderID); glDeleteShader(FragmentShaderID); return ProgramID; }
大意是通过文件流获取两个着色器代码,然后分别编译这两个代码得到两个着色器句柄,最后把创建一个进程把这两个句柄合一块,最后把这个进程链接到主程序中。
由于此代码是如此的枯燥,所以最好封装成一个库。
1.传入属性(Attribute):
In GLSL,Vertex Shader:
layout(location = 0) in vec3 vertexPosition_modelspace;
我们要知道,我们传进去的数据是顶点属性,0代表一个位置,OpenGL确保至少有16个包含4分量的顶点属性可用,但是有些硬件或许允许更多的顶点属性,你可以查询GL_MAX_VERTEX_ATTRIBS来获取具体的上限。
in vec3表示输入,使用vec3这种类型,后面的是名字。
In C++:
第一步创建一个VBO把顶点数据拷进去:
GLuint vertexbuffer; glGenBuffers(1, &vertexbuffer); glBindBuffer(GL_ARRAY_BUFFER, vertexbuffer); glBufferData(GL_ARRAY_BUFFER, sizeof(g_vertex_buffer_data), g_vertex_buffer_data, GL_STATIC_DRAW);
参数应该很好懂,最后一个函数glBufferData的第四个参数是GL_STATIC_DRAW,代表静态数据,不能修改,可以提高性能。
第二步:
glEnableVertexAttribArray(0); glBindBuffer(GL_ARRAY_BUFFER, vertexbuffer); glVertexAttribPointer( 0, // attribute. No particular reason for 0, but must match the layout in the shader. 3, // size GL_FLOAT, // type GL_FALSE, // normalized? 0, // stride (void*)0 // array buffer offset );
一步步解释。首先得知道,我们向着色器里传进去的是顶点属性(Vertex Attribute),之前说过它的数量是有限制的。
第一行glEnableVertexAttribArray(0)指定的是启用location=0的顶点属性,此时在GLSL中可以用layout(location = 0)接收到。
第二行把vertexbuffer——我们定义的顶点缓冲,之前我把顶点类型为GLfloat的一维数组,其中每连续三个元素代表一个顶点——绑定到当前上下文的GL_ARRAY_BUFFER中,在使用前要绑定,这时我们就可以使用——
第三行glVertexAttribPointer,它用于指定顶点数组的数据格式和位置。首先是位置,第一个参数,这里填的0,对应layout的0;3代表三个元素一个顶点;GL_FLOAT代表我们的类型;GL_FALSE代表不进行标准化(向量标准化就是把(1,10,1,0)变成(0.1,1,0.1,0));0代表步长(此处等效于4),最后一个是数组偏移量,因为我们从数组下标为0开始定义顶点,所以填0。
为什么要指定这些呢?因为OpenGL只知道array buffer里的二进制数据,并不知道这些数据的意义,所以当我们要把这些数据传给着色器的时候,必须要事先指定类型和位置。
最后,随手关门好习惯,记得关闭顶点属性和销毁顶点数组缓冲区。
glDisableVertexAttribArray(0);//关闭位置为0的顶点属性 glDeleteBuffers(1, &vertexbuffer);//销毁顶点数组缓冲
2.传入变量(Variable)
使用uniform关键字来实现,适用于所有着色器统一传入的常量。
In GLSL,Vertex Shader
使用uniform关键字传入一个矩阵:
uniform mat4 MVP;
注意,虽然说是变量,但uniform传入的变量实际上不可修改。
In C++:
GLuint MatrixID = glGetUniformLocation(programID, "MVP");
通过glGetUniformLocation获取指定名字的uniform的句柄,programID是我们之前提到的着色器句柄。
glUniformMatrix4fv(MatrixID, 1, GL_FALSE, &MVP[0][0]);
通过glUniformMatrix4fv传入数据,MatrixID是之前获取到的句柄,1代表数组数量或者矩阵的数量,GL_FALSE指定矩阵是列优先矩阵,最后传入数据开头的指针。
除了传入矩阵,还有以下函数来对uniform进行装载(没有重载太蛋疼了):
这是获取uniform地址,返回一个句柄,接下来的装载将使用这个句柄。