着色器
我们要写一个三角形,需要经理上图流程,其中顶点着色器和片段着色器需要我们自己写。
着色器基本上只是一个程序, 不同点在于它是运行在我们的gpu上,在我们的显卡上,而不是像C++程序一样运行在cpu上。
(2)片段着色器
(3)顶点着色器和片段着色器的区别
现在你可以注意到这两者之间有一点不一样,顶点着色器调用三次,片段着色器调用成百上千次,这取决于我们的三角形在我们的屏幕上占用了多大空间,如果你有一个细小的三角形,在你的窗口上一个真的很小的三角形,那可能会调用额外的50次。如果你有一个巨大的三角形,这充满了你的屏幕,这可能是有一百万像素或者50万像素,那就意味着片段着色器要调用50万次。
#version 330 core表示OpenGL的版本号及使用核心模式。(对输入数据,只传输未处理)。
in关键字,在顶点着色器中声明所有的输入顶点属性(Input Vertex Attribute)。顶点都为3D坐标,因此创建vec3类型的输入变量aPos。
顶点着色器需要将数据赋值给预定义的gl_Position变量(顶点着色器的输出,vec4类型,w分量设置为1.0f)。我们同样也通过layout (location = 0)设定了输入变量的位置值。
片段着色器所做的是计算像素最后的颜色输出。
片段着色器只需要一个输出变量,这个变量是一个4分量向量,它表示的是最终的输出颜色,我们应该自己将其计算出来。声明输出变量可以使用out关键字,这里我们命名为FragColor。下面,我们将一个Alpha值为1.0(1.0代表完全不透明)的橘黄色的vec4赋值给颜色输出。
(4)编译着色器
现在,我们暂时将顶点着色器的源代码硬编码在代码文件顶部的C风格字符串中:
const char *vertexShaderSource = "#version 330 core\n" "layout (location = 0) in vec3 aPos;\n" "void main()\n" "{\n" " gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);\n" "}\0"; const char *fragmentShaderSource = "#version 330 core\n" "out vec4 FragColor;\n" "void main()\n" "{\n" " FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);\n" "}\n\0";
为了让OpenGL使用着色器,必须在运行时从源码中动态编译着色器。首先创建着色器对象。
各个阶段的着色器需要通过着色器程序对象链接起来。着色器程序对象是多个着色器组合的最终链接版本。
将着色器链接到程序时,会将每个着色器的输出链接到下一个着色器的输入。如果输出和输入不匹配,会出现链接错误。
我们首先要做的是创建一个着色器对象,注意还是用ID来引用的。所以我们储存这个顶点着色器为unsigned int
,然后用glCreateShader创建这个着色器:
unsigned int vertexShader; vertexShader = glCreateShader(GL_VERTEX_SHADER);
我们把需要创建的着色器类型以参数形式提供给glCreateShader。由于我们正在创建一个顶点着色器,传递的参数是GL_VERTEX_SHADER。
下一步我们把这个着色器源码附加到着色器对象上,然后编译它:
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL); glCompileShader(vertexShader);
glShaderSource函数把要编译的着色器对象作为第一个参数。第二参数指定了传递的源码字符串数量,这里只有一个。第三个参数是顶点着色器真正的源码,第四个参数我们先设置为NULL
。
(5)着色器程序
着色器程序对象(Shader Program Object)是多个着色器合并之后并最终链接完成的版本。如果要使用刚才编译的着色器我们必须把它们链接(Link)为一个着色器程序对象,然后在渲染对象的时候激活这个着色器程序。已激活着色器程序的着色器将在我们发送渲染调用的时候被使用。
当链接着色器至一个程序的时候,它会把每个着色器的输出链接到下个着色器的输入。当输出和输入不匹配的时候,你会得到一个连接错误。
创建一个程序对象很简单:
unsigned int shaderProgram; shaderProgram = glCreateProgram();
glCreateProgram函数创建一个程序,并返回新创建程序对象的ID引用。现在我们需要把之前编译的着色器附加到程序对象上,然后用glLinkProgram链接它们:
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);
得到的结果就是一个程序对象,我们可以调用glUseProgram函数,用刚创建的程序对象作为它的参数,以激活这个程序对象:
glUseProgram(shaderProgram);
对了,在把着色器对象链接到程序对象以后,记得删除着色器对象,我们不再需要它们了:
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
代码示例:
// GLAD的include文件包含所需的OpenGL头文件(如GL/GL.h),因此确保在其他需要OpenGL的头文件(如GLFW)之前包含GLAD。就是#include <glad/glad.h> 放在最前面 #include <glad/glad.h> #include <GLFW/glfw3.h> #include <iostream> float vertices[] = { -0.5f, -0.5f, 0.0f, 0.5f, -0.5f, 0.0f, 0.0f, 0.5f, 0.0f }; const char* vertexShaderSource = "#version 330 core\n" "layout (location = 0) in vec3 aPos;\n" "void main()\n" "{\n" " gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);\n" "}\0"; const char* fragmentShaderSource = "#version 330 core\n" "out vec4 FragColor;\n" "void main()\n" "{\n" " FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);\n" "}\n\0"; void framebuffer_size_callback(GLFWwindow* window, int width, int height); void processInput(GLFWwindow* window); int main() { // 初始化GLFW,只有初始化完成之后才能够使用GLFW的函数 glfwInit(); // GLFW配置设置 glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); // 如果是苹果系统的话使用下面代码 #ifdef __APPLE__ glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); #endif // 创建窗口 大小和名称 GLFWwindow* window = glfwCreateWindow(800, 600, "LearnOpenGL", NULL, NULL); if (window == NULL) { std::cout << "Failed to create GLFW window" << std::endl; // 此函数销毁所有剩余的窗口和光标 glfwTerminate(); return -1; } //GLFW将窗口的上下文设置为当前线程的上下文 glfwMakeContextCurrent(window); // 告诉GLFW我们希望每当窗口调整大小的时候调用这个函数 glfwSetFramebufferSizeCallback(window, framebuffer_size_callback); //GLAD // glad: 加载所有OpenGL函数指针 if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) { std::cout << "Failed to initialize GLAD" << std::endl; return -1; } #pragma region 着色器 // 创建和编译着色器程序 //顶点着色器 unsigned int vertexShader = glCreateShader(GL_VERTEX_SHADER); glShaderSource(vertexShader, 1, &vertexShaderSource, NULL); glCompileShader(vertexShader); // 检查编译错误 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; } // 片段着色器 unsigned int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER); glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL); glCompileShader(fragmentShader); // 检查编译错误 glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success); if (!success) { glGetShaderInfoLog(fragmentShader, 512, NULL, infoLog); std::cout << "ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n" << infoLog << std::endl; } //着色器程序 unsigned int shaderProgram = glCreateProgram(); glAttachShader(shaderProgram, vertexShader); glAttachShader(shaderProgram, fragmentShader); glLinkProgram(shaderProgram); //链接错误检查 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); #pragma endregion //创建VBO和VAO对象,并赋予ID unsigned int VBO, VAO; //创建1个VAO对象 glGenVertexArrays(1, &VAO); //创建1个VBO对象 glGenBuffers(1, &VBO); //绑定VBO和VAO对象 glBindVertexArray(VAO); //缓冲对象如果绑定的是顶点属性则用:GL_ARRAY_BUFFER glBindBuffer(GL_ARRAY_BUFFER, VBO); //为当前绑定到target的缓冲区对象创建一个新的数据存储。 //如果data不是NULL,则使用来自此指针的数据初始化数据存储 glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); //上面我们将数据存到了缓冲区对象中,下面就需要 //告知Shader(着色器)如何解析缓冲里的属性值 //第一个属性是从哪个位置开始解析 //第二个属性是每个顶点属性由几个组合而成 //第三个属性是数组中每个元素的类型 //第四个表示是否标准化,这里暂时不需要,需要注意只有整型值才会有效,如果浮点型的数据不会起作用 //第五个是步长,因为我们这个是数组中每3个元素组成一个顶点属性,而且是float类型 //第六个是偏移量,我们这里为0,就是从0开始读取数组中的数据的 glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); //开启VAO管理的第一个属性值 glEnableVertexAttribArray(0); //作为习惯,用完之后将VBO,VAO解绑,需要的时候可以重新绑定 glBindBuffer(GL_ARRAY_BUFFER, 0); glBindVertexArray(0); // 渲染循环 一个循环就是一帧 只要是窗体不关闭,就会一直循环 while (!glfwWindowShouldClose(window)) { processInput(window); // 在这里,我们将屏幕设置为了类似黑板的深蓝绿色 glClearColor(0.2f, 0.3f, 0.3f, 1.0f); //状态设置 // 调用glClear函数,清除颜色缓冲之后,整个颜色缓冲都会被填充为glClearColor里所设置的颜色。 glClear(GL_COLOR_BUFFER_BIT); //状态使用 #pragma region 绘制三角形 // 首先,要选择使用哪个着色器 每个着色器调用和渲染调用都会使用这个程序对象 glUseProgram(shaderProgram); // 需要注意的是前面我们已经解绑了VAO,所以现在是无法解析数据的,所以我们需要重新绑定, // 至于数据我们已经存到缓冲区了 glBindVertexArray(VAO); // 从数据数组中indexwei0处开始读取,每三个做一个三角形的顶点。第三个参数是说一共绘制三个顶点数据(每个顶点由vertices数组中的3个元素组成) glDrawArrays(GL_TRIANGLES, 0, 3); #pragma endregion // glfw: 交换缓冲区 该函数在指定窗口的前后缓冲区交换 // 前缓冲区:屏幕上显示的图像 // 后缓冲区:正在渲染的图像 // glfwSwapBuffers函数会交换颜色缓冲(它是一个储存着GLFW窗口每一个像素颜色值的大缓冲), // 它在这一迭代中被用来绘制,并且将会作为输出显示在屏幕上 glfwSwapBuffers(window); // 轮询IO事件(按键按下 / 释放、鼠标移动等)通过下面方法就可以是得窗体对鼠标做出的动作做出反应,比如关闭,移动窗体等 glfwPollEvents(); } // glfw: 回收前面分配的GLFW先关资源. 一定要注意,只有关闭窗体之后才会跳出while循环走到这一步!!! glfwTerminate(); // 在while循环退出后释放内存: glDeleteVertexArrays(1, &VAO); glDeleteBuffers(1, &VBO); glDeleteProgram(shaderProgram); return 0; } // glfwGetKey函数:需要一个窗口以及一个按键作为输入;函数将会返回这个按键是否正在被按下 void processInput(GLFWwindow* window) { // 如果按下了ESC键,设置窗体的关闭标志为true,代表窗体可以退出 if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS) glfwSetWindowShouldClose(window, true); } // 当改变窗口的大小的时候,视口也应该被调整。我们可以对窗口注册一个回调函数(Callback Function),它会在每次窗口大小被调整的时候被调用 void framebuffer_size_callback(GLFWwindow* window, int width, int height) { // 设置窗口维度 // glViewport(前两参数为窗口左下角位置,3.宽度,4.高度) glViewport(0, 0, width, height); }
结果:
参考:
https://learnopengl-cn.github.io/01%20Getting%20started/04%20Hello%20Triangle/#_5