- https://learnopengl-cn.github.io/01%20Getting%20started/05%20Shaders/
- 着色器
- 是使用一种叫GLSL的类C语言写成的。GLSL是为图形计算量身定制的,它包含一些针对向量和矩阵操作的有用特性。
- 数据类型
- 和其他编程语言一样,GLSL有数据类型可以来指定变量的种类。GLSL中包含C等其它语言大部分的默认基础数据类型:
int
、float
、double
、uint
和bool
。GLSL也有两种容器类型,它们会在这个教程中使用很多,分别是向量(Vector)和矩阵(Matrix)。
- 和其他编程语言一样,GLSL有数据类型可以来指定变量的种类。GLSL中包含C等其它语言大部分的默认基础数据类型:
- 输入与输出
-
- 虽然着色器是各自独立的小程序,但是它们都是一个整体的一部分,出于这样的原因,我们希望每个着色器都有输入和输出,这样才能进行数据交流和传递。GLSL定义了
in
和out
关键字专门来实现这个目的。每个着色器使用这两个关键字设定输入和输出,只要一个输出变量与下一个着色器阶段的输入匹配,它就会传递下去。但在顶点和片段着色器中会有点不同。 - 如果我们打算从一个着色器向另一个着色器发送数据,我们必须在发送方着色器中声明一个输出,在接收方着色器中声明一个类似的输入。当类型和名字都一样的时候,OpenGL就会把两个变量链接到一起,它们之间就能发送数据了(这是在链接程序对象时完成的)。为了展示这是如何工作的,我们让顶点着色器为片段着色器决定颜色。
- 顶点着色器
#version 330 core layout (location = 0) in vec3 aPos; // 位置变量的属性位置值为0 out vec4 vertexColor; // 为片段着色器指定一个颜色输出 void main() { gl_Position = vec4(aPos, 1.0); // 注意我们如何把一个vec3作为vec4的构造器的参数 vertexColor = vec4(0.5, 0.0, 0.0, 1.0); // 把输出变量设置为暗红色 }
- 片段着色器
#version 330 core out vec4 FragColor; in vec4 vertexColor; // 从顶点着色器传来的输入变量(名称相同、类型相同) void main() { FragColor = vertexColor; }
- 完整代码
-
//暗红三角形 #include "pch.h" //GLEW #include <iostream> #define GLEW_STATIC #include <GL/glew.h> #include <GLFW/glfw3.h> #include "SOIL2/SOIL2.h" #include "glm/glm.hpp" #include "glm/gtc/type_ptr.hpp" #include "Shader.h" void processInput(GLFWwindow *window); //顶点着色器源码 //顶点着色器主要的目的是把3D坐标转为另一种3D坐标(后面会解释), //同时顶点着色器允许我们对顶点属性进行一些基本处理。 const char *vertexShaderSource = "#version 330 core\n" "layout (location = 0) in vec3 aPos;\n" "out vec4 vertexColor;\n" "void main()\n" "{\n" " gl_Position = vec4(aPos, 1.0);\n" " vertexColor = vec4(0.5, 0.0, 0.0, 1.0);" "}\0"; //片段着色器的主要目的是计算一个像素的最终颜色,这也是所有OpenGL高级效果产生的地方。 //通常,片段着色器包含3D场景的数据(比如光照、阴影、光的颜色等等),这些数据可以被用来计算最终像素的颜色。 //对象着色器源码 const char *fragmentShaderSource = "#version 330 core\n" "out vec4 FragColor;\n" "in vec4 vertexColor;\n" "void main()\n" "{\n" " FragColor = vertexColor;\n" "}\n\0"; int main() { //初始化GLFW glfwInit(); //使用flfwWindowHint函数来配置GLFW /* glfwWindowHint函数的第一个参数代表选项的名称, 我们可以从很多以GLFW_开头的枚举值中选择; 第二个参数接受一个整型, 用来设置这个选项的值 */ glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); //glfwCreateWindow函数需要窗口的宽和高作为它的前两个参数 //第三个参数表示这个窗口的名称 GLFWwindow* window = glfwCreateWindow(800, 600, "B7040312", NULL, NULL); if (window == NULL) { std::cout << "Failed to create GLFW window" << std::endl; glfwTerminate(); return -1; } //创建完窗口我们就可以通知GLFW将我们窗口的上下文设置为当前线程的主上下文了 glfwMakeContextCurrent(window); //GLEW是用来管理OpenGL的函数指针的,所以在调用任何OpenGL的函数之前我们需要初始化GLEW //glewExperimental所做的是即使驱动程序的扩展程序字符串中不存在扩展程序,也允许加载扩展程序入口点。 glewExperimental = GL_TRUE; if (glewInit() != GLEW_OK) { std::cout << "Failed to initialize GLEW" << std::endl; return -1; } /* 开始渲染之前还有一件重要的事情要做,我们必须告诉OpenGL渲染窗口的尺寸大小,这样OpenGL才只能知道怎样相对于窗口大小显示数据和坐标。 我们可以通过调用glViewport函数来设置窗口的维度(Dimension): */ /* 然而,当用户改变窗口的大小的时候,视口也应该被调整。 我们可以对窗口注册一个回调函数(Callback Function), 它会在每次窗口大小被调整的时候被调用 */ int width, height; glfwGetFramebufferSize(window, &width, &height); /* glViewport函数前两个参数控制窗口左下角的位置。 第三个和第四个参数控制渲染窗口的宽度和高度(像素),这里我们是直接从GLFW中获取的。 */ glViewport(0, 0, width, height); //创建着色器对象 创建类型 附加着色器源码 编译 // build and compile our shader program // ------------------------------------ // vertex shader(顶点着色器) int vertexShader = glCreateShader(GL_VERTEX_SHADER); glShaderSource(vertexShader, 1, &vertexShaderSource, NULL); glCompileShader(vertexShader); // check for shader compile errors int success; char infoLog[512]; glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success); if (!success) { glGetShaderInfoLog(vertexShader, 512, NULL, infoLog); std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl; } // fragment shader(片段着色器) int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER); glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL); glCompileShader(fragmentShader); // check for shader compile errors glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success); if (!success) { glGetShaderInfoLog(fragmentShader, 512, NULL, infoLog); std::cout << "ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n" << infoLog << std::endl; } // link shaders int shaderProgram = glCreateProgram(); glAttachShader(shaderProgram, vertexShader); glAttachShader(shaderProgram, fragmentShader); glLinkProgram(shaderProgram); // check for linking errors glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success); if (!success) { glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog); std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog << std::endl; } glDeleteShader(vertexShader); glDeleteShader(fragmentShader); /* 由于我们希望渲染一个三角形,我们一共要指定三个顶点,每个顶点都有一个3D位置。 我们会将它们以标准化设备坐标的形式(OpenGL的可见区域)定义为一个float数组。 由于OpenGL是在3D空间中工作的,而我们渲染的是一个2D三角形,我们将它顶点的z坐标设置为0.0。 这样子的话三角形每一点的深度(Depth,译注2)都是一样的,从而使它看上去像是2D的。 */ float vertices[] = { -0.5f, -0.5f, 0.0f, // left 0.5f, -0.5f, 0.0f, // right 0.0f, 0.5f, 0.0f // top }; /* 定义这样的顶点数据以后,我们会把它作为输入发送给图形渲染管线的第一个处理阶段:顶点着色器。 它会在GPU上创建内存用于储存我们的顶点数据,还要配置OpenGL如何解释这些内存,并且指定其如何发送给显卡。 顶点着色器接着会处理我们在内存中指定数量的顶点。 我们通过顶点缓冲对象(Vertex Buffer Objects, VBO)管理这个内存, 它会在GPU内存(通常被称为显存)中储存大量顶点。 使用这些缓冲对象的好处是我们可以一次性的发送一大批数据到显卡上,而不是每个顶点发送一次。 从CPU把数据发送到显卡相对较慢,所以只要可能我们都要尝试尽量一次性发送尽可能多的数据。 当数据发送至显卡的内存中后,顶点着色器几乎能立即访问顶点,这是个非常快的过程。 */ //顶点缓冲对象 //顶点数组对象 unsigned int VBO, VAO; //以使用glGenVertexArrays函数和一个缓冲ID生成一个VAO对象: glGenVertexArrays(1, &VAO); //VBO: //以使用glGenBuffers函数和一个缓冲ID生成一个VBO对象: //OpenGL有很多缓冲对象类型,顶点缓冲对象的缓冲类型是GL_ARRAY_BUFFER。 //OpenGL允许我们同时绑定多个缓冲,只要它们是不同的缓冲类型。 //我们可以使用glBindBuffer函数把新创建的缓冲绑定到GL_ARRAY_BUFFER目标上: //从这一刻起,我们使用的任何(在GL_ARRAY_BUFFER目标上的)缓冲调用 //都会用来配置当前绑定的缓冲(VBO)。 glGenBuffers(1, &VBO); //VAO:首先绑定顶点数组对象 glBindVertexArray(VAO); glBindBuffer(GL_ARRAY_BUFFER, VBO); //VAO:然后把顶点数组复制到缓冲中供OpenGL使用 //VBO:然后我们可以调用glBufferData函数,它会把之前定义的顶点数据复制到缓冲的内存中: glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); //VAO:最后设置顶点属性指针 //对glVertexAttribPointer的调用将VBO注册为顶点属性的绑定顶点缓冲区对象,因此之后我们可以安全地解除绑定 //链接顶点属性 //每个顶点属性从一个VBO管理的内存中获得它的数据,而具体是从哪个VBO(程序中可以有多个VBO) //获取则是通过在调用glVertexAttribPointer时绑定到GL_ARRAY_BUFFER的VBO决定的。 //由于在调用glVertexAttribPointer之前绑定的是先前定义的VBO对象,顶点属性0现在会链接到它的顶点数据。 glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); //我们现在应该使用glEnableVertexAttribArray,以顶点属性位置值作为参数,启用顶点属性;顶点属性默认是禁用的 glEnableVertexAttribArray(0); glBindBuffer(GL_ARRAY_BUFFER, 0); //之后您可以解除绑定VAO,这样其他VAO调用就不会意外修改此VAO,但这很少发生。 修改其他 // VAO无论如何都需要调用glBindVertexArray,因此通常在不需要时,我们通常不会取消绑定VAO(也没有VBO)。 glBindVertexArray(0); /* 一般当你打算绘制多个物体时,你首先要生成/配置所有的VAO(和必须的VBO及属性指针), 然后储存它们供后面使用。 当我们打算绘制物体的时候就拿出相应的VAO,绑定它,绘制完物体后,再解绑VAO。 */ /* 我们希望程序在我们主动关闭它之前不断绘制图像并能够接受用户输入。 因此,我们需要在程序中添加一个while循环,我们可以把它称之为渲染循环(Render Loop), 它能在我们让GLFW退出前一直保持运行。 下面几行的代码就实现了一个简单的渲染循环: */ /* glfwWindowShouldClose函数在我们每次循环的开始前检查一次GLFW是否被要求退出, 如果是的话该函数返回true然后渲染循环便结束了,之后为我们就可以关闭应用程序了。 */ while (!glfwWindowShouldClose(window)) { // input // ----- processInput(window); // render // ------ //要把所有的渲染(Rendering)操作放到渲染循环中, //因为我们想让这些渲染指令在每次渲染循环迭代的时候都能被执行。 glClearColor(0.2f, 0.3f, 0.3f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); // draw our first triangle glUseProgram(shaderProgram); glBindVertexArray(VAO); //我们只有一个VAO,因此不必每次都绑定它,但是我们这样做是为了使事情更有条理 //glDrawArrays函数,它使用当前激活的着色器,之前定义的顶点属性配置,和VBO的顶点数据(通过VAO间接绑定)来绘制图元。 glDrawArrays(GL_TRIANGLES, 0, 3); /* glfwSwapBuffers函数会交换颜色缓冲(它是一个储存着GLFW窗口每一个像素颜色值的大缓冲), 它在这一迭代中被用来绘制,并且将会作为输出显示在屏幕上。 */ /* glfwPollEvents函数检查有没有触发什么事件(比如键盘输入、鼠标移动等)、更新窗口状态, 并调用对应的回调函数(可以通过回调方法手动设置) */ glfwSwapBuffers(window); glfwPollEvents(); } //delete glDeleteVertexArrays(1, &VAO); glDeleteBuffers(1, &VBO); /* 当渲染循环结束后我们需要正确释放/删除之前的分配的所有资源。 我们可以在main函数的最后调用glfwTerminate函数来完成。 */ glfwTerminate(); return 0; } void processInput(GLFWwindow *window) { //检测是否按下esc,按下则退出循环 if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS) glfwSetWindowShouldClose(window, true); }
- 虽然着色器是各自独立的小程序,但是它们都是一个整体的一部分,出于这样的原因,我们希望每个着色器都有输入和输出,这样才能进行数据交流和传递。GLSL定义了
-
- Uniform
- Uniform是一种从CPU中的应用向GPU中的着色器发送数据的方式,但uniform和顶点属性有些不同。首先,uniform是全局的(Global)。全局意味着uniform变量必须在每个着色器程序对象中都是独一无二的,而且它可以被着色器程序的任意着色器在任意阶段访问。第二,无论你把uniform值设置成什么,uniform会一直保存它们的数据,直到它们被重置或更新。
- 我们可以在一个着色器中添加
uniform
关键字至类型和变量名前来声明一个GLSL的uniform。从此处开始我们就可以在着色器中使用新声明的uniform了。我们来看看这次是否能通过uniform设置三角形的颜色: -
#version 330 core out vec4 FragColor; uniform vec4 ourColor; // 在OpenGL程序代码中设定这个变量 void main() { FragColor = ourColor; }
- 我们在片段着色器中声明了一个uniform
vec4
的ourColor,并把片段着色器的输出颜色设置为uniform值的内容。因为uniform是全局变量,我们可以在任何着色器中定义它们,而无需通过顶点着色器作为中介。顶点着色器中不需要这个uniform,所以我们不用在那里定义它。 - 如果你声明了一个uniform却在GLSL代码中没用过,编译器会静默移除这个变量,导致最后编译出的版本中并不会包含它,这可能导致几个非常麻烦的错误,记住这点!
- 这个uniform现在还是空的;我们还没有给它添加任何数据,所以下面我们就做这件事。我们首先需要找到着色器中uniform属性的索引/位置值。当我们得到uniform的索引/位置值后,我们就可以更新它的值了。这次我们不去给像素传递单独一个颜色,而是让它随着时间改变颜色:
float timeValue = glfwGetTime(); float greenValue = (sin(timeValue) / 2.0f) + 0.5f; int vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor"); glUseProgram(shaderProgram); glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f);
- 首先我们通过glfwGetTime()获取运行的秒数。然后我们使用sin函数让颜色在0.0到1.0之间改变,最后将结果储存到greenValue里。
- 接着,我们用glGetUniformLocation查询uniform ourColor的位置值。我们为查询函数提供着色器程序和uniform的名字(这是我们希望获得的位置值的来源)。如果glGetUniformLocation返回
-1
就代表没有找到这个位置值。最后,我们可以通过glUniform4f函数设置uniform值。注意,查询uniform地址不要求你之前使用过着色器程序,但是更新一个uniform之前你必须先使用程序(调用glUseProgram),因为它是在当前激活的着色器程序中设置uniform的。 - 现在你知道如何设置uniform变量的值了,我们可以使用它们来渲染了。如果我们打算让颜色慢慢变化,我们就要在游戏循环的每一次迭代中(所以他会逐帧改变)更新这个uniform,否则三角形就不会改变颜色。下面我们就计算greenValue然后每个渲染迭代都更新这个uniform:
while(!glfwWindowShouldClose(window)) { // 输入 processInput(window); // 渲染 // 清除颜色缓冲 glClearColor(0.2f, 0.3f, 0.3f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); // 记得激活着色器 glUseProgram(shaderProgram); // 更新uniform颜色 float timeValue = glfwGetTime(); float greenValue = sin(timeValue) / 2.0f + 0.5f; int vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor"); glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f); // 绘制三角形 glBindVertexArray(VAO); glDrawArrays(GL_TRIANGLES, 0, 3); // 交换缓冲并查询IO事件 glfwSwapBuffers(window); glfwPollEvents(); }
这里的代码对之前代码是一次非常直接的修改。这次,我们在每次迭代绘制三角形前先更新uniform值。如果你正确更新了uniform,你会看到你的三角形逐渐由绿变黑再变回绿色。
- 完整代码
-
//渐变 #include "pch.h" //GLEW #include <iostream> #define GLEW_STATIC #include <GL/glew.h> #include <GLFW/glfw3.h> #include "SOIL2/SOIL2.h" #include "glm/glm.hpp" #include "glm/gtc/type_ptr.hpp" #include "Shader.h" void processInput(GLFWwindow *window); //顶点着色器源码 //顶点着色器主要的目的是把3D坐标转为另一种3D坐标(后面会解释), //同时顶点着色器允许我们对顶点属性进行一些基本处理。 const char *vertexShaderSource = "#version 330 core\n" "layout (location = 0) in vec3 aPos;\n" "void main()\n" "{\n" " gl_Position = vec4(aPos, 1.0);\n" "}\0"; //片段着色器的主要目的是计算一个像素的最终颜色,这也是所有OpenGL高级效果产生的地方。 //通常,片段着色器包含3D场景的数据(比如光照、阴影、光的颜色等等),这些数据可以被用来计算最终像素的颜色。 //对象着色器源码 const char *fragmentShaderSource = "#version 330 core\n" "out vec4 FragColor;\n" "uniform vec4 ourColor;\n" "void main()\n" "{\n" " FragColor = ourColor;\n" "}\n\0"; int main() { //初始化GLFW glfwInit(); //使用flfwWindowHint函数来配置GLFW /* glfwWindowHint函数的第一个参数代表选项的名称, 我们可以从很多以GLFW_开头的枚举值中选择; 第二个参数接受一个整型, 用来设置这个选项的值 */ glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); //glfwCreateWindow函数需要窗口的宽和高作为它的前两个参数 //第三个参数表示这个窗口的名称 GLFWwindow* window = glfwCreateWindow(800, 600, "B7040312", NULL, NULL); if (window == NULL) { std::cout << "Failed to create GLFW window" << std::endl; glfwTerminate(); return -1; } //创建完窗口我们就可以通知GLFW将我们窗口的上下文设置为当前线程的主上下文了 glfwMakeContextCurrent(window); //GLEW是用来管理OpenGL的函数指针的,所以在调用任何OpenGL的函数之前我们需要初始化GLEW //glewExperimental所做的是即使驱动程序的扩展程序字符串中不存在扩展程序,也允许加载扩展程序入口点。 glewExperimental = GL_TRUE; if (glewInit() != GLEW_OK) { std::cout << "Failed to initialize GLEW" << std::endl; return -1; } /* 开始渲染之前还有一件重要的事情要做,我们必须告诉OpenGL渲染窗口的尺寸大小,这样OpenGL才只能知道怎样相对于窗口大小显示数据和坐标。 我们可以通过调用glViewport函数来设置窗口的维度(Dimension): */ /* 然而,当用户改变窗口的大小的时候,视口也应该被调整。 我们可以对窗口注册一个回调函数(Callback Function), 它会在每次窗口大小被调整的时候被调用 */ int width, height; glfwGetFramebufferSize(window, &width, &height); /* glViewport函数前两个参数控制窗口左下角的位置。 第三个和第四个参数控制渲染窗口的宽度和高度(像素),这里我们是直接从GLFW中获取的。 */ glViewport(0, 0, width, height); //创建着色器对象 创建类型 附加着色器源码 编译 // build and compile our shader program // ------------------------------------ // vertex shader int vertexShader = glCreateShader(GL_VERTEX_SHADER); glShaderSource(vertexShader, 1, &vertexShaderSource, NULL); glCompileShader(vertexShader); // check for shader compile errors int success; char infoLog[512]; glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success); if (!success) { glGetShaderInfoLog(vertexShader, 512, NULL, infoLog); std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl; } // fragment shader int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER); glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL); glCompileShader(fragmentShader); // check for shader compile errors glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success); if (!success) { glGetShaderInfoLog(fragmentShader, 512, NULL, infoLog); std::cout << "ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n" << infoLog << std::endl; } // link shaders int shaderProgram = glCreateProgram(); glAttachShader(shaderProgram, vertexShader); glAttachShader(shaderProgram, fragmentShader); glLinkProgram(shaderProgram); // check for linking errors glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success); if (!success) { glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog); std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog << std::endl; } glDeleteShader(vertexShader); glDeleteShader(fragmentShader); /* 由于我们希望渲染一个三角形,我们一共要指定三个顶点,每个顶点都有一个3D位置。 我们会将它们以标准化设备坐标的形式(OpenGL的可见区域)定义为一个float数组。 由于OpenGL是在3D空间中工作的,而我们渲染的是一个2D三角形,我们将它顶点的z坐标设置为0.0。 这样子的话三角形每一点的深度(Depth,译注2)都是一样的,从而使它看上去像是2D的。 */ float vertices[] = { 0.5f, -0.5f, 0.0f, // bottom right -0.5f, -0.5f, 0.0f, // bottom left 0.0f, 0.5f, 0.0f // top }; unsigned int VBO, VAO; glGenVertexArrays(1, &VAO); glGenBuffers(1, &VBO); // bind the Vertex Array Object first, then bind and set vertex buffer(s), and then configure vertex attributes(s). glBindVertexArray(VAO); glBindBuffer(GL_ARRAY_BUFFER, VBO); glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); glEnableVertexAttribArray(0); // You can unbind the VAO afterwards so other VAO calls won't accidentally modify this VAO, but this rarely happens. Modifying other // VAOs requires a call to glBindVertexArray anyways so we generally don't unbind VAOs (nor VBOs) when it's not directly necessary. // glBindVertexArray(0); // bind the VAO (it was already bound, but just to demonstrate): seeing as we only have a single VAO we can // just bind it beforehand before rendering the respective triangle; this is another approach. glBindVertexArray(VAO); // render loop // ----------- while (!glfwWindowShouldClose(window)) { // 输入 processInput(window); // 渲染 // 清除颜色缓冲 glClearColor(0.2f, 0.3f, 0.3f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); // 记得激活着色器 glUseProgram(shaderProgram); // 更新uniform颜色 float timeValue = glfwGetTime(); float greenValue = sin(timeValue) / 2.0f + 0.5f; int vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor"); glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f); // 绘制三角形 glBindVertexArray(VAO); glDrawArrays(GL_TRIANGLES, 0, 3); // 交换缓冲并查询IO事件 glfwSwapBuffers(window); glfwPollEvents(); } // optional: de-allocate all resources once they've outlived their purpose: // ------------------------------------------------------------------------ glDeleteVertexArrays(1, &VAO); glDeleteBuffers(1, &VBO); /* 当渲染循环结束后我们需要正确释放/删除之前的分配的所有资源。 我们可以在main函数的最后调用glfwTerminate函数来完成。 */ glfwTerminate(); return 0; } void processInput(GLFWwindow *window) { //检测是否按下esc,按下则退出循环 if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS) glfwSetWindowShouldClose(window, true); }
- https://learnopengl-cn.github.io/img/01/05/shaders.mp4
- 更多属性
-
- 在前面的教程中,我们了解了如何填充VBO、配置顶点属性指针以及如何把它们都储存到一个VAO里。这次,我们同样打算把颜色数据加进顶点数据中。我们将把颜色数据添加为3个float值至vertices数组。我们将把三角形的三个角分别指定为红色、绿色和蓝色:
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 // 顶部 };
- 由于现在有更多的数据要发送到顶点着色器,我们有必要去调整一下顶点着色器,使它能够接收颜色值作为一个顶点属性输入。需要注意的是我们用
layout
标识符来把aColor属性的位置值设置为1:#version 330 core layout (location = 0) in vec3 aPos; // 位置变量的属性位置值为 0 layout (location = 1) in vec3 aColor; // 颜色变量的属性位置值为 1 out vec3 ourColor; // 向片段着色器输出一个颜色 void main() { gl_Position = vec4(aPos, 1.0); ourColor = aColor; // 将ourColor设置为我们从顶点数据那里得到的输入颜色 }
- 由于我们不再使用uniform来传递片段的颜色了,现在使用
ourColor
输出变量,我们必须再修改一下片段着色器:#version 330 core out vec4 FragColor; in vec3 ourColor; void main() { FragColor = vec4(ourColor, 1.0); }
- 因为我们添加了另一个顶点属性,并且更新了VBO的内存,我们就必须重新配置顶点属性指针。更新后的VBO内存中的数据现在看起来像这样:
- https://learnopengl-cn.github.io/img/01/05/vertex_attribute_pointer_interleaved.png
- 知道了现在使用的布局,我们就可以使用glVertexAttribPointer函数更新顶点格式,
// 位置属性 glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0); glEnableVertexAttribArray(0); // 颜色属性 glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3* sizeof(float))); glEnableVertexAttribArray(1);
- glVertexAttribPointer函数的前几个参数比较明了。这次我们配置属性位置值为1的顶点属性。颜色值有3个float那么大,我们不去标准化这些值。
- 由于我们现在有了两个顶点属性,我们不得不重新计算步长值。为获得数据队列中下一个属性值(比如位置向量的下个
x
分量)我们必须向右移动6个float,其中3个是位置值,另外3个是颜色值。这使我们的步长值为6乘以float的字节数(=24字节)。 - 同样,这次我们必须指定一个偏移量。对于每个顶点来说,位置顶点属性在前,所以它的偏移量是0。颜色属性紧随位置数据之后,所以偏移量就是
3 * sizeof(float)
,用字节来计算就是12字节。 - 完整代码
-
//调色盘三角形 #include "pch.h" //GLEW #include <iostream> #define GLEW_STATIC #include <GL/glew.h> #include <GLFW/glfw3.h> #include "SOIL2/SOIL2.h" #include "glm/glm.hpp" #include "glm/gtc/type_ptr.hpp" #include "Shader.h" void processInput(GLFWwindow *window); //顶点着色器源码 //顶点着色器主要的目的是把3D坐标转为另一种3D坐标(后面会解释), //同时顶点着色器允许我们对顶点属性进行一些基本处理。 const char *vertexShaderSource ="#version 330 core\n" "layout (location = 0) in vec3 aPos;\n" "layout (location = 1) in vec3 aColor;\n" "out vec3 ourColor;\n" "void main()\n" "{\n" " gl_Position = vec4(aPos, 1.0);\n" " ourColor = aColor;\n" "}\0"; const char *fragmentShaderSource = "#version 330 core\n" "out vec4 FragColor;\n" "in vec3 ourColor;\n" "void main()\n" "{\n" " FragColor = vec4(ourColor, 1.0f);\n" "}\n\0"; int main() { //初始化GLFW glfwInit(); //使用flfwWindowHint函数来配置GLFW /* glfwWindowHint函数的第一个参数代表选项的名称, 我们可以从很多以GLFW_开头的枚举值中选择; 第二个参数接受一个整型, 用来设置这个选项的值 */ glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); //glfwCreateWindow函数需要窗口的宽和高作为它的前两个参数 //第三个参数表示这个窗口的名称 GLFWwindow* window = glfwCreateWindow(800, 600, "B7040312", NULL, NULL); if (window == NULL) { std::cout << "Failed to create GLFW window" << std::endl; glfwTerminate(); return -1; } //创建完窗口我们就可以通知GLFW将我们窗口的上下文设置为当前线程的主上下文了 glfwMakeContextCurrent(window); //GLEW是用来管理OpenGL的函数指针的,所以在调用任何OpenGL的函数之前我们需要初始化GLEW //glewExperimental所做的是即使驱动程序的扩展程序字符串中不存在扩展程序,也允许加载扩展程序入口点。 glewExperimental = GL_TRUE; if (glewInit() != GLEW_OK) { std::cout << "Failed to initialize GLEW" << std::endl; return -1; } /* 开始渲染之前还有一件重要的事情要做,我们必须告诉OpenGL渲染窗口的尺寸大小,这样OpenGL才只能知道怎样相对于窗口大小显示数据和坐标。 我们可以通过调用glViewport函数来设置窗口的维度(Dimension): */ /* 然而,当用户改变窗口的大小的时候,视口也应该被调整。 我们可以对窗口注册一个回调函数(Callback Function), 它会在每次窗口大小被调整的时候被调用 */ int width, height; glfwGetFramebufferSize(window, &width, &height); /* glViewport函数前两个参数控制窗口左下角的位置。 第三个和第四个参数控制渲染窗口的宽度和高度(像素),这里我们是直接从GLFW中获取的。 */ glViewport(0, 0, width, height); //创建着色器对象 创建类型 附加着色器源码 编译 // build and compile our shader program // ------------------------------------ // vertex shader(顶点着色器) int vertexShader = glCreateShader(GL_VERTEX_SHADER); glShaderSource(vertexShader, 1, &vertexShaderSource, NULL); glCompileShader(vertexShader); // check for shader compile errors int success; char infoLog[512]; glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success); if (!success) { glGetShaderInfoLog(vertexShader, 512, NULL, infoLog); std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl; } // fragment shader(片段着色器) int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER); glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL); glCompileShader(fragmentShader); // check for shader compile errors glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success); if (!success) { glGetShaderInfoLog(fragmentShader, 512, NULL, infoLog); std::cout << "ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n" << infoLog << std::endl; } // link shaders int shaderProgram = glCreateProgram(); glAttachShader(shaderProgram, vertexShader); glAttachShader(shaderProgram, fragmentShader); glLinkProgram(shaderProgram); // check for linking errors glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success); if (!success) { glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog); std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog << std::endl; } glDeleteShader(vertexShader); glDeleteShader(fragmentShader); /* 由于我们希望渲染一个三角形,我们一共要指定三个顶点,每个顶点都有一个3D位置。 我们会将它们以标准化设备坐标的形式(OpenGL的可见区域)定义为一个float数组。 由于OpenGL是在3D空间中工作的,而我们渲染的是一个2D三角形,我们将它顶点的z坐标设置为0.0。 这样子的话三角形每一点的深度(Depth,译注2)都是一样的,从而使它看上去像是2D的。 */ float vertices[] = { // positions // colors 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, // bottom right -0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, // bottom left 0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f // top }; unsigned int VBO, VAO; glGenVertexArrays(1, &VAO); glGenBuffers(1, &VBO); // bind the Vertex Array Object first, then bind and set vertex buffer(s), and then configure vertex attributes(s). glBindVertexArray(VAO); glBindBuffer(GL_ARRAY_BUFFER, VBO); glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); // position attribute glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0); glEnableVertexAttribArray(0); // color attribute glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3 * sizeof(float))); glEnableVertexAttribArray(1); glBindBuffer(GL_ARRAY_BUFFER, 0); //之后您可以解除绑定VAO,这样其他VAO调用就不会意外修改此VAO,但这很少发生。 修改其他 // VAO无论如何都需要调用glBindVertexArray,因此通常在不需要时,我们通常不会取消绑定VAO(也没有VBO)。 glBindVertexArray(0); /* 一般当你打算绘制多个物体时,你首先要生成/配置所有的VAO(和必须的VBO及属性指针), 然后储存它们供后面使用。 当我们打算绘制物体的时候就拿出相应的VAO,绑定它,绘制完物体后,再解绑VAO。 */ /* 我们希望程序在我们主动关闭它之前不断绘制图像并能够接受用户输入。 因此,我们需要在程序中添加一个while循环,我们可以把它称之为渲染循环(Render Loop), 它能在我们让GLFW退出前一直保持运行。 下面几行的代码就实现了一个简单的渲染循环: */ /* glfwWindowShouldClose函数在我们每次循环的开始前检查一次GLFW是否被要求退出, 如果是的话该函数返回true然后渲染循环便结束了,之后为我们就可以关闭应用程序了。 */ while (!glfwWindowShouldClose(window)) { // input // ----- processInput(window); // render // ------ //要把所有的渲染(Rendering)操作放到渲染循环中, //因为我们想让这些渲染指令在每次渲染循环迭代的时候都能被执行。 glClearColor(0.2f, 0.3f, 0.3f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); // draw our first triangle glUseProgram(shaderProgram); glBindVertexArray(VAO); //我们只有一个VAO,因此不必每次都绑定它,但是我们这样做是为了使事情更有条理 //glDrawArrays函数,它使用当前激活的着色器,之前定义的顶点属性配置,和VBO的顶点数据(通过VAO间接绑定)来绘制图元。 glDrawArrays(GL_TRIANGLES, 0, 3); /* glfwSwapBuffers函数会交换颜色缓冲(它是一个储存着GLFW窗口每一个像素颜色值的大缓冲), 它在这一迭代中被用来绘制,并且将会作为输出显示在屏幕上。 */ /* glfwPollEvents函数检查有没有触发什么事件(比如键盘输入、鼠标移动等)、更新窗口状态, 并调用对应的回调函数(可以通过回调方法手动设置) */ glfwSwapBuffers(window); glfwPollEvents(); } //delete glDeleteVertexArrays(1, &VAO); glDeleteBuffers(1, &VBO); /* 当渲染循环结束后我们需要正确释放/删除之前的分配的所有资源。 我们可以在main函数的最后调用glfwTerminate函数来完成。 */ glfwTerminate(); return 0; } void processInput(GLFWwindow *window) { //检测是否按下esc,按下则退出循环 if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS) glfwSetWindowShouldClose(window, true); }
- 在前面的教程中,我们了解了如何填充VBO、配置顶点属性指针以及如何把它们都储存到一个VAO里。这次,我们同样打算把颜色数据加进顶点数据中。我们将把颜色数据添加为3个float值至vertices数组。我们将把三角形的三个角分别指定为红色、绿色和蓝色:
- 运行程序你应该会看到如下结果:
- https://learnopengl-cn.github.io/img/01/05/shaders3.png
-
- 着色器类
-
-
- 编写、编译、管理着色器是件麻烦事。在着色器主题的最后,我们会写一个类来让我们的生活轻松一点,它可以从硬盘读取着色器,然后编译并链接它们,并对它们进行错误检测,这就变得很好用了。这也会让你了解该如何封装目前所学的知识到一个抽象对象中。
- 我们会把着色器类全部放在在头文件里,主要是为了学习用途,当然也方便移植。我们先来添加必要的include,并定义类结构:
#ifndef SHADER_H #define SHADER_H #include <glad/glad.h>; // 包含glad来获取所有的必须OpenGL头文件 #include <string> #include <fstream> #include <sstream> #include <iostream> class Shader { public: // 程序ID unsigned int ID; // 构造器读取并构建着色器 Shader(const GLchar* vertexPath, const GLchar* fragmentPath); // 使用/激活程序 void use(); // uniform工具函数 void setBool(const std::string &name, bool value) const; void setInt(const std::string &name, int value) const; void setFloat(const std::string &name, float value) const; }; #endif
- 在上面,我们在头文件顶部使用了几个预处理指令(Preprocessor Directives)。这些预处理指令会告知你的编译器只在它没被包含过的情况下才包含和编译这个头文件,即使多个文件都包含了这个着色器头文件。它是用来防止链接冲突的。
- 着色器类储存了着色器程序的ID。它的构造器需要顶点和片段着色器源代码的文件路径,这样我们就可以把源码的文本文件储存在硬盘上了。除此之外,为了让我们的生活更轻松一点,还加入了一些工具函数:use用来激活着色器程序,所有的set…函数能够查询一个unform的位置值并设置它的值。
-
我们使用C++文件流读取着色器内容,储存到几个
string
对象里:Shader(const char* vertexPath, const char* fragmentPath) { // 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(vertexPath); fShaderFile.open(fragmentPath); 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(); [...]
下一步,我们需要编译和链接着色器。注意,我们也将检查编译/链接是否失败,如果失败则打印编译时错误,调试的时候这些错误输出会及其重要(你总会需要这些错误日志的):
// 2. 编译着色器 unsigned int vertex, fragment; int success; char infoLog[512]; // 顶点着色器 vertex = glCreateShader(GL_VERTEX_SHADER); glShaderSource(vertex, 1, &vShaderCode, NULL); glCompileShader(vertex); // 打印编译错误(如果有的话) glGetShaderiv(vertex, GL_COMPILE_STATUS, &success); if(!success) { glGetShaderInfoLog(vertex, 512, NULL, infoLog); std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl; }; // 片段着色器也类似 [...] // 着色器程序 ID = glCreateProgram(); glAttachShader(ID, vertex); glAttachShader(ID, fragment); glLinkProgram(ID); // 打印连接错误(如果有的话) glGetProgramiv(ID, GL_LINK_STATUS, &success); if(!success) { glGetProgramInfoLog(ID, 512, NULL, infoLog); std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog << std::endl; } // 删除着色器,它们已经链接到我们的程序中了,已经不再需要了 glDeleteShader(vertex); glDeleteShader(fragment);
use函数非常简单:
void use() { glUseProgram(ID); }
uniform的setter函数也很类似:
void setBool(const std::string &name, bool value) const { glUniform1i(glGetUniformLocation(ID, name.c_str()), (int)value); } void setInt(const std::string &name, int value) const { glUniform1i(glGetUniformLocation(ID, name.c_str()), value); } void setFloat(const std::string &name, float value) const { glUniform1f(glGetUniformLocation(ID, name.c_str()), value); }
现在我们就写完了一个完整的着色器类。使用这个着色器类很简单;只要创建一个着色器对象,从那一点开始我们就可以开始使用了:
Shader ourShader("path/to/shaders/shader.vs", "path/to/shaders/shader.fs"); ... while(...) { ourShader.use(); ourShader.setFloat("someUniform", 1.0f); DrawStuff(); }
- 我们把顶点和片段着色器储存为两个叫做
core.vs
和core.fs
的文件。你可以使用自己喜欢的名字命名着色器文件;用.vs
和.fs
作为扩展名很直观。 - core.vs
#version 330 core layout (location = 0) in vec3 aPos; // 位置变量的属性位置值为0 out vec4 vertexColor; // 为片段着色器指定一个颜色输出 void main() { gl_Position = vec4(aPos, 1.0); // 注意我们如何把一个vec3作为vec4的构造器的参数 vertexColor = vec4(0.5, 0.0, 0.0, 1.0); // 把输出变量设置为暗红色 }
- core.fs
#version 330 core out vec4 FragColor; in vec4 vertexColor; // 从顶点着色器传来的输入变量(名称相同、类型相同) void main() { FragColor = vertexColor; }
- Shader.h
- glad头文件版本
-
#ifndef SHADER_H #define SHADER_H #include <glad/glad.h> #include <string> #include <fstream> #include <sstream> #include <iostream> class Shader { public: unsigned int ID; // constructor generates the shader on the fly // ------------------------------------------------------------------------ Shader(const char* vertexPath, const char* fragmentPath) { // 1. retrieve the vertex/fragment source code from filePath std::string vertexCode; std::string fragmentCode; std::ifstream vShaderFile; std::ifstream fShaderFile; // ensure ifstream objects can throw exceptions: vShaderFile.exceptions (std::ifstream::failbit | std::ifstream::badbit); fShaderFile.exceptions (std::ifstream::failbit | std::ifstream::badbit); try { // open files vShaderFile.open(vertexPath); fShaderFile.open(fragmentPath); std::stringstream vShaderStream, fShaderStream; // read file's buffer contents into streams vShaderStream << vShaderFile.rdbuf(); fShaderStream << fShaderFile.rdbuf(); // close file handlers vShaderFile.close(); fShaderFile.close(); // convert stream into 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(); // 2. compile shaders unsigned int vertex, fragment; // vertex shader vertex = glCreateShader(GL_VERTEX_SHADER); glShaderSource(vertex, 1, &vShaderCode, NULL); glCompileShader(vertex); checkCompileErrors(vertex, "VERTEX"); // fragment Shader fragment = glCreateShader(GL_FRAGMENT_SHADER); glShaderSource(fragment, 1, &fShaderCode, NULL); glCompileShader(fragment); checkCompileErrors(fragment, "FRAGMENT"); // shader Program ID = glCreateProgram(); glAttachShader(ID, vertex); glAttachShader(ID, fragment); glLinkProgram(ID); checkCompileErrors(ID, "PROGRAM"); // delete the shaders as they're linked into our program now and no longer necessary glDeleteShader(vertex); glDeleteShader(fragment); } // activate the shader // ------------------------------------------------------------------------ void use() { glUseProgram(ID); } // utility uniform functions // ------------------------------------------------------------------------ void setBool(const std::string &name, bool value) const { glUniform1i(glGetUniformLocation(ID, name.c_str()), (int)value); } // ------------------------------------------------------------------------ void setInt(const std::string &name, int value) const { glUniform1i(glGetUniformLocation(ID, name.c_str()), value); } // ------------------------------------------------------------------------ void setFloat(const std::string &name, float value) const { glUniform1f(glGetUniformLocation(ID, name.c_str()), value); } private: // utility function for checking shader compilation/linking errors. // ------------------------------------------------------------------------ void checkCompileErrors(unsigned int shader, std::string type) { int success; char infoLog[1024]; if (type != "PROGRAM") { glGetShaderiv(shader, GL_COMPILE_STATUS, &success); if (!success) { glGetShaderInfoLog(shader, 1024, NULL, infoLog); std::cout << "ERROR::SHADER_COMPILATION_ERROR of type: " << type << "\n" << infoLog << "\n -- --------------------------------------------------- -- " << std::endl; } } else { glGetProgramiv(shader, GL_LINK_STATUS, &success); if (!success) { glGetProgramInfoLog(shader, 1024, NULL, infoLog); std::cout << "ERROR::PROGRAM_LINKING_ERROR of type: " << type << "\n" << infoLog << "\n -- --------------------------------------------------- -- " << std::endl; } } } }; #endif
- glew头文件版本
-
#pragma once #include <string> #include <fstream> #include <sstream> #include <iostream> #include <GL/glew.h> class Shader { GLuint vertex, fragment; public: GLuint Program; Shader(const GLchar *vertexPath, const GLchar *fragmentPath) { // 1.Retrieve the source code std::string vertexCode; std::string fragmentCode; std::ifstream vShaderFile; std::ifstream fShaderFile; vShaderFile.exceptions(std::ifstream::badbit); fShaderFile.exceptions(std::ifstream::badbit); try { // Open File vShaderFile.open(vertexPath); fShaderFile.open(fragmentPath); // Stream the file; std::stringstream vShaderStream, fShaderStream; vShaderStream << vShaderFile.rdbuf(); fShaderStream << fShaderFile.rdbuf(); // Close the file vShaderFile.close(); fShaderFile.close(); // Convert to the string vertexCode = vShaderStream.str(); fragmentCode = fShaderStream.str(); const GLchar *vShaderCode = vertexCode.c_str(); const GLchar *fShaderCode = fragmentCode.c_str(); // Compile shaders vertex = glCreateShader(GL_VERTEX_SHADER); glShaderSource(vertex, 1, &vShaderCode, NULL); glCompileShader(vertex); GLint success; GLchar inforLog[512]; glGetShaderiv(vertex, GL_COMPILE_STATUS, &success); if (!success) { glGetShaderInfoLog(vertex, 512, NULL, inforLog); std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << inforLog << std::endl; } fragment = glCreateShader(GL_FRAGMENT_SHADER); glShaderSource(fragment, 1, &fShaderCode, NULL); glCompileShader(fragment); glGetShaderiv(fragment, GL_COMPILE_STATUS, &success); if (!success) { glGetShaderInfoLog(fragment, 512, NULL, inforLog); std::cout << "ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n" << inforLog << std::endl; } // create the program and link it this->Program = glCreateProgram(); glAttachShader(this->Program, vertex); glAttachShader(this->Program, fragment); glLinkProgram(this->Program); glGetProgramiv(this->Program, GL_LINK_STATUS, &success); if (!success) { glGetProgramInfoLog(this->Program, 512, NULL, inforLog); std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << inforLog << std::endl; } } catch (std::ifstream::failure e) { std::cout << "ERROR::SHADER::FILE_NOT_SUCCESSFULLY_READ" << std::endl; } } ~Shader() { glDetachShader(this->Program, vertex); glDetachShader(this->Program, fragment); glDeleteShader(vertex); glDeleteShader(fragment); glDeleteProgram(this->Program); } void Use() { glUseProgram(this->Program); } };
- main.cpp
-
//用着色器类的调色盘三角形 #include "pch.h" //GLEW #include <iostream> #define GLEW_STATIC #include <GL/glew.h> #include <GLFW/glfw3.h> #include "SOIL2/SOIL2.h" #include "glm/glm.hpp" #include "glm/gtc/type_ptr.hpp" #include "Shader.h" void processInput(GLFWwindow *window); int main() { //初始化GLFW glfwInit(); //使用flfwWindowHint函数来配置GLFW /* glfwWindowHint函数的第一个参数代表选项的名称, 我们可以从很多以GLFW_开头的枚举值中选择; 第二个参数接受一个整型, 用来设置这个选项的值 */ glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); //glfwCreateWindow函数需要窗口的宽和高作为它的前两个参数 //第三个参数表示这个窗口的名称 GLFWwindow* window = glfwCreateWindow(800, 600, "B7040312", NULL, NULL); if (window == NULL) { std::cout << "Failed to create GLFW window" << std::endl; glfwTerminate(); return -1; } //创建完窗口我们就可以通知GLFW将我们窗口的上下文设置为当前线程的主上下文了 glfwMakeContextCurrent(window); //GLEW是用来管理OpenGL的函数指针的,所以在调用任何OpenGL的函数之前我们需要初始化GLEW //glewExperimental所做的是即使驱动程序的扩展程序字符串中不存在扩展程序,也允许加载扩展程序入口点。 glewExperimental = GL_TRUE; if (glewInit() != GLEW_OK) { std::cout << "Failed to initialize GLEW" << std::endl; return -1; } /* 开始渲染之前还有一件重要的事情要做,我们必须告诉OpenGL渲染窗口的尺寸大小,这样OpenGL才只能知道怎样相对于窗口大小显示数据和坐标。 我们可以通过调用glViewport函数来设置窗口的维度(Dimension): */ /* 然而,当用户改变窗口的大小的时候,视口也应该被调整。 我们可以对窗口注册一个回调函数(Callback Function), 它会在每次窗口大小被调整的时候被调用 */ int width, height; glfwGetFramebufferSize(window, &width, &height); /* glViewport函数前两个参数控制窗口左下角的位置。 第三个和第四个参数控制渲染窗口的宽度和高度(像素),这里我们是直接从GLFW中获取的。 */ glViewport(0, 0, width, height); Shader ourShader("res/shaders/core.vs", "res/shaders/core.fs"); /* 由于我们希望渲染一个三角形,我们一共要指定三个顶点,每个顶点都有一个3D位置。 我们会将它们以标准化设备坐标的形式(OpenGL的可见区域)定义为一个float数组。 由于OpenGL是在3D空间中工作的,而我们渲染的是一个2D三角形,我们将它顶点的z坐标设置为0.0。 这样子的话三角形每一点的深度(Depth,译注2)都是一样的,从而使它看上去像是2D的。 */ float vertices[] = { // positions // colors 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, // bottom right -0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, // bottom left 0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f // top }; unsigned int VBO, VAO; glGenVertexArrays(1, &VAO); glGenBuffers(1, &VBO); // bind the Vertex Array Object first, then bind and set vertex buffer(s), and then configure vertex attributes(s). glBindVertexArray(VAO); glBindBuffer(GL_ARRAY_BUFFER, VBO); glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); // position attribute glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0); glEnableVertexAttribArray(0); // color attribute glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3 * sizeof(float))); glEnableVertexAttribArray(1); glBindBuffer(GL_ARRAY_BUFFER, 0); //之后您可以解除绑定VAO,这样其他VAO调用就不会意外修改此VAO,但这很少发生。 修改其他 // VAO无论如何都需要调用glBindVertexArray,因此通常在不需要时,我们通常不会取消绑定VAO(也没有VBO)。 glBindVertexArray(0); /* 一般当你打算绘制多个物体时,你首先要生成/配置所有的VAO(和必须的VBO及属性指针), 然后储存它们供后面使用。 当我们打算绘制物体的时候就拿出相应的VAO,绑定它,绘制完物体后,再解绑VAO。 */ /* 我们希望程序在我们主动关闭它之前不断绘制图像并能够接受用户输入。 因此,我们需要在程序中添加一个while循环,我们可以把它称之为渲染循环(Render Loop), 它能在我们让GLFW退出前一直保持运行。 下面几行的代码就实现了一个简单的渲染循环: */ /* glfwWindowShouldClose函数在我们每次循环的开始前检查一次GLFW是否被要求退出, 如果是的话该函数返回true然后渲染循环便结束了,之后为我们就可以关闭应用程序了。 */ while (!glfwWindowShouldClose(window)) { // input // ----- processInput(window); // render // ------ //要把所有的渲染(Rendering)操作放到渲染循环中, //因为我们想让这些渲染指令在每次渲染循环迭代的时候都能被执行。 glClearColor(0.2f, 0.3f, 0.3f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); // draw our first triangle ourShader.use();//glUseProgram(shaderProgram); glBindVertexArray(VAO); //我们只有一个VAO,因此不必每次都绑定它,但是我们这样做是为了使事情更有条理 //glDrawArrays函数,它使用当前激活的着色器,之前定义的顶点属性配置,和VBO的顶点数据(通过VAO间接绑定)来绘制图元。 glDrawArrays(GL_TRIANGLES, 0, 3); /* glfwSwapBuffers函数会交换颜色缓冲(它是一个储存着GLFW窗口每一个像素颜色值的大缓冲), 它在这一迭代中被用来绘制,并且将会作为输出显示在屏幕上。 */ /* glfwPollEvents函数检查有没有触发什么事件(比如键盘输入、鼠标移动等)、更新窗口状态, 并调用对应的回调函数(可以通过回调方法手动设置) */ glfwSwapBuffers(window); glfwPollEvents(); } //delete glDeleteVertexArrays(1, &VAO); glDeleteBuffers(1, &VBO); /* 当渲染循环结束后我们需要正确释放/删除之前的分配的所有资源。 我们可以在main函数的最后调用glfwTerminate函数来完成。 */ glfwTerminate(); return 0; } void processInput(GLFWwindow *window) { //检测是否按下esc,按下则退出循环 if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS) glfwSetWindowShouldClose(window, true); }
-
-