基于OpenGL编写一个简易的2D渲染框架-09 重构渲染器-Shader
Shader 只是进行一些简单的封装,主要功能:
1、编译着色程序
2、绑定 Uniform 数据
3、根据着色程序的顶点属性传递顶点数据到 GPU
着色程序的编译
GLuint Shader::createShaderProgram(const char* vsname, const char* psname) { std::string vShaderSource, fShaderSource; std::ifstream vShaderFile, fShaderFile; vShaderFile.exceptions(std::ifstream::badbit); fShaderFile.exceptions(std::ifstream::badbit); try { vShaderFile.open(PathHelper::fullPath(vsname), std::ios::in); fShaderFile.open(PathHelper::fullPath(psname), std::ios::in); std::stringstream vShaderStream, fShaderStream; vShaderStream << vShaderFile.rdbuf(); fShaderStream << fShaderFile.rdbuf(); vShaderSource = vShaderStream.str(); fShaderSource = fShaderStream.str(); vShaderFile.close(); fShaderFile.close(); } catch ( std::ifstream::failure e ) { throw std::exception("Error shader: file not succesfully read"); } const GLchar* vShaderCode = vShaderSource.c_str(); const GLchar* fShaderCode = fShaderSource.c_str(); /* 创建顶点作色器 */ GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER); glShaderSource(vertexShader, 1, &vShaderCode, NULL); glCompileShader(vertexShader); GLint success; GLchar infoLog[512]; glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success); if ( !success ) { glGetShaderInfoLog(vertexShader, 512, NULL, infoLog); throw std::exception(""); } /* 创建片段着色器 */ GLuint fragmentShader = glCreateShader(GL_FRAGMENT_SHADER); glShaderSource(fragmentShader, 1, &fShaderCode, NULL); glCompileShader(fragmentShader); glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success); if ( !success ) { glGetShaderInfoLog(fragmentShader, 512, NULL, infoLog); throw std::exception(""); } /* 创建着色程序 */ GLuint shaderProgram = glCreateProgram(); glAttachShader(shaderProgram, vertexShader); glAttachShader(shaderProgram, fragmentShader); glLinkProgram(shaderProgram); glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success); if ( !success ) { glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog); throw std::exception(""); } glDeleteShader(vertexShader); glDeleteShader(fragmentShader); /* 使用着色程序 */ return shaderProgram; }
Simple2D 只支持顶点着色器和片段着色器,暂不支持其他着色器。
OpenGL 绘制方式
使用openGL图形库绘制,都需要通过openGL接口向图像显卡提交顶点数据,显卡根据提交的数据绘制出相应的图形。
其中有四种方式:
1、立即模式
2、显示列表
3、顶点数组
4、现代VAO、ABO
立即模式和显示列表是 OpenGL 传统模式的绘制方法(现在都 2017 年了,应该没有人用这种方式了吧?),后两种是现代方式绘制。前面渲染器用的就是第四种方式:现代 VBO 和 VAO。
VBO 即 Vertex Buffer Object,是一个在高速视频卡中的内存缓冲,用来保存顶点数据,也可用于包含诸如归一化向量、纹理和索引等数据。
VAO 即 Vertex Array Object ,是一个包含一个或多个VBO的对象,被设计用来存储一个完整被渲染对象所需的信息。
这里不再对其进行介绍,感兴趣的可以点击这个链接:https://learnopengl-cn.github.io/01%20Getting%20started/04%20Hello%20Triangle/
本次渲染器用的是第三种方式,为了更好地理解这种绘制方式,下面举一个例子,假设顶点着色器的顶点属性为
layout(location = 0) in vec3 Position; layout(location = 1) in vec2 Texcoord; layout(location = 2) in vec4 Color;
这里随机给定一些顶点数据,包含有位置、颜色和纹理坐标
GLfloat vertexes[] = { 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, };
GLfloat colors[] = { 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f };
GLfloat texCoordes[] = { 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f };
接下来使用函数 glVertexAttribPointer 把顶点数据传递到 GPU。
void glVertexAttribPointer( GLuint index, GLint size, GLenum type, GLboolean normalized, GLsizei stride,const GLvoid * pointer);
glEnableVertexAttribArray(0); glEnableVertexAttribArray(1); glEnableVertexAttribArray(2); glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, vertexes); glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 0, colores); glVertexAttribPointer(2, 4, GL_FLOAT, GL_FALSE, 0, texCoordes);
然后调用 glDrawArrays 函数进行绘制(使用了顶点索引的可调用 glDrawElements),上面的例子中每个顶点属性的数据储存在不同的数组中。如果你想把数据都储存在一个数组中,就如下面一样
GLfloat data[] = { 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f };
这样称之为交错数组,而且使用函数 glVertexAttribPointer 的参数需要作出相应的改变
glEnableVertexAttribArray(0); glEnableVertexAttribArray(1); glEnableVertexAttribArray(2); glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 9 * sizeof(GLfloat), (char*)data + sizeof(GLfloat) * 0);
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 9 * sizeof(GLfloat), (char*)data + sizeof(GLfloat) * 3);
glVertexAttribPointer(2, 4, GL_FLOAT, GL_FALSE, 9 * sizeof(GLfloat), (char*)data + sizeof(GLfloat) * 5);
第五个参数是跨度,即所有顶点属性的大小,3 + 2 + 4 = 9,最后一个参数是数据的指针,不同顶点属性的数据要加上适当的偏移。最后调用绘制函数即可。
通过上面的例子可知,每个顶点着色器的顶点属性都不一定一样,所以在传递顶点数据到 GPU 时所执行的操作不一样。所以在创建 Shader 前需要设置 Shader 的顶点属性数组,通过顶点属性数组来执行传递数据到 GPU 的操作。
分析 glVertexAttribPointer 函数的参数,定义下面顶点属性的结构
struct VertexAttribute { int layout; int size; int type; int stride; int offset; };
Shader 默认有两种顶点属性,分别是 位置-颜色 和 位置-纹理坐标-颜色
enum CustomVertexAttribute { CVA_UNKNOEWN, CVA_V3F_C4F, CVA_V3F_T2F_C4F };
设置着色器的顶点属性数组,默认最多存在8个顶点属性
void Shader::setVertexAttribute(CustomVertexAttribute cva) { if ( cva == CustomVertexAttribute::CVA_V3F_C4F ) { VertexAttribute vertexAttributes[2] = { { 0, 3, GL_FLOAT, 7 * sizeof(GL_FLOAT), 0 * sizeof(GL_FLOAT) }, { 1, 4, GL_FLOAT, 7 * sizeof(GL_FLOAT), 3 * sizeof(GL_FLOAT) } }; this->setVertexAttribute(vertexAttributes, sizeof(vertexAttributes) / sizeof(VertexAttribute)); } else if(cva == CustomVertexAttribute::CVA_V3F_T2F_C4F){ VertexAttribute vertexAttributes[3] = { { 0, 3, GL_FLOAT, 9 * sizeof(GL_FLOAT), 0 * sizeof(GL_FLOAT) }, { 1, 2, GL_FLOAT, 9 * sizeof(GL_FLOAT), 3 * sizeof(GL_FLOAT) }, { 2, 4, GL_FLOAT, 9 * sizeof(GL_FLOAT), 5 * sizeof(GL_FLOAT) } }; this->setVertexAttribute(vertexAttributes, sizeof(vertexAttributes) / sizeof(VertexAttribute)); } } void Shader::setVertexAttribute(VertexAttribute* attribute, int count) { assert(count < 8); for ( nVertexAttributeCount = 0; nVertexAttributeCount < count; nVertexAttributeCount++ ) { vertexAttributes[nVertexAttributeCount] = attribute[nVertexAttributeCount]; } }
在设置好 Shader 的顶点属性数组后,就可以正确的传递顶点数据到 GPU 了
void Shader::bindVertexDataToGPU(void* data) { void* data_offset = nullptr; for ( int i = 0; i < nVertexAttributeCount; i++ ) { data_offset = static_cast< char* > ( data ) +vertexAttributes[i].offset; /* 上传顶点数据 */ glVertexAttribPointer( vertexAttributes[i].layout, vertexAttributes[i].size, vertexAttributes[i].type, GL_FALSE, vertexAttributes[i].stride, data_offset); glEnableVertexAttribArray(vertexAttributes[i].layout); } }
函数接受顶点数据的指针,然后根据顶点属性数组调用 glVertexAttribPointer 函数设置顶点属性数组的数据格式和位置,最后调用绘制函数绘制即可。
绑定 Uniform 数据
假如你要绑定一个整数 2 到着色器,你可以通过两步完成:
1、调用函数 glGetUniformLocation 获取着色器中 Uniform 变量的绑定点 location。
2、调用 glUniform 为当前着色程序对象指定Uniform变量的值。
由于只是绑定一个整型数 2,可以使用下面的代码绑定
glUniform1i(glGetUniformLocation(shaderProgram, "valueName"), 2);
值得注意的是,C 语言没有函数重载,所以会有很多名字相同后缀不同的函数版本存在。其中函数名中包含数字(1、2、3、4)表示接受这个数字个用于更改uniform变量的值,i表示32位整形,f表示32位浮点型,ub表示8位无符号byte,ui表示32位无符号整形,v表示接受相应的指针类型。
下面列举了这些函数
void glUniform1f(GLint location, GLfloat v0); void glUniform2f(GLint location, GLfloat v0, GLfloat v1); void glUniform3f(GLint location, GLfloat v0, GLfloat v1, GLfloat v2); void glUniform4f(GLint location, GLfloat v0, GLfloat v1, GLfloat v2, GLfloat v3);
void glUniform1i(GLint location, GLint v0); void glUniform2i(GLint location, GLint v0, GLint v1); void glUniform3i(GLint location, GLint v0, GLint v1, GLint v2); void glUniform4i(GLint location, GLint v0, GLint v1, GLint v2, GLint v3); void glUniform1fv(GLint location, GLsizei count, const GLfloat *value); void glUniform2fv(GLint location, GLsizei count, const GLfloat *value); void glUniform3fv(GLint location, GLsizei count, const GLfloat *value); void glUniform4fv(GLint location, GLsizei count, const GLfloat *value); void glUniform1iv(GLint location, GLsizei count, const GLint *value); void glUniform2iv(GLint location, GLsizei count, const GLint *value); void glUniform3iv(GLint location, GLsizei count, const GLint *value); void glUniform4iv(GLint location, GLsizei count, const GLint *value); void glUniformMatrix2fv(GLint location, GLsizei count, GLboolean transpose, const GLfloat *value); void glUniformMatrix3fv(GLint location, GLsizei count, GLboolean transpose, const GLfloat *value); void glUniformMatrix4fv(GLint location, GLsizei count, GLboolean transpose, const GLfloat *value);
这次 Shader 类对 Uniform 的绑定做一点简单的封装,新定义一个 Uniform 类,实现 Uniform 数据的绑定(一个 Uniform 类对象表示着色器中的一个 Uniform 数据,如果着色器存在多个 Uniform 数据,则 Shader 也相应有 Uniform 对象数组)。首先定义 Uniform 类型枚举
enum UniformType { UT_1I, UT_1F, UT_2I, UT_2F, UT_3I, UT_3F, UT_4I, UT_4F, UT_TEXTURE };
这次绑定 Uniform 数据不包括数组和矩阵,只是一些绑定简单的数据。
要绑定一个 Uniform 数据,需要绑定点 location 和值 value,故 Uniform 类成员属性如下
int nLocation; UniformType uniformType; float fV0, fV1, fV2, fV3;
当绑定一个 float 数据时使用 fV0,如果绑定 vec2 数据则使用 fV0 和 fV1(后两个不使用),其他情况类似。
下面看具体的绑定函数
bool bind(int tex = 0) { switch ( uniformType ) { case Simple2D::Uniform::UT_1I: glUniform1i(nLocation, fV0); break; case Simple2D::Uniform::UT_1F: glUniform1f(nLocation, fV0); break; case Simple2D::Uniform::UT_2I: glUniform2i(nLocation, fV0, fV1); break; case Simple2D::Uniform::UT_2F: glUniform2f(nLocation, fV0, fV1); break; case Simple2D::Uniform::UT_3I: glUniform3i(nLocation, fV0, fV1, fV2); break; case Simple2D::Uniform::UT_3F: glUniform3f(nLocation, fV0, fV1, fV2); break; case Simple2D::Uniform::UT_4I: glUniform4i(nLocation, fV0, fV1, fV2, fV3); break; case Simple2D::Uniform::UT_4F: glUniform4f(nLocation, fV0, fV1, fV2, fV3); break; case Simple2D::Uniform::UT_TEXTURE: glActiveTexture(GL_TEXTURE0 + tex); glBindTexture(GL_TEXTURE_2D, fV0); glUniform1i(nLocation, tex); return true; } return false; }
函数的最后实现的是绑定纹理的功能,设置纹理到着色器,我们可以使用 glActiveTexture 激活纹理单元,传入我们需要使用的纹理单元:
glActiveTexture(GL_TEXTURE0); // 在绑定纹理之前先激活纹理单元 glBindTexture(GL_TEXTURE_2D, texture);
glUniform1i(location, 0);
激活纹理单元之后,接下来的 glBindTexture 函数调用会绑定这个纹理到当前激活的纹理单元,最后的 glUniform1i 函数传入纹理单元序号 0(纹理单元 GL_TEXTURE8 的序号为 8) 参数实现将纹理设置到着色器。纹理单元 GL_TEXTURE0 默认总是被激活,所以我们只有一张纹理时使用 glBindTexture 的时候,无需激活任何纹理单元。
OpenG L至少保证有 16 个纹理单元供你使用,也就是说你可以激活从 GL_TEXTURE0 到 GL_TEXTURE15。它们都是按顺序定义的,所以我们也可以通过GL_TEXTURE0 + 8 的方式获得 GL_TEXTURE8。
如果我要使用 Shader 绑定一个 vec2 数据,希望可以通过这样的代码实现:
shader->getUniformByName("valueName")->setValue(2, 2);
主要是 getUniformByName 和 setValue 函数的实现:
Uniform* Shader::getUniformByName(const char* name) { int location = glGetUniformLocation(program, name); assert(location != -1); auto it = mUniforms.find(location); if ( it != mUniforms.end() ) { return &it->second; } return &mUniforms.insert(std::make_pair(location, Uniform(location))).first->second; }
Shader 有一张 Uniform 数据表:
std::map<int, Uniform> mUniforms;
绑定点可以索引到 Uniform 数据,函数 getUniformByName 一开始根据 Uniform 名称查找其绑定点 location,然后通过 location 查找 Uniform 数据表,有就直接返回 Uniform,否则插入一个新的 Uniform 后返回。
接下来就是将值储存到 Uniform 对象中:
void setValue(int v0) { uniformType = UT_1I; fV0 = v0; } void setValue(float v0) { uniformType = UT_1F; fV0 = v0; } void setValue(int v0, int v1) { uniformType = UT_2I; fV0 = v0; fV1 = v1; } void setValue(float v0, float v1) { uniformType = UT_2F; fV0 = v0; fV1 = v1; } void setValue(int v0, int v1, int v2) { uniformType = UT_3I; fV0 = v0; fV1 = v1; fV2 = v2; } void setValue(float v0, float v1, float v2) { uniformType = UT_3F; fV0 = v0; fV1 = v1; fV2 = v2; } void setValue(int v0, int v1, int v2, int v3) { uniformType = UT_4I; fV0 = v0; fV1 = v1; fV2 = v2; fV3 = v3; } void setValue(float v0, float v1, float v2, float v3) { uniformType = UT_4F; fV0 = v0; fV1 = v1; fV2 = v2; fV3 = v3; } void setTexture(int v0) { uniformType = UT_TEXTURE; fV0 = v0; }
最后,在 Shader 的 bindUniform 函数实现 Shader 中所有 Uniform 的绑定:
void Shader::bindUniform() { int tex = 0; for ( auto& ele : mUniforms ) { if ( ele.second.bind(tex) ) { tex++; } } }
就是调用 Uniform 的 bind 函数而已。
Shader 简易封装到此结束,源码在完成重构渲染器后给出。