OpenGL入门——着色器类
着色器的编写、编译、管理是个很繁琐的事。所以就需要写一个类,这个类可以从文件读取着色器源码,可以编译链接它们,可以对它们进行错误检测,可以设置Uniform值。
1. 类的声明
#pragma once #include <glad/glad.h> #include <string> #include <fstream> #include <sstream> #include <iostream> //着色器对象类型 #define SHADER_TYPE_VERTEX 0 //顶点着色器对象 #define SHADER_TYPE_FRAGMENT 1 //片段着色器对象 #define SHADER_TYPE_PROGRAM 2 //着色器程序对象 class CShader { public: CShader(std::string vertex_path, std::string fragment_path); ~CShader(); //激活着色器程序 void run(); //设置uniform值 void setUniformBool(const std::string val_name, bool val); void setUniformInt(const std::string val_name, int val); void setUniformFloat(const std::string val_name, float val); private: //编译、链接着色器 void initialize(const char *vertexShaderSource, const char *fragmentShaderSource); //编译检测 void checkShader(unsigned int shader_id, int type); private: unsigned int ID; };
2. 类的定义
#include "shader.h" CShader::CShader(std::string vertex_path, std::string fragment_path) { // 1. 从文件路径中获取顶点/片段着色器 std::string vertexCode; std::string fragmentCode; std::ifstream vShaderFile; std::ifstream fShaderFile; // 保证ifstream对象可以抛出异常: vShaderFile.exceptions(std::ifstream::failbit | std::ifstream::badbit); fShaderFile.exceptions(std::ifstream::failbit | std::ifstream::badbit); try { // 打开文件 vShaderFile.open(vertex_path); fShaderFile.open(fragment_path); std::stringstream vShaderStream, fShaderStream; // 读取文件的缓冲内容到数据流中 vShaderStream << vShaderFile.rdbuf(); fShaderStream << fShaderFile.rdbuf(); // 关闭文件处理器 vShaderFile.close(); fShaderFile.close(); // 转换数据流到string vertexCode = vShaderStream.str(); fragmentCode = fShaderStream.str(); } catch (std::ifstream::failure e) { std::cout << "ERROR::SHADER::FILE_NOT_SUCCESFULLY_READ" << std::endl; } const char* vShaderCode = vertexCode.c_str(); const char* fShaderCode = fragmentCode.c_str(); initialize(vShaderCode, fShaderCode); } CShader::~CShader() { glDeleteProgram(ID); } void CShader::initialize(const char *vertexShaderSource, const char *fragmentShaderSource) { //创建一个顶点着色器对象 unsigned int vertexShader; vertexShader = glCreateShader(GL_VERTEX_SHADER); glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);//着色器源码附加到着色器对象上 glCompileShader(vertexShader);//编译源码 checkShader(vertexShader, SHADER_TYPE_VERTEX); //创建一个片段着色器对象,注意还是用ID来引用的 unsigned int fragmentShader; fragmentShader = glCreateShader(GL_FRAGMENT_SHADER); glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL); glCompileShader(fragmentShader);//编译源码 checkShader(fragmentShader, SHADER_TYPE_FRAGMENT); //创建一个着色器的程序 ID = glCreateProgram(); glAttachShader(ID, vertexShader);//把之前编译的着色器附加到程序对象上 glAttachShader(ID, fragmentShader); glLinkProgram(ID);//glLinkProgram链接它们 checkShader(ID, SHADER_TYPE_PROGRAM); //链接后即可删除 glDeleteShader(vertexShader); glDeleteShader(fragmentShader); } void CShader::checkShader(unsigned int shader_id, int type) { int success; char infoLog[512]; if (type != SHADER_TYPE_PROGRAM) { glGetShaderiv(shader_id, GL_COMPILE_STATUS, &success);//用glGetShaderiv检查是否编译成功 if (!success) { glGetShaderInfoLog(shader_id, 512, NULL, infoLog); std::cout << "ERROR::SHADER::COMPILATION_FAILED\n" << infoLog << std::endl; } } else { glGetProgramiv(shader_id, GL_COMPILE_STATUS, &success);//用glGetProgramiv检查是否编译成功 if (!success) { glGetShaderInfoLog(shader_id, 512, NULL, infoLog); std::cout << "ERROR::SHADER::PROGRAM::LINK_FAILED\n" << infoLog << std::endl; } } } void CShader::run() { glUseProgram(ID); } void CShader::setUniformBool(const std::string val_name, bool val) { glUniform1i(glGetUniformLocation(ID, val_name.c_str()), (int)val); } void CShader::setUniformInt(const std::string val_name, int val) { glUniform1i(glGetUniformLocation(ID, val_name.c_str()), val); } void CShader::setUniformFloat(const std::string val_name, float val) { glUniform1i(glGetUniformLocation(ID, val_name.c_str()), val); }
3. 类的使用
把着色器源码保存到文件中,后缀可以自定义,如下:
顶点着色器文件hello_triangle.vs
//vertex shader source #version 330 core layout(location = 0) in vec3 position; //位置变量的属性位置为0 layout(location = 1) in vec3 color; //颜色变量的属性位置为1 out vec3 vertexColor; //为片段着色器指定一个颜色输出 void main() { gl_Position = vec4(position, 1.0); //opengl顶点坐标 vertexColor = color; //从顶点数据那里得到的输入颜色 }
片段着色器文件hello_triangle.fs
//fragment shader source #version 330 core out vec4 fragColor; //像素的最终颜色 in vec3 vertexColor; //从顶点着色器传来的输入变量(名称、类型必须相同) //uniform vec3 ourColor;//程序中设定这个变量 void main() { fragColor = vec4(vertexColor, 1.0); }
应用程序中使用着色器类,只需要2步:定义着色器和激活程序对象
...//初始化窗口 ///定义着色器 CShader shader("hello_triangle.vs", "hello_triangle.fs"); ...//定义顶点对象及其属性 while (!glfwWindowShouldClose(window)) { processInput(window); //清空屏幕 glClearColor(0.2f, 0.3f, 0.3f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); ///绘制物体 shader.run();//激活程序对象 ... glfwPollEvents();//检查有没有触发什么事件 } //释放对象 ...
完整代码示例
//GLAD的头文件包含了正确的OpenGL头文件(例如GL/gl.h),所以需要在其它依赖于OpenGL的头文件之前包含GLAD #include <glad/glad.h> #include <GLFW/glfw3.h> #include <iostream> #include "shader.h" //改变窗口大小 void framebuffer_size_callback(GLFWwindow* window, int width, int height) { glViewport(0, 0, width, height); } //输入 void processInput(GLFWwindow *window) { if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)//点击ESC键退出绘制 glfwSetWindowShouldClose(window, true); } GLFWwindow* init_window() { ///窗口初始化 glfwInit(); glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);//主版本号,当API以不兼容的方式更改时,该值会增加。 glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);//次版本号,当特性被添加到API中时,它会增加,但是它保持向后兼容。 glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);//使用核心模式,不兼容已废弃函数 //创建glfw窗口 GLFWwindow* window = glfwCreateWindow(800, 600, "ping-window", NULL, NULL); if (window == NULL) { std::cout << "failed to create GLFW window" << std::endl; glfwTerminate();//释放/删除之前的分配的所有资源 return nullptr; } glfwMakeContextCurrent(window);//将窗口的上下文设置为当前线程的主上下文 glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);//注册为调整窗口回调函数 //GLAD是用来管理OpenGL的函数指针的,在调用任何OpenGL的函数之前初始化GLAD if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))//给GLAD传入了用来加载系统相关的OpenGL函数指针地址的函数 { std::cout << "failed to intialize GLAD" << std::endl; return nullptr; } glViewport(0, 0, 800, 600);//处理过的OpenGL坐标范围只为-1到1,因此我们事实上将(-1到1)范围内的坐标映射到(0, 800)和(0, 600) return window; } int hello_triangle() { GLFWwindow* window = init_window(); ///定义着色器 CShader shader("hello_triangle.vs", "hello_triangle.fs"); ///定义顶点对象 float vertices[] = { // 位置 // 颜色 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, // 右下 -0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, // 左下 0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f // 顶部 }; //生成VAO对象,缓冲ID为VAO unsigned int VAO; glGenVertexArrays(1, &VAO); glBindVertexArray(VAO);//绑定VAO,从绑定之后起,我们应该绑定和配置对应的VBO和属性指针,之后解绑VAO,供之后使用 //生成VBO对象,缓冲ID为VBO unsigned int VBO; glGenBuffers(1, &VBO);//第一个参数GLsizei是要生成的缓冲对象的数量,第二个GLuint是要输入用来存储缓冲对象名称的数组 //绑定到目标对象,VBO变成了一个顶点缓冲类型 glBindBuffer(GL_ARRAY_BUFFER, VBO);//第一个就是缓冲对象的类型,第二个参数就是要绑定的缓冲对象的名称 glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);//数据传入缓冲内存中,GL_STATIC_DRAW:数据不会或几乎不会改变; GL_DYNAMIC_DRAW:数据会被改变很多; GL_DYNAMIC_DRAW:数据会被改变很多 //设置顶点属性指针,如何解析顶点数据 /* 第一个参数指定我们要配置的顶点属性,顶点着色器中使用layout(location = 0)定义 第二个参数指定顶点属性的大小 第三个参数指定数据的类型 第四个参数定义我们是否希望数据被标准化(Normalize)。如果我们设置为GL_TRUE,所有数据都会被映射到0(对于有符号型signed数据是-1)到1之间 第五个参数步长(Stride),它告诉我们在连续的顶点属性组之间的间隔 最后一个参数的类型是void*,数据在缓冲中起始位置的偏移量(Offset) */ glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0); glEnableVertexAttribArray(0);//启用顶点属性layout(location = 0),顶点属性默认是禁用的 glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3 * sizeof(float))); glEnableVertexAttribArray(1);//启用顶点属性layout(location = 1),顶点属性默认是禁用的 glBindBuffer(GL_ARRAY_BUFFER, 0);//设置完属性,解绑VBO glBindVertexArray(0);//配置完VBO及其属性,解绑VAO //绘制模式为线条GL_LINE,填充面GL_FILL //glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);//正反面 while (!glfwWindowShouldClose(window)) { processInput(window); //清空屏幕 glClearColor(0.2f, 0.3f, 0.3f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); ///绘制物体 shader.run(); glBindVertexArray(VAO); //使用VAO绘制 glDrawArrays(GL_TRIANGLES, 0, 3);//绘制图元为三角形,起始索引0,绘制顶点数量3 glfwSwapBuffers(window);//交换颜色缓冲(它是一个储存着GLFW窗口每一个像素颜色值的大缓冲) glfwPollEvents();//检查有没有触发什么事件 } //释放对象 glDeleteVertexArrays(1, &VAO); glDeleteBuffers(1, &VBO); std::cout << "finish!" << std::endl; glfwTerminate();//释放/删除之前的分配的所有资源 return 0; } int main() { hello_triangle(); return 0; }
运行结果