文章目录

  • 前言
  • GLSL
  • 数据类型
  • 输入与输出
    • 顶点着色器
    • 片段着色器
  • Uniform
  • 多个属性
  • 着色器类模板
    • 创建着色器程序
    • 创建Shader脚本
    • 使用模板和Shader脚本文件

前言

着色器(Shader)是运行在GPU上的小程序。这些小程序为图形渲染管线的某个特定部分而运行。

从基本意义上来说,着色器只是一种把输入转化为输出的程序。着色器也是一种非常独立的程序,因为它们之间不能相互通信。它们之间唯一的沟通只有通过输入和输出。

GLSL

着色器是使用一种叫GLSL的类C语言写成的。GLSL是为图形计算量身定制的,它包含一些针对向量和矩阵操作的有用特性。

着色器的开头总是要声明版本,接着是输入和输出变量、uniform和main函数。每个着色器的入口点都是main函数,在这个函数中我们处理所有的输入变量,并将结果输出到输出变量中。

每个输入变量也叫顶点属性(Vertex Attribute)。能声明的顶点属性是有上限的,它一般由硬件来决定。OpenGL确保至少有16个包含4分量的顶点属性可用,但是有些硬件或许允许更多的顶点属性,你可以查询GL_MAX_VERTEX_ATTRIBS来获取具体的上限:

int nrAttributes;glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &nrAttributes);std::cout << "Maximum nr of vertex attributes supported: " << nrAttributes << std::endl;

数据类型

GLSL中包含C等其它语言大部分的默认基础数据类型:intfloatdoubleuintbool

GLSL也有两种容器类型:向量(Vector)和矩阵(Matrix)

这个重点介绍一下向量(Vector):

GLSL中的向量是一个可以包含有2、3或者4个分量的容器,分量的类型可以是前面默认基础类型的任意一个。

类型含义
vecn包含n个float分量的默认向量
bvecn包含n个bool分量的向量
ivecn包含n个int分量的向量
uvecn包含n个unsigned int分量的向量
dvecn包含n个double分量的向量

向量这一数据类型也允许一些有趣而灵活的分量选择方式,叫做重组(Swizzling)。重组允许这样的语法:

vec2 someVec;vec4 differentVec = someVec.xyxx;vec3 anotherVec = differentVec.zyw;vec4 otherVec = someVec.xxxx + anotherVec.yxzy;

我们也可以把一个向量作为一个参数传给不同的向量构造函数,以减少需求参数的数量:

vec2 vect = vec2(0.5, 0.7);vec4 result = vec4(vect, 0.0, 0.0);vec4 otherResult = vec4(result.xyz, 1.0);

输入与输出

GLSL定义了inout关键字专门来实现这个目的。

每个着色器使用这两个关键字设定输入和输出,只要一个输出变量与下一个着色器阶段的输入匹配,它就会传递下去。

为了定义顶点数据该如何管理,我们使用location这一元数据指定输入变量,这样我们才可以在CPU上配置顶点属性。

顶点着色器

#version 330 corelayout (location = 0) in vec3 aPos; // 位置变量的属性位置值为0out vec4 vertexColor; // 为片段着色器指定一个颜色输出void main(){gl_Position = vec4(aPos, 1.0); // 注意我们如何把一个vec3作为vec4的构造器的参数vertexColor = vec4(0.5, 0.0, 0.0, 1.0); // 把输出变量设置为暗红色}

片段着色器

#version 330 coreout vec4 FragColor;in vec4 vertexColor; // 从顶点着色器传来的输入变量(名称相同、类型相同)void main(){FragColor = vertexColor;}

完整脚本

