OpenGL-1 初识
Dionysen

OpenGL本身是一种规范,只是规定了一些应有的函数和参数,没有任何实现(实现由第三方库完成,如glfw、glew).

image

本文之所以放在[编程]->[框架]的分类,是因为本文主要内容是用OpenGL规范实现的库的使用,而非OpenGL规范本身

图形渲染管线是实时渲染的核心组件。渲染管线的功能是通过给定虚拟相机、3D场景物体以及光源等场景要素来产生或者渲染一副2D的图像。渲染管线是实时渲染的重要工具,主要包括两个功能:一是将物体3D坐标转变为屏幕空间2D坐标,二是为屏幕每个像素点进行着色。

渲染管线的一般流程分别是:顶点数据的输入、顶点着色器、曲面细分过程、几何着色器、图元组装、裁剪剔除、光栅化、片段着色器以及混合测试

OpenGL中,所有事物都是在3D空间中的,而屏幕是2D,因此必须把三维的坐标转换成二维坐标。

图形渲染管线

图形渲染管线(Graphics Pipeline): 一个原始数据,经过一定变化和处理,最终显示在屏幕上。

主要有两个步骤:

  1. 把3D坐标转换为2D坐标
  2. 把2D坐标转变为实际的有颜色的像素

2D坐标和像素也是不同的,2D坐标精确表示一个点在2D空间中的位置,而2D像素是这个点的近似值,2D像素受到你的屏幕/窗口分辨率的限制。

可被划分为几个阶段,连接而成,都具有特定的函数,很容易执行(正因为容易执行,才可以在GPU上运行成千上万个各自阶段的小程序,这些小程序为着色器).

着色器运行在GPU上,节省了CPU的(宝贵的)时间.

OpenGL着色器是用OpenGL着色器语言(OpenGL Shading Language, GLSL)写成的

具体阶段:

image

  1. 输入数组,也即**顶点数据(Vertex Data),它是用顶点属性(Vertex Attribute)**表示的,如坐标、颜色等
  2. 使用图元(Primitive)可以告诉OpenGL把顶点渲染成什么样,如一系列点、一系列三角形或线,可用的**提示(Hint)**如GL_POINTSGL_TRIANGLESGL_LINE_STRIP
  3. 图形渲染管线的第一个部分是顶点着色器(Vertex Shader),目的是把3D坐标数据转换成另一种3D坐标**,同时允许我们对顶点属性进行一些基本处理
  4. 图元装配是将顶点着色器输出的所有顶点作为输入,并所有的点装配成指定图元的形状(如果是GL_POINTS,那么就是一个顶点),如三角形
  5. 图元装配阶段的输出会传递给**几何着色器(Geometry Shader)**。几何着色器把图元形式的一系列顶点的集合作为输入,它可以通过产生新顶点构造出新的(或是其它的)图元来生成其他形状;例子中,它生成了另一个三角形
  6. 几何着色器的输出会被传入**光栅化阶段(Rasterization Stage),这里它会把图元映射为最终屏幕上相应的像素,生成供片段着色器(Fragment Shader)使用的片段(Fragment)。在片段着色器运行之前会执行裁切(Clipping)**。裁切会丢弃超出你的视图以外的所有像素,用来提升执行效率

OpenGL中的一个片段是OpenGL渲染一个像素所需的所有数据

片段着色器的主要目的是计算一个像素的最终颜色,这也是所有OpenGL高级效果产生的地方。通常,片段着色器包含3D场景的数据(比如光照、阴影、光的颜色等等),这些数据可以被用来计算最终像素的颜色

  1. 在所有对应颜色值确定以后,最终的对象将会被传到最后一个阶段,我们叫做Alpha测试和混合(Blending)阶段。这个阶段检测片段的对应的深度(和模板(Stencil))值(后面会讲),用它们来判断这个像素是其它物体的前面还是后面,决定是否应该丢弃。这个阶段也会检查alpha值(alpha值定义了一个物体的透明度)并对物体进行**混合(Blend)**;所以,即使在片段着色器中计算出来了一个像素输出的颜色,在渲染多个三角形的时候最后的像素颜色也可能完全不同

第一个三角形

此处给出源码和详细注释,结构清晰:

