赞
踩
关于在OpenGL中怎么创建Shader这个在很早我博客中就有过详细介绍了。这里全当复习,温故而知新~
在OpenGL中,存在Program和Shader两个概念,Program相当于当前渲染管线所使用的程序,是Shader的容器,可以挂载多个Shader。而每个Shader相当于一个C模块,首先需要对Shader脚本进行编译,然后讲编译好的Shader挂载到Program上,在OpenGL的渲染中使用Program来使Shader生效,整个流程如下图所示:
整个流程其实就是创建Shader和创建Program两个子流程。创建Shader的流程如下:
然后创建好的Shader需要被挂载到Program中,创建Program的流程如下:
整个流程中的编译、链接其实和我们的c/c++的编译、链接一样,先编译成.o/.dll库,然后把这些库文件链接成一个可执行的程序。
我们可以创建多个Program,但同时只可以激活一个Program。创建完Program之后,只需要在调用OpenGL绘制方法前使用glUseProgram即可应用当前的Shader。如果激活的Program没有挂载片段Shader,那么片段Shader执行的结果就是为定义的。如果激活一个不存在的Shader,那么所有Shader的执行结果都是未定义的。
下面来简单介绍下流程相关的API:
//创建一个Shader对象,并返回引用该对象的句柄--一个非0整数
//参数shaderType 为Shader类型,一般是GL_VERTEX_SHADER、GL_FRAGMENT_SHADER、GL_GEOMETRY_SHADER
GLuint glCreateShader(GLenum shaderType);
//加载源码到Shader中,这个操作会将Shader脚本代码复制到Shader对象中,多次调用会覆盖上次脚本
//参数shader是创建时返回的shader对象句柄
//参数count是string数组的长度
//参数string是shader的源码,一个字符串数组
//参数length是一个int数组,对应string参数这个字符串数组中每个字符串的长度,当这些字符串都是以‘/0‘结尾时,可以将这个参数设为NULL
void glShaderSource(GLuint shader, int count, const char **string, int *length);
//编译存储于Shader中的代码,参数shader表示对象句柄
void glCompileShader(GLuint shader);
----------
//创建一个Program对象,同样返回引用它的对象句柄--一个非0整数
GLuint glCreateProgram();
//将一个已经编译好的Shader挂载到Program中
//参数program和shader分别表示创建时它两对象返回的的句柄
//一个Shader可以同时被挂载到多个Program中,但同一种类型的Shader,Program只能挂载一个
void glAttachShader(GLuint program, GLuint shader);
//对指定的Program对象执行链接操作,Program在链接成功后才可以执行
//链接操作会将Program中的所有Uniform变量初始化为0
void glLinkProgram(GLuint program);
//激活指定的Program,接下来的绘制会使用指定的Program进行渲染
void glUseProgram(GLuint program);
最后演示下上面讲的接口的使用事例,useShader()函数接收两个参数,分别是顶点着色器和片段着色器的源码。
void useShader(const char* vs, const char* fs)
{
int v = glCreateShader(GL_VERTEX_SHADER);
int f = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(v, 1, &vs, NULL);
glShaderSource(f, 1, &fs, NULL);
glCompileShader(v);
glCompileShader(f);
int p = glCreateProgram();
glAttachShader(p, v);
glAttachShader(p, f);
glLinkProgram(p);
glUseProgram(p);
}
在Shader中,属性变量和统一变量由应用程序设置,Attribute属性变量用于传递顶点信息,而Uniform统一变量则用于传递用户自定义的变量。这两种变量在Shader中会被定义为全局变量,在OpenGL中要设置这两种变量,就需要先获取它们的地址,然后调用OpenGL相关的设置接口为它们赋值。
1.设置属性变量的接口
属性变量包含的是顶点数据,因此在片段着色器中不能直接使用。而且在顶点着色器中它是只读的。要使用它首先得获取地址,这个信息只有在Program链接之后才可以获取。(某些驱动程序,在获取地址之前还必须调用glUseProgram()方法激活Program)
下面是获取属性变量地址的接口:
//参数program为要操作的Program对象句柄
//参数name为要获取的属性变量名
GLint glGetAttribLocation(GLuint program, char *name);
获取到位置后,然后就可以使用glVertexAttribxx系列方法来为这个属性赋值了。
2.设置属性变量的时机
先假设有这样一个属性变量,获取它的位置如下:
GLint local = glGetAttribLocation(program, "myattribute");
设置属性变量是在渲染时为其赋值的,OpenGL渲染时赋值有两种形式,一种是在glBegin()和glEnd()中间,在使用glVertex系列函数生成顶点前。先调用glVertexAttrib系列函数进行赋值,接下来生成的顶点会绑定前面设置的属性变量。
glBegin(GL_TRIANGLE_STRIP);
glVertexAttrib1f(local, 1.0f);
glVertex2f(0.0f, 0.0f);
glVertexAttrib1f(local, 2.0f);
glVertex2f(0.0f, 1.0f);
glVertexAttrib1f(local, 3.0f);
glVertex2f(1.0f, 1.0f);
glVertexAttrib1f(local, 4.0f);
glVertex2f(1.0f, 0.0f);
glEnd();
第二种情况是使用顶点数组渲染时,这个得先激活属性变量数组的功能,
void glEnableVertexAttribArray(GLint local);
开启这个功能后,需要调用glVertexAttribPointer()方法,将属性变量的值批量传入,属性变量的数组和顶点数组是一一对应的。
//参数local,属性变量的位置
//参数size,属性变量的分量数量,必须为1~4,如1为float、2~3为vec2~3
//参数type,属性类型,如GL_FLOAT
//参数normalized,是否对传入的值执行一次归一化操作
//参数stride,顶点数组中,两个顶点之间的步幅,0表连续的顶点
//参数pointer,属性变量列表指针,与顶点数组中的顶点一一对应
void glVertexAttribPointer(GLint local, GLint size, GLenum type, GLboolean normalized, GLsizei stride, const void *pointer);
下面是使用顶点数组进行渲染时,为顶点数组中的每一个顶点绑定属性变量的事例:
//定义4个顶点
float vertices[8] = {
0.0f, 0.0f,
0.0f, 1.0f,
1.0f, 1.0f,
1.0f, 0.0f
};
float myattributes[4] = {1.0f, 2.0f, 3.0f, 4.0f};
//获取一个已经成功链接的Program中的myattribute属性变量
GLint local = glGetAttribLocation(p, "myattribute");
//使用顶点数组
glEnableClientState(GL_VERTEX_ARRAY);
//启用顶点属性变量数组
glEnableVertexAttribArray(local);
//设置顶点数组
glVertexPointer(2, GL_FLOAT, 0, vertices);
//设置顶点属性数组
glVertexAttribPointer(local, 1, GL_FLOAT, GL_FALSE, 0, myattributes);
属性变量相当于每个顶点的私有只读变量,而Uniform统一变量则相当于整个Program的全局只读变量。统一变量和属性变量一样,都是先获取变量的位置,然后调用相关的接口进行设置的。不过统一变量在绘制时不能修改,所以必须在绘制前设置它的值。
至于设置接口和属性变量一致,只是将方法名中的Attrib或VertexAttrib替换成Uniform。统一变量的设置比属性变量要轻松得多,因为不需要想办法绑定到每个顶点上,只需要在渲染之前进行设置就可以了。当然它数据类型可以是纹理或者矩阵类型,这些使用时自行find~这里就不再累赘呢。
Shader在编译或链接的时候一般比较容易出现错误,而编译和链接方法的返回值都是void,那么当它们出现错误时怎么知道呢?这就需要使用glGetShaderiv()和glGetProgramiv()这两个函数,它们接口原型一致,都是传入指定的对象,以及要获取的状态枚举,并传入一个GLint指针来接收状态的值。
//查询GL_COMPILE_STATUS可以得到编译的结果,GL_TRUE表示成功,GL_FALSE表示失败
void glGetShaderiv(GLuint shader, GLenum pname, GLint *params);
//查询GL_LINK_STATUS可以得到链接的结果,GL_TRUE表示成功,GL_FALSE表示失败
void glGetProgramiv(GLuint program, GLenum pname, GLint *params);
如果发生错误,错误日志会被保存到InfoLog中,可以调用glGetShaderInfoLog()和glGetProgramInfoLog()方法从中查询错误相关信息。这两个方法原型也一致,第一个参数为Shader或Program的句柄,maxLength参数表示infoLog缓冲区的长度,length 参数指针会输出实际复制到infoLog中的字节数,infoLog参数为用于接收日志信息字符串。
void glGetShaderInfoLog(GLuint shader, GLsizei maxLength, GLsizei *length, GLchar *infoLog);
void glGetProgramInfoLog(GLuint program, GLsizei maxLength, GLsizei *length, GLchar *infoLog);
那么InfoLog日志长度是多少呢?使用glGetShaderiv()和glGetProgramiv()这两个函数,传入GL_INFO_LOG_LENGTH类型,可以获取日志的长度。
下面我们来实际看个事例,它获取Shader日志并将日志用printf()打印出来。
void printShaderLog(GLuint shader)
{
GLint shaderState;
glGetShaderiv(shader, GL_COMPILE_STATES, &shaderState);
if(shaderState == GL_TRUE)
{
return;
}
GLsizei bufferSize = 0;
glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &bufferSize);
if(bufferSize > 0)
{
GLchae* buffer = new char[bufferSize];
glGetShaderInfoLog(shader, bufferSize, NULL, buffer);
printf("%s", buffer);
delete[] buffer;
}
}
在调用glCreateShader()和glCreateProgram()方法后,不需要使用的时候,需要使用glDeleteShader()和glDeleteProgram()进行释放。
当一个Shader被挂载到Program中时,glDeleteShader()是无法释放这个Shader的,只会将这个Shader标记为以删除,还需要调用glDetachShader()将Shader从Program中卸除。
void glDetachShader(GLuint program, GLuint shader);
void glDeleteShader(GLuint shader);
void glDeleteProgram(GLuint program);
同样当一个Program在被使用时,glDeleteProgram()方法是无法释放这个Program的,只是将这个Program标记为已删除,当Program不再被使用时,Program才会被释放。当Program真正被释放时,所有挂载在它上面的Shader都会被自动卸载。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。