相关库
- OpenGL 渲染管线(规范的接口文档+各平台实现不同)
- GLAD 确定当前显卡驱动版本下OpenGL函数的位置(由于OpenGL驱动版本众多,它大多数函数的位置都无法在编译时确定下来,需要在运行时查询)用来管理OpenGL的函数指针。
- GLFW 窗口操作、定义OpenGL上下文、处理用户输入(区分OpenGL概念下的视口)
创建、初始化与释放
对窗口注册回调函数(创建后 循环前)
渲染循环 Render Loopvoid func_callback(){} // 自定义处理操作 glfwSetxxx(GLFWwindows*, func_callback); // 绑定某事件触发时的处理操作
while(!glfwWindowShouldClose(window)) { // 每循环的开始前检查一次GLFW是否被要求退出 // ... // 放入所有渲染操作 glfwSwapBuffers(window); // 绘制GLFW窗口各像素颜色值的缓冲结果 glfwPollEvents(); // 检查是否触发事件并调用相应callback函数 }
单缓冲渲染生成的图像不是一下子被绘制出来的,而是按照从左到右,由上而下逐像素地绘制而成的。最终图像不是在瞬间显示给用户,而是通过一步一步生成的,这会导致渲染的结果很不真实,时可能出现图像闪烁。双缓冲(Double Buffer)机制下,前缓冲保存着最终输出的图像,它会在屏幕上显示;而所有的的渲染指令都会在后缓冲上绘制。当所有的渲染指令执行完毕后,我们Swap前缓冲和后缓冲,这样图像就立即呈显出来,之前提到的不真实感就消除了。
渲染管线 Graphics Pipeline
抽象各阶段
着色器(Shader)是运行在GPU上的小程序。这些小程序为图形渲染管线的某个特定部分而运行
着色器之间不能相互通信,沟通只有通过阶段性的输入和输出
必须定义至少一个顶点着色器和一个片段着色器(GPU中没有默认的顶点/片段着色器)
- 顶点着色器
3D坐标转换为 标准化设备坐标 Normalized Device Coordinates, NDC(x、y和z范围在-1.0到1.0之间,且y轴向上) - 图元装配
图元类型有 GL_POINTS、GL_TRIANGLES、GL_LINE_STRIP - 几何着色器
把图元形式的一系列点集作为输入,通过产生新顶点构造出新的(或是其它的)图元来生成其他形状 - 光栅化阶段
还会进行视图剪裁 - 片段着色器
计算一个像素的最终颜色,包含3D场景的数据(比如光照、阴影、光的颜色等等),会进行Fragment Interpolation(利用所有输入属性进行插值) - Alpha测试和混合(Blending)阶段
遮挡效果和色彩融合
渲染数据准备
-
定义顶点缓冲对象 Vertex Buffer Object VBO
开辟存储顶点数据的显存,当数据从内存发送至显存后,顶点着色器几乎能立即访问顶点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 }; unsigned int VBO; glGenBuffers(1, &VBO); // 创建对象 glBindBuffer(GL_ARRAY_BUFFER, VBO); // 绑定对象至上下文, 同时指定对象的具体类型 glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); // 一些操作或设置 // 允许程序存在多个VBO, 根据当前状态绑定的VBO来获取顶点数据
-
定义顶点数组对象 Vertex Array Object VAO
如何解析顶点缓冲数据(顶点属性)glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0); // 链接位置属性 // glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3* sizeof(float))); // 颜色属性 // 首个参数:要配置哪个顶点属性,即属性对应的location值 // 倒数第二个:步长 // 最后参数:该属性的数据在缓冲中起始位置的偏移量 glEnableVertexAttribArray(0); // 启用
当有多个顶点属性需要配置时使用 VAO
unsigned int VAO; glGenVertexArrays(1, &VAO); // 创建顶点数组对象 glBindVertexArray(VAO); // 绑定 // 创建VBO unsigned int VBO; glGenBuffers(1, &VBO); glBindBuffer(GL_ARRAY_BUFFER, VBO); // 绑定顶点数据缓冲 glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); // 复制 // 创建EBO Element Buffer Object 元素缓冲对象 unsigned int EBO; glGenBuffers(1, &EBO); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); // 绑定顶点索引缓冲 glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW); // 顶点属性指针 glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); // 设置 glEnableVertexAttribArray(0); // 激活 // 配置完所有 VAO(数据缓存在哪, 如何解释) // .. 绘制代码(渲染循环中).. glUseProgram(shaderProgram); glBindVertexArray(VAO); glDrawArrays(GL_TRIANGLES, 0, 3); glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
可编程着色器 Shader
-
编程语言 GLSL opengl shading language
除了支持C的基本数据类型外,还定义了向量与矩阵
对于顶点着色器,每个输入变量又叫做顶点属性,有内存上限,一般为16个(4分量)如果打算从一个着色器向另一个着色器发送数据,我们必须在发送方着色器中声明一个输出,在接收方着色器中声明一个类似的输入。当类型和名字都一样的时候,OpenGL就会把两个变量链接到一起,它们之间就能发送数据了(这是在链接程序对象时完成的)。
Uniform是一种从CPU中的应用向GPU中的着色器发送数据的方式(程序和着色器间的数据交互)
但uniform和顶点属性有些不同。首先,uniform是全局的(Global)。全局意味着uniform变量必须在每个着色器程序对象中都是独一无二的,而且它可以被着色器程序的任意着色器在任意阶段访问。第二,无论你把uniform值设置成什么,uniform会一直保存它们的数据,直到它们被重置或更新。
#version version_number in type in_var; in type in_var; out type out_var; uniform type uniform_name; int main() { ... out_var = xxx; }
-
OpenGL 代码运行时动态编译着色器的源代码
// GLSL 暂时放入文件顶部的c风格字符串中 // in 关键字声明所有的输入顶点属性(可以有多个)同时设定输入变量的 location // 解析数据, 绑定顶点属性指针时需要指定 location 参数,以指明绑定到哪个顶点属性上 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"; // openGL渲染部分 /* 创建可编程着色器对象 */ unsigned int vertexShader; vertexShader = glCreateShader(GL_VERTEX_SHADER); // 创建着色器对象 glShaderSource(vertexShader, 1, &vertexShaderSource, NULL); // 附加源码 glCompileShader(vertexShader); // 编译 /* 多个着色器合并链接,创建着色器程序对象 */ unsigned int shaderProgram; shaderProgram = glCreateProgram(); glAttachShader(shaderProgram, vertexShader); glAttachShader(shaderProgram, fragmentShader); glLinkProgram(shaderProgram); glUseProgram(shaderProgram); // 激活 glDeleteShader(vertexShader); glDeleteShader(fragmentShader); int location = glGetUniformLocation(shaderProgram, "某个Uniform变量名"); // 查询Uniform变量地址, 不要求着色器程序激活 glUniformNX(location, 0.0f, greenValue, 0.0f, 1.0f); // 更新Uniform变量值, 要求着色器激活
-
GLSL定义自己的着色器,并编写相关接口方便复用
// GLSL将自定义着色器写入某个文本文件,存入硬盘 // 1. for example, shader.vs/shader.fs
// c++ 类的头文件 // 2. shader.h #ifndef SHADER_H #define SHADER_H #include <glad/glad.h>; // 包含glad来获取所有的必须OpenGL头文件 #include <string> #include <fstream> #include <sstream> #include <iostream> class Shader { public: unsigned int ID; // 程序ID Shader(const char* vertexPath, const char* 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; }; // 3. 当然还要定义实现 shader.cpp
// 4. 某个项目 myProgram.cpp 使用自定义着色器 Shader ourShader("path/to/shaders/shader.vs", "path/to/shaders/shader.fs"); ... while(...) { ourShader.use(); ourShader.setFloat("someUniform", 1.0f); DrawStuff(); }
坐标系统及转换
-
glm 库
OpenGL不自带任何的矩阵和向量操作定义。使用 glm库(OpenGL Mathematics)完成相关数学运算(它只有头文件) -
坐标系统
Local/Object Space 物体
World Space 多个物体+相机系统
View/Camera/Eye Space 相机空间
Clip Space 裁剪+缩放+各分量归一化后的相机空间,决定最终显示的内容范围、有无变形等等
投影矩阵创建平截头体 Frustum 进行剪裁和缩放(此处投影相比多视几何的投影概念范围更小,仅指相机坐标系到像平面的映射),然后通过 透视除法 Perspective Division 将齐次坐标变为 3D 标准化设备坐标
Screen Space 2维屏幕
Viewport Transform 视口变换将位于-1.0到1.0范围的坐标变换到由glViewport函数所定义的坐标范围内