#include "glad/glad.h"

#include <GLFW/glfw3.h>
#include <iostream>

void framebuffer_size_callback(GLFWwindow* window, int width, int height); //回调函数的声明
void processInput(GLFWwindow* window); // 处理对窗口的输入

// --------------- 以字符串的形式定义着色器程序的源码,第4步需要用到 ---------------
const char
* vertexShaderSource = // 创建顶点着色器程序的源码,使用c风格的常量字符串存储
"#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";

// 创建片段着色器程序的源码,使用c风格的常量字符串存储
const char* fragmentShaderSource =
"#version 330 core\n"
"out vec4 FragColor;\n"
"void main() {\n"
"FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);\n" // 此处设置片段颜色为黄色
"}\0";

int main(void) {
// -------------- 1. glfw的初始化 ----------------

if (!glfwInit()) {
return -1;
}

// --------------- 2. 使用glfw创建窗口 ---------------
GLFWwindow* window = glfwCreateWindow(800, 600, "LearnOpenGL", NULL, NULL);
if (window == NULL) {
std::cout << "Failed to create GLFW window" << std::endl;
glfwTerminate();
return -1;
}

glfwMakeContextCurrent(window); // 此函数使指定窗口的 OpenGL 或 OpenGL ES 上下文成为调用线程的当前上下文
glfwSetFramebufferSizeCallback(
window,
framebuffer_size_callback); // 回调函数,保证每次窗口大小调整时,重新绘制
// --------------- 3. glad:加载OpenGL所有的函数指针 ---------------
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) {
std::cout << "Failed to initialize GLAD" << std::endl;
return -1;
}

// --------------- 4. 创建和编译着色器 ---------------
unsigned int vertexShader; // 创建顶点着色器索引
vertexShader = glCreateShader(GL_VERTEX_SHADER); // 创建顶点着色器
glShaderSource(
vertexShader, 1, &vertexShaderSource,
NULL); // 为顶点着色器添加源码,第二个参数是添加的源码中字符串的数量,第三个先设置为NULL
glCompileShader(vertexShader); // 编译顶点着色器

int success; // 创建编译状态指示变量
char info_log[512]; // 创建用以存储log的数组
glGetShaderiv(vertexShader, GL_COMPILE_STATUS,
&success); // 获取状态和log,输出到变量和数组中
if (!success) { // 如果编译失败,打印错误信息和log
glGetShaderInfoLog(vertexShader, 512, NULL, info_log);
std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n"
<< info_log << std::endl;
}

unsigned int fragmentShader;
fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
glCompileShader(fragmentShader);

glGetShaderiv(fragmentShader, GL_COMPILE_STATUS,
&success); // 获取状态和log,输出到变量和数组中
if (!success) { // 如果编译失败,打印错误信息和log
glGetShaderInfoLog(fragmentShader, 512, NULL, info_log);
std::cout << "ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n"
<< info_log << std::endl;
}
// --------------- 5. 链接着色器 ---------------

unsigned int shaderProgram;
shaderProgram = glCreateProgram();

// 把着色器添加到着色器程序中
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram); // 链接着色器程序

glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success); // 异常检测
if (!success) {
glGetProgramInfoLog(shaderProgram, 512, NULL, info_log);
std::cout << "ERROR::SHADER::PROGRAM::LINK_FAILED\n"
<< info_log << std::endl;
}

glUseProgram(
shaderProgram); // 调用此函数后,渲染和着色器调用都会使用此前所写的着色器了
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader); // 链接完成之后就可以删除着色器了

// --------------- 6. 创建顶点数据缓冲区 ---------------
float vertices[] =
{ // 第一个三角形
0.5f, 0.5f, 0.0f, // 右上角
0.5f, -0.5f, 0.0f, // 右下角
-0.5f, -0.5f, 0.0f, // 左下角
-0.5f, 0.5f, 0.0f }; // 左上角
unsigned int indices[] = {
// 注意索引从0开始!
// 此例的索引(0,1,2,3)就是顶点数组vertices的下标,
// 这样可以由下标代表顶点组合成矩形

0, 1, 3, // 第一个三角形
1, 2, 3 // 第二个三角形
};
//{ -0.5f, -0.5f, 0.0f, // left
// 0.5f, -0.5f, 0.0f, // right
// 0.0f, 0.5f, 0.0f }; // top
unsigned int EBO;
glGenBuffers(1, &EBO);