#include #include #include void InitGLFW();bool CreateWindow();bool InitGLAD();// 窗口大小改变时调用void framebuffer_size_callback(GLFWwindow *window, int width, int height);void processInput(GLFWwindow *window);// settings 窗口宽高const unsigned int SCR_WIDTH = 800;const unsigned int SCR_HEIGHT = 600;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.x, aPos.y, aPos.z, 1.0);\n" "vertexColor = vec4(0.5, 0.0, 0.0, 1.0);\n" "}\0";const char *fragmentShaderSource = "#version 330 core\n" "out vec4 FragColor;\n" " in vec4 vertexColor;\n" "void main()\n" "{\n" " FragColor =vertexColor;\n" "}\n\0";GLFWwindow *window;int main(){InitGLFW();// 初始化GLFWbool isCreated = CreateWindow(); // 创建一个窗口对象if (!isCreated)return -1;bool isGLAD = InitGLAD(); // 初始化GLAD,传入加载系统相关opengl函数指针的函数if (!isGLAD)return -1;// TODO:创建一个三角形,使用着色器语言GLSL编写顶点着色器和片段着色器,将它们编译链接到一个着色器程序中,并使用这个着色器程序绘制三角形。// 1.顶点着色器(vertex shader) 用于处理顶点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;}// 2.片段着色器(fragment shader) 用于处理片段/片元(一个片段是OpenGL渲染一个像素所需的所有数据)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;}// 3.着色器程序(shader program) 用来管理着色器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); // 删除着色器// 设置顶点数据(和缓冲区)并配置顶点属性// 1.顶点输入float vertices[] = {-0.5f, -0.5f, 0.0f, // 左0.5f, -0.5f, 0.0f,// 右0.0f, 0.5f, 0.0f// 上};// 2.创建一个顶点缓冲对象(Vertex Buffer Objects, VBO) 顶点数组对象(Vertex Array Object, VAO)unsigned int VBO, VAO;glGenVertexArrays(1, &VAO); // 生成一个VAO对象glGenBuffers(1, &VBO);// 生成一个VBO对象// 3.首先绑定顶点数组对象,然后绑定和设置顶点缓冲区,然后配置顶点属性。glBindVertexArray(VAO);// 绑定VAOglBindBuffer(GL_ARRAY_BUFFER, VBO);// 绑定VBOglBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); // 将顶点数据复制到缓冲区中// 4.配置顶点属性glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void *)0); // 解析顶点数据glEnableVertexAttribArray(0);// 启用顶点属性// 注意这是允许的,调用glVertexAttribPointer将VBO注册为顶点属性的绑定顶点缓冲对象,因此之后我们可以安全地解除绑定glBindBuffer(GL_ARRAY_BUFFER, 0);// 你可以在之后解除对VAO的绑定,这样其他VAO调用就不会意外地修改这个VAO,但这种情况很少发生。修改其他//VAOs无论如何都需要调用glBindVertexArray,所以当不直接需要时,我们通常不会取消绑定VAOs(也不会取消绑定vbo)。glBindVertexArray(0);// 取消此调用的注释以绘制线框多边形。// 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);// 绘制三角形glUseProgram(shaderProgram);// 使用着色器程序glBindVertexArray(VAO); // 绑定VAOglDrawArrays(GL_TRIANGLES, 0, 3); // 绘制三角形// glBindVertexArray(0); // 不需要每次都绑定VAO// 检查并调用事件,交换缓冲glfwSwapBuffers(window);glfwPollEvents();}// 可选:一旦资源超出其用途,就取消分配所有资源:glDeleteVertexArrays(1, &VAO);glDeleteBuffers(1, &VBO);glDeleteProgram(shaderProgram);// 释放/删除之前的分配的所有资源glfwTerminate();std::cout << "Hello, World!" << std::endl;return 0;}void InitGLFW(){// 初始化GLFWglfwInit();// 配置GLFW第一个参数代表选项的名称,我们可以从很多以GLFW_开头的枚举值中选择;// 第二个参数接受一个整型,用来设置这个选项的值。glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);// glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);}bool CreateWindow(){// 创建一个窗口对象window = glfwCreateWindow(800, 600, "LearnOpenGL", NULL, NULL);if (window == NULL){std::cout << "Failed to create GLFW window" << std::endl;// 创建失败,终止程序glfwTerminate();return false;}// 将我们窗口的上下文设置为当前线程的主上下文glfwMakeContextCurrent(window);// 设置窗口大小改变时的回调函数glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);return true;}bool InitGLAD(){// 初始化GLAD,传入加载系统相关opengl函数指针的函数if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)){std::cout << "Failed to initialize GLAD" << std::endl;// 初始化失败,终止程序return false;}return true;}// 窗口大小改变时调用void framebuffer_size_callback(GLFWwindow *window, int width, int height){// 设置窗口的维度glViewport(0, 0, width, height);}// 输入void processInput(GLFWwindow *window){// 当用户按下esc键,我们设置window窗口的windowShouldClose属性为true// 关闭应用程序if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)glfwSetWindowShouldClose(window, true);}

