基于C++的OpenGL 02 之着色器
1. 概述
本文基于C++语言,描述OpenGL的着色器
环境搭建以及绘制流程可参考:
笔者这里不过多描述每个名词、函数和细节,更详细的文档可以参考:
2. 着色器
着色器(Shader)是运行在GPU上的小程序,这些小程序为图形渲染管线的某个特定部分而运行
从基本意义上来说,着色器只是一种把输入转化为输出的程序
着色器也是一种非常独立的程序,因为它们之间不能相互通信,它们之间唯一的沟通只有通过输入和输出
着色器语言(英语:Shader Language)也叫着色语言(英语:Shading Language),是一类专门用来为着色器编程的编程语言
Shader Language目前主要有3种语言:
- 基于 OpenGL 的 OpenGL Shading Language,简称 GLSL
- 基于 DirectX 的 High Level Shading Language,简称 HLSL
- 还有 NVIDIA 公司的 C for Graphic,简称 Cg 语言
3. GLSL
OpenGL着色器是使用一种叫GLSL的类C语言写成的,为图形计算量身定制的,包含一些针对向量和矩阵操作的有用特性
着色器的开头总是要声明版本,接着是输入和输出变量、uniform和main函数
每个着色器的入口点都是main函数,在这个函数中我们处理所有的输入变量,并将结果输出到输出变量中
uniform可以理解为globe
,即全局变量
一个典型的OpenGL着色器的GLSL结构如下:
#version version_number in type in_variable_name; in type in_variable_name; out type out_variable_name; uniform type uniform_name; int main() { // 处理输入并进行一些图形操作 ... // 输出处理过的结果到输出变量 out_variable_name = result_we_processed; }
4. 数据类型
GLSL包含的基础变量类型:
- int
- uint
- float
- double
- bool
GLSL包含的容器类型:
- Vector(向量)
- Matrix(矩阵)
4.1 向量
GLSL中的向量可以是包含2-4个分量的容器,分量的类型可以是任意基础类型
向量的类型大致可以表示为:<基础类型首字母>+vec+<分量个数>
如包含3个double分量的向量:dvec3
例外的是float类型,float向量是GLSL中最常用的变量,其类型为:vec+<分量个数>
向量的分量支持.x
、.y
、.z
的方式获取,向量支持重组,例如:
vec2 someVec; vec4 differentVec = someVec.xyxx; vec3 anotherVec = differentVec.zyw; vec4 otherVec = someVec.xxxx + anotherVec.yxzy;
5. 输入与输出
OpenGL中着色器是独立的小程序,每个着色器具有输入与输出,从而进行数据传递
GLSL定义了in
和out
两个关键字实现这种流程
前面的着色器使用out
定义的变量会传递到后面的着色器中用in
声明且类型与变量名相同的变量
例外的是:
- 顶点着色器使用
layout (location = <n>)
指定输入变量(也可以用glGetAttribLocation()查询属性位置) - 片段着色器需要
vec4
类型的颜色输出
综述就是,一般而言:
in
和out
两个关键字主要用于顶点着色器与片段着色器的数据传递- 顶点着色器需要使用
location
进行输入,输出中要指定gl_Position
- 片段着色器需要指定输出颜色
一个简单的示例代码如下:
顶点着色器:
#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(1.0, 0.0, 0.0, 1.0); // 把输出变量设置为红色 }
片段着色器:
#version 330 core out vec4 FragColor; in vec4 vertexColor; // 从顶点着色器传来的输入变量(名称相同、类型相同) void main() { FragColor = vertexColor; }
这里只是修改GLSL,环境代码在这片文章的结尾:
实现的效果:
6. Uniform
in
和out
两个关键字主要用于着色器的数据传递,Uniform则主要用于CPU与GPU的数据传递
Uniform是全局变量,变量名不可重复,可以被任何着色器随时读取
以下是在片段着色器中声明一个Uniform的颜色变量:
#version 330 core out vec4 FragColor; uniform vec4 ourColor; // 在OpenGL程序代码中设定这个变量 void main() { FragColor = ourColor; }
接下来是在渲染中动态设置这个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);
一切正常的话,将会出现一个三角形逐渐由绿变黑再变回绿色:
一些想法:
- 既然说Uniform是全局变量,那是否可以在没有声明Uniform的着色器中访问呢?经过笔者的实验,似乎不可以
7. 绑定更多属性
利用Uniform可以实现颜色从CPU到GPU的传递,但是变量多时声明很多Uniform就不那么合适,另一个方案是将颜色属性绑定到顶点数据中,从顶点着色器传递到片段着色器中,代码如下
设置顶点与对应的颜色:
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 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设置为我们从顶点数据那里得到的输入颜色 }
在片段着色器中接收颜色属性:
#version 330 core out vec4 FragColor; in vec3 ourColor; void main() { FragColor = vec4(ourColor, 1.0); }
绑定属性:
现在的VBO内存布局如下:
绑定VBO的顶点格式:
// 位置属性 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);
运行程序结果:
8. 封装着色器
着色器程序的生成步骤大致都是:
- 编写GLSL
- 创建Shader
- 加载GLSL
- 编译Shader
- 创建着色器程序
- 附加Shader到着色器程序
- 链接着色器程序
封装一个着色器类,可以有效简化创建一个着色器程序的步骤
这是封装的Shader.hpp:
#ifndef SHADER_HPP #define SHADER_HPP #include <glad/glad.h> // 包含glad来获取所有的必须OpenGL头文件 #include <string> #include <fstream> #include <sstream> #include <iostream> class Shader { public: // 程序ID unsigned 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(); // 转换数据流到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; }; // 片段着色器也类似 fragment = glCreateShader(GL_FRAGMENT_SHADER); glShaderSource(fragment, 1, &fShaderCode, NULL); glCompileShader(fragment); // 打印编译错误(如果有的话) glGetShaderiv(fragment, GL_COMPILE_STATUS, &success); if (!success) { glGetShaderInfoLog(fragment, 512, NULL, infoLog); std::cout << "ERROR::SHADER::FRAGMENT::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); } // 使用/激活程序 void use() { glUseProgram(ID); } // uniform工具函数 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); } }; #endif
以下是一个简单的调用程序test.cpp
:
#include <glad/glad.h> #include <GLFW/glfw3.h> #include <iostream> #include <math.h> #include "Shader.hpp" void framebuffer_size_callback(GLFWwindow *window, int width, int height); void process_input(GLFWwindow *window); unsigned int *renderInit(); void render(unsigned int shaderProgram, unsigned int VAO); bool checkCompile(unsigned int shader); bool checkProgram(unsigned int shaderProgram); int main() { glfwInit(); glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); GLFWwindow *window = glfwCreateWindow(800, 600, "hello triangle", nullptr, nullptr); if (window == nullptr) { std::cout << "Faild to create window" << std::endl; glfwTerminate(); } glfwMakeContextCurrent(window); if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) { std::cout << "Faild to initialize glad" << std::endl; return -1; } glad_glViewport(0, 0, 800, 600); glfwSetFramebufferSizeCallback(window, framebuffer_size_callback); unsigned int *arr = renderInit(); while (!glfwWindowShouldClose(window)) { process_input(window); // render std::cout << arr[0] << " " << arr[1] << " " << arr[2] << std::endl; render(arr[0], arr[1]); glfwSwapBuffers(window); glfwPollEvents(); } glDeleteProgram(arr[0]); glDeleteVertexArrays(1, &arr[1]); glDeleteBuffers(1, &arr[2]); glfwTerminate(); return 0; } void framebuffer_size_callback(GLFWwindow *window, int width, int height) { glViewport(0, 0, width, height); } void process_input(GLFWwindow *window) { if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS) { glfwSetWindowShouldClose(window, true); } } unsigned int *renderInit() { unsigned int VAO; glGenVertexArrays(1, &VAO); glBindVertexArray(VAO); float vertices[] = { 0.0f, 0.5f, 0.0f, -0.5f, -0.5f, 0.0f, 0.5f, -0.5f, 0.0f}; unsigned int VBO; glGenBuffers(1, &VBO); 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); Shader shaderProgram = Shader("test.vs", "test.fs"); shaderProgram.use(); return new unsigned int[3]{shaderProgram.ID, VAO, VBO}; } void render(unsigned int shaderProgram, unsigned int VAO) { glClearColor(0.2, 0.3, 0.3, 1.0); glClear(GL_COLOR_BUFFER_BIT); glUseProgram(shaderProgram); glBindVertexArray(VAO); glDrawArrays(GL_TRIANGLES, 0, 3); }
test.vs
如下:
#version 330 core layout (location = 0) in vec3 aPos; // 位置变量的属性位置值为 0 void main() { gl_Position = vec4(aPos, 1.0); }
test.fs
如下:
#version 330 core out vec4 FragColor; void main() { FragColor = vec4(0.0, 1.0, 0.0, 1.0); }
9. 参考资料
[1]着色器 - LearnOpenGL CN (learnopengl-cn.github.io)
[3]三大 Shader 编程语言(CG/HLSL/GLSL) - 知乎 (zhihu.com)
[4]OpenGL学习笔记(四)着色器 - 知乎 (zhihu.com)
[5]着色器语言 GLSL (opengl-shader-language)入门大全 - 善未易明 - 博客园 (cnblogs.com)
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了