unsigned int
VBO; // 缓冲区类似socket编程中的文件描述符,buffer作为唯一的标识来表示生成的一个缓冲区,GPU可以通过这个标识来读取缓冲区的数据,进而绘制出图形;它是一个整数
// VBO是顶点缓冲对象
glGenBuffers(1, &VBO); // 创建缓冲区
glBindBuffer(GL_ARRAY_BUFFER, VBO); // 绑定缓冲区
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// -------- 创建顶点数组对象 ----------
unsigned int VAO;
glGenVertexArrays(1, &VAO);
glBindVertexArray(VAO);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
// 此时可以直接给缓冲区一个数据,或者不给数据,后面再给数据然后更新缓冲区
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
// -------- 告诉GPU如何读取顶点数据 ----------
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float),
(void*)0);
glEnableVertexAttribArray(0); // 启用顶点属性

glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);

// --------------- 7. 窗口绘制循环 ---------------
while (!glfwWindowShouldClose(window)) {
// input
// -----
processInput(window);
// render
// -----
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
glUseProgram(shaderProgram);
//glBindVertexArray(VAO); // seeing as we only have a single VAO there's
//// no need to bind it every time, but we'll do
//// so to keep things a bit more organized
//glDrawArrays(GL_TRIANGLES, 0, 3);
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
glBindVertexArray(0);
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
glfwSwapBuffers(window);
glfwPollEvents();
}

// glfw: terminate, clearing all previously allocated GLFW resources.
// ------------------------------------------------------------------
glfwTerminate();
return 0;
}
// process all input: query GLFW whether relevant keys are pressed/released this
// frame and react accordingly
// ---------------------------------------------------------------------------------------------------------
void processInput(GLFWwindow* window) {
if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
glfwSetWindowShouldClose(window, true);
}

// glfw: whenever the window size changed (by OS or user resize) this callback
// function executes
// ---------------------------------------------------------------------------------------------
void framebuffer_size_callback(GLFWwindow* window, int width, int height) {
// make sure the viewport matches the new window dimensions; note that width
// and height will be significantly larger than specified on retina
// displays.
glViewport(0, 0, width, height);
}

概念与作用

第一个三角形涉及到一些概念和特性,最引人注目的是三个对象,即:

  • 顶点数组对象:Vertex Array Object,VAO
  • 顶点缓冲对象:Vertex Buffer Object,VBO
  • 元素缓冲对象:Element Buffer Object,EBO 或 索引缓冲对象 Index Buffer Object,IBO

标准化设备坐标

标准化设备坐标(Normalized Device Coordinates):OpenGL会将坐标转化为单位坐标,即所有轴上的大小范围为(-1, 1)

顶点缓冲对象

顶点缓冲对象(Vertex Buffer Objects, VBO):管理顶点的内存,在显存中储存大量顶点

这样可以一次性发送大量数据到显卡,而不用每次绘制都到cpu的内存中读取数据

  1. 创建
unsigned int VBO; //创建ID,类似套接字,可以通过此ID访问此对象(唯一绑定)
glGenBuffers(1, &VBO);//生成
glBindBuffer(GL_ARRAY_BUFFER, VBO);//绑定,将生成的VBO与GL_ARRAY_BUFFER绑定,从此任何对GL_ARRAY_BUFFER的调用都会操作当前绑定的VBO
// 如下面的操作:
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
//此函数将vertices的数据拷贝到缓冲GL_ARRAY_BUFFER中,因为之前绑定了VBO,所以实际上拷贝到了VBO上

glBufferData函数的最后一个参数是指定显卡管理数据的方式:

  • GL_STATIC_DRAW :数据不会或几乎不会改变,修改一次,使用多次
  • GL_DYNAMIC_DRAW:数据会被改变很多,修改多次,使用多次
  • GL_STREAM_DRAW :数据每次绘制时都会改变,每次都会修改和使用
  1. 使用
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float),
(void *)0);
// 链接顶点属性,即告诉OpenGL如何解释顶点对象中的数据
glEnableVertexAttribArray(0); // 启用顶点属性