运行效果:

Uniform

Uniform是一种从CPU中的应用向GPU中的着色器发送数据的方式,但uniform和顶点属性有些不同。

  1. uniform是全局的(Global)。全局意味着uniform变量必须在每个着色器程序对象中都是独一无二的,而且它可以被着色器程序的任意着色器在任意阶段访问。
  2. 无论你把uniform值设置成什么,uniform会一直保存它们的数据,直到它们被重置或更新

如果你声明了一个uniform却在GLSL代码中没用过,编译器会静默移除这个变量,导致最后编译出的版本中并不会包含它,这可能导致几个非常麻烦的错误!

在片段着色器中声明一个带有uniform关键字的GLSL脚本:

#version 330 coreout vec4 FragColor;uniform vec4 ourColor; // 在OpenGL程序代码中设定这个变量void main(){FragColor = ourColor;}

此时,这个uniform现在还是空的,然后,我们在C++程序中访问更新它的值:

float timeValue = glfwGetTime();float greenValue = (sin(timeValue) / 2.0f) + 0.5f;//用glGetUniformLocation查询uniform ourColor的位置值。int vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor");//更新一个uniform之前你必须先使用程序(调用glUseProgram),//因为它是在当前激活的着色器程序中设置uniform的。glUseProgram(shaderProgram);//通过glUniform4f函数设置uniform值glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f);

完整代码

#include #include #include #include void InitGLFW();bool CreateWindow();bool InitGLAD();// 窗口大小改变时调用void framebuffer_size_callback(GLFWwindow *window, int width, int height);void processInput(GLFWwindow *window);// settings 窗口宽高const unsigned int SCR_WIDTH = 800;const unsigned int SCR_HEIGHT = 600;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" "uniform vec4 ourColor;\n" "void main()\n" "{\n" " FragColor =ourColor;\n" "}\n\0";GLFWwindow *window;int main(){InitGLFW();// 初始化GLFWbool isCreated = CreateWindow(); // 创建一个窗口对象if (!isCreated)return -1;bool isGLAD = InitGLAD(); // 初始化GLAD,传入加载系统相关opengl函数指针的函数if (!isGLAD)return -1;// TODO:创建一个三角形,使用着色器语言GLSL编写顶点着色器和片段着色器,将它们编译链接到一个着色器程序中,并使用这个着色器程序绘制三角形。// 1.顶点着色器(vertex shader) 用于处理顶点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;}// 2.片段着色器(fragment shader) 用于处理片段/片元(一个片段是OpenGL渲染一个像素所需的所有数据)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;}// 3.着色器程序(shader program) 用来管理着色器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); // 删除着色器// 设置顶点数据(和缓冲区)并配置顶点属性// 1.顶点输入float vertices[] = {-0.5f, -0.5f, 0.0f, // 左0.5f, -0.5f, 0.0f,// 右0.0f, 0.5f, 0.0f// 上};// 2.创建一个顶点缓冲对象(Vertex Buffer Objects, VBO) 顶点数组对象(Vertex Array Object, VAO)unsigned int VBO, VAO;glGenVertexArrays(1, &VAO); // 生成一个VAO对象glGenBuffers(1, &VBO);// 生成一个VBO对象// 3.首先绑定顶点数组对象,然后绑定和设置顶点缓冲区,然后配置顶点属性。glBindVertexArray(VAO);// 绑定VAOglBindBuffer(GL_ARRAY_BUFFER, VBO);// 绑定VBOglBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); // 将顶点数据复制到缓冲区中// 4.配置顶点属性glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void *)0); // 解析顶点数据glEnableVertexAttribArray(0);// 启用顶点属性// 注意这是允许的,调用glVertexAttribPointer将VBO注册为顶点属性的绑定顶点缓冲对象,因此之后我们可以安全地解除绑定glBindBuffer(GL_ARRAY_BUFFER, 0);// 你可以在之后解除对VAO的绑定,这样其他VAO调用就不会意外地修改这个VAO,但这种情况很少发生。修改其他//VAOs无论如何都需要调用glBindVertexArray,所以当不直接需要时,我们通常不会取消绑定VAOs(也不会取消绑定vbo)。glBindVertexArray(0);// 取消此调用的注释以绘制线框多边形。// 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);// 记得激活着色器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); // 绑定VAOglDrawArrays(GL_TRIANGLES, 0, 3); // 绘制三角形// glBindVertexArray(0); // 不需要每次都绑定VAO// 检查并调用事件,交换缓冲glfwSwapBuffers(window);glfwPollEvents();}// 可选:一旦资源超出其用途,就取消分配所有资源:glDeleteVertexArrays(1, &VAO);glDeleteBuffers(1, &VBO);glDeleteProgram(shaderProgram);// 释放/删除之前的分配的所有资源glfwTerminate();std::cout << "Hello, World!" << std::endl;return 0;}void InitGLFW(){// 初始化GLFWglfwInit();// 配置GLFW第一个参数代表选项的名称,我们可以从很多以GLFW_开头的枚举值中选择;// 第二个参数接受一个整型,用来设置这个选项的值。glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);// glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);}bool CreateWindow(){// 创建一个窗口对象window = glfwCreateWindow(800, 600, "LearnOpenGL", NULL, NULL);if (window == NULL){std::cout << "Failed to create GLFW window" << std::endl;// 创建失败,终止程序glfwTerminate();return false;}// 将我们窗口的上下文设置为当前线程的主上下文glfwMakeContextCurrent(window);// 设置窗口大小改变时的回调函数glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);return true;}bool InitGLAD(){// 初始化GLAD,传入加载系统相关opengl函数指针的函数if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)){std::cout << "Failed to initialize GLAD" << std::endl;// 初始化失败,终止程序return false;}return true;}// 窗口大小改变时调用void framebuffer_size_callback(GLFWwindow *window, int width, int height){// 设置窗口的维度glViewport(0, 0, width, height);}// 输入void processInput(GLFWwindow *window){// 当用户按下esc键,我们设置window窗口的windowShouldClose属性为true// 关闭应用程序if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)glfwSetWindowShouldClose(window, true);}

多个属性

把位置数据和颜色数据添加到顶点数据中:

声明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// 顶部};

添加到顶点着色器中,修改着色器:

#version 330 corelayout (location = 0) in vec3 aPos; // 位置变量的属性位置值为 0 layout (location = 1) in vec3 aColor; // 颜色变量的属性位置值为 1out vec3 ourColor; // 向片段着色器输出一个颜色void main(){gl_Position = vec4(aPos, 1.0);ourColor = aColor; // 将ourColor设置为我们从顶点数据那里得到的输入颜色}
#version 330 coreout vec4 FragColor;in vec3 ourColor;void main(){FragColor = vec4(ourColor, 1.0);}

因为我们添加了另一个顶点属性,并且更新了VBO的内存,我们就必须重新配置顶点属性指针。更新后的VBO内存中的数据现在看起来像这样:
知道了现在使用的布局,我们就可以使用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);

完整脚本