// 然后即可在窗口事件循环中绘制:
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
glUseProgram(shaderProgram);
glBindVertexArray(VBO);
glDrawArrays(GL_TRIANGLES, 0, 3);

glVertexAttribPointer函数参数:

  • 第一个参数指定我们要配置的顶点属性。还记得我们在顶点着色器中使用layout(location = 0)定义了position顶点属性的位置值(Location)吗?它可以把顶点属性的位置值设置为0。因为我们希望把数据传递到这一个顶点属性中,所以这里我们传入0
  • 第二个参数指定顶点属性的大小。顶点属性是一个vec3,它由3个值组成,所以大小是3。
  • 第三个参数指定数据的类型,这里是GL_FLOAT(GLSL中vec*都是由浮点数值组成的)。
  • 下个参数定义我们是否希望数据被标准化(Normalize)。如果我们设置为GL_TRUE,所有数据都会被映射到0(对于有符号型signed数据是-1)到1之间。我们把它设置为GL_FALSE。
  • 第五个参数叫做步长(Stride),它告诉我们在连续的顶点属性组之间的间隔。由于下个组位置数据在3个float之后,我们把步长设置为3 * sizeof(float)。要注意的是由于我们知道这个数组是紧密排列的(在两个顶点属性之间没有空隙)我们也可以设置为0来让OpenGL决定具体步长是多少(只有当数值是紧密排列时才可用)。一旦我们有更多的顶点属性,我们就必须更小心地定义每个顶点属性之间的间隔,我们在后面会看到更多的例子(译注: 这个参数的意思简单说就是从这个属性第二次出现的地方到整个数组0位置之间有多少字节)。
  • 最后一个参数的类型是void*,所以需要我们进行这个奇怪的强制类型转换。它表示位置数据在缓冲中起始位置的偏移量(Offset)。由于位置数据在数组的开头,所以这里是0。我们会在后面详细解释这个参数。

元素缓冲对象

元素缓冲对象(Element Buffer Object,EBO):当有重复的顶点需要绘制时,不需要定义出相同的顶点,而是使用索引来引用重复的顶点

这样只需要定义出不重复的所有顶点,需要哪个顶点时使用索引找到并使用它即可,这是EBO的工作方式

  1. 创建顶点数组和索引
// 绘制两个三角形组合成矩形,本来需要六个顶点,但是有两组重复的顶点
float vertices[] = {
// 第一个三角形
0.5f, 0.5f, 0.0f, // 右上角
0.5f, -0.5f, 0.0f, // 右下角
-0.5f, 0.5f, 0.0f, // 左上角
// 第二个三角形
0.5f, -0.5f, 0.0f, // 右下角
-0.5f, -0.5f, 0.0f, // 左下角
-0.5f, 0.5f, 0.0f // 左上角
};

// 下面只定义四个顶点,使用索引就可以构造出两个三角形
float vertices[] = {
0.5f, 0.5f, 0.0f, // 右上角
0.5f, -0.5f, 0.0f, // 右下角
-0.5f, -0.5f, 0.0f, // 左下角
-0.5f, 0.5f, 0.0f // 左上角
};
unsigned int indices[] = {
// 注意索引从0开始!
// 此例的索引(0,1,2,3)就是顶点数组vertices的下标,
// 这样可以由下标代表顶点组合成矩形
0, 1, 3, // 第一个三角形
1, 2, 3 // 第二个三角形
};
  1. 创建EBO(与VBO类似)
unsigned int VBO; 
glGenBuffers(1, &VBO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

unsigned int EBO;
glGenBuffers(1, &EBO);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); // 注意绑定的缓冲类型为GL_ELEMENT_ARRAY_BUFFER
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
  1. 使用
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float),
(void *)0);
// 链接顶点属性,即告诉OpenGL如何解释顶点对象中的数据
glEnableVertexAttribArray(0); // 启用顶点属性

//绘制时使用glDrawElements替换glDrawArrays
glBindVertexArray(VBO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);

必须结合VBO才能使用EBO,因为EBO只储存了索引,而没有顶点数据

顶点数组对象