#include #include #include #include void InitGLFW();bool CreateWindow();bool InitGLAD();// 窗口大小改变时调用void framebuffer_size_callback(GLFWwindow *window, int width, int height);void processInput(GLFWwindow *window);// settings 窗口宽高const unsigned int SCR_WIDTH = 800;const unsigned int SCR_HEIGHT = 600;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.0);\n" "}\n\0";GLFWwindow *window;int main(){InitGLFW();// 初始化GLFWbool isCreated = CreateWindow(); // 创建一个窗口对象if (!isCreated)return -1;bool isGLAD = InitGLAD(); // 初始化GLAD,传入加载系统相关opengl函数指针的函数if (!isGLAD)return -1;// TODO:创建一个三角形,使用着色器语言GLSL编写顶点着色器和片段着色器,将它们编译链接到一个着色器程序中,并使用这个着色器程序绘制三角形。// 1.顶点着色器(vertex shader) 用于处理顶点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;}// 2.片段着色器(fragment shader) 用于处理片段/片元(一个片段是OpenGL渲染一个像素所需的所有数据)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;}// 3.着色器程序(shader program) 用来管理着色器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); // 删除着色器// 设置顶点数据(和缓冲区)并配置顶点属性// 1.顶点输入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// 顶部};// 2.创建一个顶点缓冲对象(Vertex Buffer Objects, VBO) 顶点数组对象(Vertex Array Object, VAO)unsigned int VBO, VAO;glGenVertexArrays(1, &VAO); // 生成一个VAO对象glGenBuffers(1, &VBO);// 生成一个VBO对象// 3.首先绑定顶点数组对象,然后绑定和设置顶点缓冲区,然后配置顶点属性。glBindVertexArray(VAO);// 绑定VAOglBindBuffer(GL_ARRAY_BUFFER, VBO);// 绑定VBOglBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); // 将顶点数据复制到缓冲区中// 4.配置位置属性glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void *)0);glEnableVertexAttribArray(0);// 5. 颜色属性glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void *)(3 * sizeof(float)));glEnableVertexAttribArray(1);// 注意这是允许的,调用glVertexAttribPointer将VBO注册为顶点属性的绑定顶点缓冲对象,因此之后我们可以安全地解除绑定glBindBuffer(GL_ARRAY_BUFFER, 0);// 你可以在之后解除对VAO的绑定,这样其他VAO调用就不会意外地修改这个VAO,但这种情况很少发生。修改其他//VAOs无论如何都需要调用glBindVertexArray,所以当不直接需要时,我们通常不会取消绑定VAOs(也不会取消绑定vbo)。glBindVertexArray(0);// 取消此调用的注释以绘制线框多边形。// 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);// 记得激活着色器glUseProgram(shaderProgram);// 绘制三角形glBindVertexArray(VAO); // 绑定VAOglDrawArrays(GL_TRIANGLES, 0, 3); // 绘制三角形// glBindVertexArray(0); // 不需要每次都绑定VAO// 检查并调用事件,交换缓冲glfwSwapBuffers(window);glfwPollEvents();}// 可选:一旦资源超出其用途,就取消分配所有资源:glDeleteVertexArrays(1, &VAO);glDeleteBuffers(1, &VBO);glDeleteProgram(shaderProgram);// 释放/删除之前的分配的所有资源glfwTerminate();std::cout << "Hello, World!" << std::endl;return 0;}void InitGLFW(){// 初始化GLFWglfwInit();// 配置GLFW第一个参数代表选项的名称,我们可以从很多以GLFW_开头的枚举值中选择;// 第二个参数接受一个整型,用来设置这个选项的值。glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);// glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);}bool CreateWindow(){// 创建一个窗口对象window = glfwCreateWindow(800, 600, "LearnOpenGL", NULL, NULL);if (window == NULL){std::cout << "Failed to create GLFW window" << std::endl;// 创建失败,终止程序glfwTerminate();return false;}// 将我们窗口的上下文设置为当前线程的主上下文glfwMakeContextCurrent(window);// 设置窗口大小改变时的回调函数glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);return true;}bool InitGLAD(){// 初始化GLAD,传入加载系统相关opengl函数指针的函数if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)){std::cout << "Failed to initialize GLAD" << std::endl;// 初始化失败,终止程序return false;}return true;}// 窗口大小改变时调用void framebuffer_size_callback(GLFWwindow *window, int width, int height){// 设置窗口的维度glViewport(0, 0, width, height);}// 输入void processInput(GLFWwindow *window){// 当用户按下esc键,我们设置window窗口的windowShouldClose属性为true// 关闭应用程序if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)glfwSetWindowShouldClose(window, true);}

效果展示:

着色器类模板

每创建一个着色器编写、编译、管理很麻烦,封装通用的Shader模板类可帮助我们省去很多时间,重点关注在效果实现上。

创建着色器程序

创建Shader程序模板头文件shader_s.h, 并放在指定头文件文件夹:

#ifndef SHADER_H#define SHADER_H#include  // 包含glad来获取所有的必须OpenGL头文件#include #include #include #include class Shader{public:// 程序IDunsigned int ID;// 构造器读取并构建着色器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();// 转换数据流到stringvertexCode = vShaderStream.str();fragmentCode = fShaderStream.str();std::cout << "vertexCode: " << vertexCode << std::endl;std::cout << "fragmentCode: " << fragmentCode << std::endl;}catch (std::ifstream::failure &e){std::cout << "ERROR::SHADER::FILE_NOT_SUCCESSFULLY_READ: " << e.what() << std::endl;}const char *vShaderCode = vertexCode.c_str();const char *fShaderCode = fragmentCode.c_str();// 2. 编译着色器unsigned int vertex, fragment;// 顶点着色器vertex = glCreateShader(GL_VERTEX_SHADER); // 创建顶点着色器glShaderSource(vertex, 1, &vShaderCode, NULL); // 把着色器源码附加到着色器对象上glCompileShader(vertex); // 编译着色器checkCompileErrors(vertex, "VERTEX");// 检查编译错误// 片段着色器fragment = glCreateShader(GL_FRAGMENT_SHADER); // 创建片段着色器glShaderSource(fragment, 1, &fShaderCode, NULL); // 把着色器源码附加到着色器对象上glCompileShader(fragment); // 编译着色器checkCompileErrors(fragment, "FRAGMENT");// 检查编译错误 // 着色器程序ID = glCreateProgram();glAttachShader(ID, vertex);glAttachShader(ID, fragment);glLinkProgram(ID);checkCompileErrors(ID, "PROGRAM");// 删除着色器,它们已经链接到我们的程序中了,已经不再需要了glDeleteShader(vertex);glDeleteShader(fragment);}// 使用/激活程序void use(){glUseProgram(ID);}// uniform工具函数void setBool(const std::string &name, bool value) const{glUniform1i(glGetUniformLocation(ID, name.c_str()), (int)value); // 获取uniform变量的位置值,设置值}void setInt(const std::string &name, int value) const{glUniform1i(glGetUniformLocation(ID, name.c_str()), value); // 获取uniform变量的位置值,设置值}void setFloat(const std::string &name, float value) const{glUniform1f(glGetUniformLocation(ID, name.c_str()), value); // 获取uniform变量的位置值,设置值}private:// 检查着色器编译/连接是否成功的工具函数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

创建Shader脚本

顶点着色器文件:shader.vs

#version 330 corelayout (location = 0) in vec3 aPos;layout (location = 1) in vec3 aColor;out vec3 ourColor;void main(){gl_Position = vec4(aPos, 1.0);ourColor = aColor;}

片元着色器文件:shader.fs

#version 330 coreout vec4 FragColor;in vec3 ourColor;void main(){FragColor = vec4(ourColor, 1.0f);}

然后把这两个文件放到指定的文件夹下:

使用模板和Shader脚本文件

需要注意的是,在加载Shader脚本时要,指定正确的脚本路径:

完整脚本