顶点数组对象(Vertex Array Object, VAO):主要用于管理 VBO 或 EBO ,减少glBindBufferglEnableVertexAttribArrayglVertexAttribPointer 这些调用操作,高效地实现在顶点数组配置之间切换。

VAO的简单使用:

// 创建VAO
unsigned 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
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); // 启用顶点属性

//解绑
glBindBuffer(GL_ARRAY_BUFFER, 0); // 解绑buffer
glBindVertexArray(0); // 解绑VAO

其他

实践表明,无法使用同一个VAO绑定不同的VBO,画出两个不同的图形,一般是多个VAO分别对应多个VBO。一个EBO可以画出多个图形,但只是从预先设置好的数组中读取顶点数据,只不过可以重复使用顶点。

词汇表

  • OpenGL: 一个定义了函数布局和输出的图形API的正式规范。
  • GLAD: 一个扩展加载库,用来为我们加载并设定所有OpenGL函数指针,从而让我们能够使用所有(现代)OpenGL函数。
  • **视口(Viewport)**: 我们需要渲染的窗口。
  • **图形管线(Graphics Pipeline)**: 一个顶点在呈现为像素之前经过的全部过程。
  • **着色器(Shader)**: 一个运行在显卡上的小型程序。很多阶段的图形管道都可以使用自定义的着色器来代替原有的功能。
  • **标准化设备坐标(Normalized Device Coordinates, NDC)**: 顶点在通过在剪裁坐标系中剪裁与透视除法后最终呈现在的坐标系。所有位置在NDC下-1.0到1.0的顶点将不会被丢弃并且可见。
  • **顶点缓冲对象(Vertex Buffer Object)**: 一个调用显存并存储所有顶点数据供显卡使用的缓冲对象。
  • **顶点数组对象(Vertex Array Object)**: 存储缓冲区和顶点属性状态。
  • **元素缓冲对象(Element Buffer Object,EBO),也叫索引缓冲对象(Index Buffer Object,IBO)**: 一个存储元素索引供索引化绘制使用的缓冲对象。
  • Uniform: 一个特殊类型的GLSL变量。它是全局的(在一个着色器程序中每一个着色器都能够访问uniform变量),并且只需要被设定一次。
  • **纹理(Texture)**: 一种包裹着物体的特殊类型图像,给物体精细的视觉效果。
  • **纹理环绕(Texture Wrapping)**: 定义了一种当纹理顶点超出范围(0, 1)时指定OpenGL如何采样纹理的模式。
  • **纹理过滤(Texture Filtering)**: 定义了一种当有多种纹素选择时指定OpenGL如何采样纹理的模式。这通常在纹理被放大情况下发生。
  • **多级渐远纹理(Mipmaps)**: 被存储的材质的一些缩小版本,根据距观察者的距离会使用材质的合适大小。
  • stb_image.h: 图像加载库。
  • **纹理单元(Texture Units)**: 通过绑定纹理到不同纹理单元从而允许多个纹理在同一对象上渲染。
  • **向量(Vector)**: 一个定义了在空间中方向和/或位置的数学实体。
  • **矩阵(Matrix)**: 一个矩形阵列的数学表达式。
  • GLM: 一个为OpenGL打造的数学库。
  • **局部空间(Local Space)**: 一个物体的初始空间。所有的坐标都是相对于物体的原点的。
  • **世界空间(World Space)**: 所有的坐标都相对于全局原点。
  • **观察空间(View Space)**: 所有的坐标都是从摄像机的视角观察的。
  • **裁剪空间(Clip Space)**: 所有的坐标都是从摄像机视角观察的,但是该空间应用了投影。这个空间应该是一个顶点坐标最终的空间,作为顶点着色器的输出。OpenGL负责处理剩下的事情(裁剪/透视除法)。
  • **屏幕空间(Screen Space)**: 所有的坐标都由屏幕视角来观察。坐标的范围是从0到屏幕的宽/高。
  • LookAt矩阵: 一种特殊类型的观察矩阵,它创建了一个坐标系,其中所有坐标都根据从一个位置正在观察目标的用户旋转或者平移。
  • **欧拉角(Euler Angles)**: 被定义为偏航角(Yaw),俯仰角(Pitch),和滚转角(Roll)从而允许我们通过这三个值构造任何3D方向。
显示评论