#include #include #include void InitGLFW();bool CreateWindow();bool InitGLAD();// 窗口大小改变时调用void framebuffer_size_callback(GLFWwindow *window, int width, int height);void processInput(GLFWwindow *window);// settings 窗口宽高const unsigned int SCR_WIDTH = 800;const unsigned int SCR_HEIGHT = 600;GLFWwindow *window;int main(){InitGLFW();// 初始化GLFWbool isCreated = CreateWindow(); // 创建一个窗口对象if (!isCreated)return -1;bool isGLAD = InitGLAD(); // 初始化GLAD,传入加载系统相关opengl函数指针的函数if (!isGLAD)return -1;// 构建和编译着色程序Shader ourShader("shader/P1_Basic/03_Shader/shader.vs", "shader/P1_Basic/03_Shader/shader.fs");// 设置顶点数据(和缓冲区)并配置顶点属性// 1.顶点输入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// 顶部};// 2.创建一个顶点缓冲对象(Vertex Buffer Objects, VBO) 顶点数组对象(Vertex Array Object, VAO)unsigned int VBO, VAO;glGenVertexArrays(1, &VAO); // 生成一个VAO对象glGenBuffers(1, &VBO);// 生成一个VBO对象// 3.首先绑定顶点数组对象,然后绑定和设置顶点缓冲区,然后配置顶点属性。glBindVertexArray(VAO);// 绑定VAOglBindBuffer(GL_ARRAY_BUFFER, VBO);// 绑定VBOglBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); // 将顶点数据复制到缓冲区中// 4.配置位置属性glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void *)0);glEnableVertexAttribArray(0);// 5. 颜色属性glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void *)(3 * sizeof(float)));glEnableVertexAttribArray(1);// 注意这是允许的,调用glVertexAttribPointer将VBO注册为顶点属性的绑定顶点缓冲对象,因此之后我们可以安全地解除绑定glBindBuffer(GL_ARRAY_BUFFER, 0);// 你可以在之后解除对VAO的绑定,这样其他VAO调用就不会意外地修改这个VAO,但这种情况很少发生。修改其他//VAOs无论如何都需要调用glBindVertexArray,所以当不直接需要时,我们通常不会取消绑定VAOs(也不会取消绑定vbo)。glBindVertexArray(0);// 取消此调用的注释以绘制线框多边形。// 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);// 记得激活着色器ourShader.use();// 绘制三角形glBindVertexArray(VAO); // 绑定VAOglDrawArrays(GL_TRIANGLES, 0, 3); // 绘制三角形// glBindVertexArray(0); // 不需要每次都绑定VAO// 检查并调用事件,交换缓冲glfwSwapBuffers(window);glfwPollEvents();}// 可选:一旦资源超出其用途,就取消分配所有资源:glDeleteVertexArrays(1, &VAO);glDeleteBuffers(1, &VBO);// 释放/删除之前的分配的所有资源glfwTerminate();std::cout << "Hello, World!" << std::endl;return 0;}void InitGLFW(){// 初始化GLFWglfwInit();// 配置GLFW第一个参数代表选项的名称,我们可以从很多以GLFW_开头的枚举值中选择;// 第二个参数接受一个整型,用来设置这个选项的值。glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);// glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);}bool CreateWindow(){// 创建一个窗口对象window = glfwCreateWindow(800, 600, "LearnOpenGL", NULL, NULL);if (window == NULL){std::cout << "Failed to create GLFW window" << std::endl;// 创建失败,终止程序glfwTerminate();return false;}// 将我们窗口的上下文设置为当前线程的主上下文glfwMakeContextCurrent(window);// 设置窗口大小改变时的回调函数glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);return true;}bool InitGLAD(){// 初始化GLAD,传入加载系统相关opengl函数指针的函数if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)){std::cout << "Failed to initialize GLAD" << std::endl;// 初始化失败,终止程序return false;}return true;}// 窗口大小改变时调用void framebuffer_size_callback(GLFWwindow *window, int width, int height){// 设置窗口的维度glViewport(0, 0, width, height);}// 输入void processInput(GLFWwindow *window){// 当用户按下esc键,我们设置window窗口的windowShouldClose属性为true// 关闭应用程序if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)glfwSetWindowShouldClose(window, true);}