本节内容

  • 第一个三角形

Hello Triangle

在本章之前首先需要明确以下几个名词

  • VAO(Vertex Array Object): 顶点数组对象,用于存储顶点数据
  • VBO(Vertex Buffer Object): 顶点缓冲对象,用于存储顶点数据
  • EBO(Element Buffer Object): 元素缓冲对象,用于存储索引数据
  • IBO(Index Buffer Object): 索引缓冲对象,用于存储索引数据

OpenGL 渲染管线

alt text

  • 顶点着色器: 将顶点转换到NDC(Normalized Device Coordinates)
  • 几何着色器: 处理顶点的几何变换
  • 图元装配: 将顶点组成图元
  • 光栅化: 将图元转换为像素
  • 片段着色器: 计算每个像素的颜色
  • 测试混合: 融合颜色

输入顶点数据

由于我们想要渲染一个三角形,所以首先需要指定三个顶点.

float vertices[] = {
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
0.0f, 0.5f, 0.0f
};

这里将顶点位置已经设定在了NDC上,即-1到1的范围内.

接下来我们将顶点数据发送到显卡的显存中

float vertices[] = {
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
0.0f, 0.5f, 0.0f
};//顶点坐标
unsigned int VBO;//声明一个VBO
glGenBuffers(1, &VBO);//生成一个VBO,并ID存储在VBO中,此时VBO拥有了其唯一的id
glBindBuffer(GL_ARRAY_BUFFER, VBO);//VBO到GL_ARRAY_BUFFER目标,GL_ARRAY_BUFFEVBO(顶点缓冲对象)的缓冲类型
glBufferData(GL_ARRAY_BUFFER, size(vertices), vertices, GL_STATIC_DRAW);//将顶点数据复制到缓冲中,并指定数据用以静态访问

glBufferData函数第四个参数指定了显卡如何管理给定数据,有三种形式:

  • GL_STATIC_DRAW: 仅在显存上进行读取,不进行写入,适用于静态数据
  • GL_DYNAMIC_DRAW: 仅在显存上进行读取,并允许写入,数据经常改变
  • GL_STREAM_DRAW: 仅在显存上进行读取,不进行写入,数据每次绘制都改变

顶点着色器

在CPU阶段,我们使用C++作为我们处理数据的语言.然而在显卡上则需要使用到着色器语言.OpenGL使用的是GLSL(OpenGL Shading Language)作为它的着色器语言.

这里先使用一个非常简单的着色器,将顶点数据输出给几何着色器.

#version 330 core
layout (location = 0) in vec3 aPos;//输入数据,类型为vec3,保存到变量 aPos中

void main()
{
gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);
}

编译着色器

为了方便,我们先暂时将Vertex Shader保存在一个字符串中.

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";

为了让显卡可以运行这个Shader,我们需要在运行时动态编译这个着色器.

输入以下代码

unsigned int vertexShaderObject;//声明一个顶点着色器对象
vertexShaderObject = glCreateShader(GL_VERTEX_SHADER);//创建顶点着色器,并将ID存储在vertexShaderObject中
glShaderSource(vertexShaderObject, 1, &vertexShaderSource, NULL);//将顶点色器源代码加载到vertexShaderObject中
glCompileShader(vertexShaderObject);//编译着色器对象
//检查编译是否成功
int success;
char infoLog[512]; // 用于存储错误信息
glGetShaderiv(vertexShaderObject, GL_COMPILE_STATUS, &success);//获取编译态
if (!success)
{
glGetShaderInfoLog(vertexShaderObject, 512, NULL, infoLog);//获取错误息
std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog<< std::endl;
}

片元着色器

为了方便,我们首先写一个输出固定颜色的片元着色器.

#version 330 core
out vec4 FragColor;

void main()
{
FragColor = vec4(1.0f, 0.5f, 0.7f, 1.0f);
}

同理将其保存为一个字符串,并且编译.

const char *fragmentShaderSource = 
"#version 330 core\n"
"out vec4 FragColor;\n"
"void main()\n"
"{\n"
" FragColor = vec4(1.0f, 0.5f, 0.7f, 1.0f);\n"
"}\0";
unsigned int fragmentShaderObject;//声明一个片段着色器对象
fragmentShaderObject = glCreateShader(GL_FRAGMENT_SHADER);//创建片段着色器,并将其ID存储在fragmentShaderObject中
glShaderSource(fragmentShaderObject, 1, &fragmentShaderSource, NULL);//将片段着色器源代码加载到fragmentShaderObject中
glCompileShader(fragmentShaderObject);//编译着色器对象

唯一不同指出在于使用GL_FRAGMENT_SHADER作为着色器类型.

着色器程序

现在我们已经拥有了两个已经编译完成的Shader Object(着色器对象),接下来我们需要将他们链接到一个Shader Program(着色器程序)中.

Shader Program是多个shader的集合,链接这一步骤就是为了确保上游数据能够正确被下游接受.当输入输出不匹配时,链接就会失败.

输入以下代码

//链接着色器程序
unsigned int shaderProgramObject;//声明一个着色器程序对象
shaderProgramObject = glCreateProgram();//创建着色器程序,并将其ID存储shaderProgramObject中
glAttachShader(shaderProgramObject, vertexShaderObject);//将顶点着色器对象附到着色器程序对象中
glAttachShader(shaderProgramObject, fragmentShaderObject);//将片段着色器对附加到着色器程序对象中
glLinkProgram(shaderProgramObject);//链接着色器程序对象
glGetProgramiv(shaderProgramObject, GL_LINK_STATUS, &success);//获取链接状态
if (!success)
{
glGetProgramInfoLog(shaderProgramObject, 512, NULL, infoLog);//获取错信息
std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog <<std::endl;
}
glUseProgram(shaderProgramObject);//激活刚才创建的着色器程序对象
glDeleteShader(vertexShaderObject);//删除顶点着色器对象,因为我们不再需要它了
glDeleteShader(fragmentShaderObject);//删除片段着色器对象,因为我们不再需要它了

链接顶点属性

现在,我们完成了数据输入,以及数据在渲染管线中的处理方式,唯一的问题在于,OpenGL并不知道应该如何解释我们输入的数据,以及该如何把这些数据连接到顶点着色器的属性上.我们需要告诉OpenGL怎么做

首先我们需要知道OpenGl对顶点数据的解释标准:
alt text

然后我们需要告诉OpenGL如何解析顶点数据

glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);

这里的参数非常多,具体含义如下:

  • GLuint index:指定要配置的顶点属性的索引.回顾上文的VertexShader中,使用了layout (location = 0)定义了position顶点属性.因为我们希望将position数据传递给上文的顶点着色器,所以这里我们指定0.
  • GLint size:指定顶点属性大小.由于顶点属性是一个vec3,所以这里我们指定3.
  • GLenum type:指定顶点属性的数据类型.由于我们的数据是float,所以这里我们指定GL_FLOAT.
  • GLboolean normalized:指定顶点属性是否需要被映射到0-1之间.
  • GLsizei stride(步长):指定连续两个顶点属性之间的字节数.由于我们的数据是float,所以这里我们指定3*sizeof(float),即3个float的大小.
  • const GLvoid* pointer:指定顶点数据在缓冲中的起始位置.由于我们的数据是从0开始的,所以这里我们指定(void*)0,即从缓冲的开头开始.

每个顶点属性从一个VBO管理的内存中获得它的数据,而具体是从哪个VBO(程序中可以有多个VBO)获取则是通过在调用glVertexAttribPointer时绑定到GL_ARRAY_BUFFER的VBO决定的。由于在调用glVertexAttribPointer之前绑定的是先前定义的VBO对象,顶点属性0现在会链接到它的顶点数据。

VAO

当顶点和物体数量变得很多时,绑定正确的缓冲对象,为每个物体配置所有顶点属性就变成一件麻烦事,所以
VAO(Vertex Array Object)顶点数组对象应运而生.

VAO是一个存储了 $顶点属性配置和应该使用的VBO$ 的对象

一个VAO会存储以下数据:

  • glEnableVertexAttribArray 和 glDisableVertexAttribArray: 启用和禁用顶点属性的调用
    即控制顶点属性是否被使用以及顶点属性位置
  • 通过glVertexAttribPointer设置的顶点属性指针
  • 通过glVertexAttribPointer调用与顶点属性关联的VBO

使用以下代码创建VAO

unsigned int VAO;
glGenVertexArrays(1, &VAO);

EBO

在模型中,常常会有一个顶点被多个三角形共享,如果我们单独存储每个三角形的顶点,就会造成大量重复元素造成浪费,所以我们使用EBO来存储每个三角形的顶点索引.

使用以下代码创建EBO

unsigned int EBO;
glGenBuffers(1, &EBO);

在绘制时不再使用glDrawArrays,而是使用glDrawElements

VBO & VAO & EBO

  • VBO: 存储大量顶点,因而可以利用VBO一次性发送大量数据到显卡
  • VAO: 配置并告诉了OpenGL如何使用VBO,以及使用哪个VBO
  • EBO: 用于指定三角形顶点的连接方式

源码

/*
* @Author: Vanish
* @Date: 2024-09-09 21:35:01
* @LastEditTime: 2024-09-10 16:20:17
* Also View: http://vanishing.cc
* Copyright@ https://creativecommons.org/licenses/by/4.0/deed.zh-hans
*/
#include <iostream>
#include "glad/glad.h" //glad 必须在Glfw前加载
#include "Glfw/glfw3.h"

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.7f, 1.0f);\n"
"}\0";

//窗口大小变化回调函数
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
glViewport(0, 0, width, height);
}

//输入处理函数
void ProcessInput(GLFWwindow* window)
{
//按下ESC键退出程序
if(glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
glfwSetWindowShouldClose(window, true);
}

int main()
{
//初始化glfw
glfwInit();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); // 设置opengl主版本为3
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); // 设置opengl次版本为3
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); //告诉GLFW 我们使用的是核心模式
//glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);

//创建窗口
GLFWwindow* window = glfwCreateWindow(800, 600, "LearnOpenGL-BickRenderer", NULL, NULL);//创建窗口,800*600为窗口大小,LearnOpenGL为窗口标题
if (window == NULL)
{
std::cout << "Failed to create GLFW window" << std::endl;
glfwTerminate();//终止glfw
return -1;
}
glfwMakeContextCurrent(window);//通知GLFW将此窗口的上下文设置为当前线程的主上下文

//初始化GLAD
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))//TODO: 这里暂时不是很清楚
{
std::cout << "Failed to initialize GLAD" << std::endl;
return -1;
}

//设置Viewport
glViewport(0, 0, 800, 600);//左下角坐标为(0,0),右上角坐标为(800,600)
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);//设置窗口大小变化回调函数

//编译着色器
unsigned int vertexShaderObject;//声明一个顶点着色器对象
vertexShaderObject = glCreateShader(GL_VERTEX_SHADER);//创建顶点着色器,并将其ID存储在vertexShaderObject中
glShaderSource(vertexShaderObject, 1, &vertexShaderSource, NULL);//将顶点着色器源代码加载到vertexShaderObject中
glCompileShader(vertexShaderObject);//编译着色器对象
//检查编译是否成功
int success;
char infoLog[512]; // 用于存储错误信息
glGetShaderiv(vertexShaderObject, GL_COMPILE_STATUS, &success);//获取编译状态
if (!success)
{
glGetShaderInfoLog(vertexShaderObject, 512, NULL, infoLog);//获取错误信息
std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;
}
unsigned int fragmentShaderObject;//声明一个片段着色器对象
fragmentShaderObject = glCreateShader(GL_FRAGMENT_SHADER);//创建片段着色器,并将其ID存储在fragmentShaderObject中
glShaderSource(fragmentShaderObject, 1, &fragmentShaderSource, NULL);//将片段着色器源代码加载到fragmentShaderObject中
glCompileShader(fragmentShaderObject);//编译着色器对象
glGetShaderiv(fragmentShaderObject, GL_COMPILE_STATUS, &success);//获取编译状态
if (!success)
{
glGetShaderInfoLog(fragmentShaderObject, 512, NULL, infoLog);//获取错误信息
std::cout << "ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n" << infoLog << std::endl;
}
//链接着色器程序
unsigned int shaderProgramObject;//声明一个着色器程序对象
shaderProgramObject = glCreateProgram();//创建着色器程序,并将其ID存储在shaderProgramObject中
glAttachShader(shaderProgramObject, vertexShaderObject);//将顶点着色器对象附加到着色器程序对象中
glAttachShader(shaderProgramObject, fragmentShaderObject);//将片段着色器对象附加到着色器程序对象中
glLinkProgram(shaderProgramObject);//链接着色器程序对象
glGetProgramiv(shaderProgramObject, GL_LINK_STATUS, &success);//获取链接状态
if (!success)
{
glGetProgramInfoLog(shaderProgramObject, 512, NULL, infoLog);//获取错误信息
std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog << std::endl;
}
glUseProgram(shaderProgramObject);//激活刚才创建的着色器程序对象
glDeleteShader(vertexShaderObject);//删除顶点着色器对象,因为我们不再需要它了
glDeleteShader(fragmentShaderObject);//删除片段着色器对象,因为我们不再需要它了

//输入顶点数据
float vertices[] = {
0.5f, 0.5f, 0.0f, // top right
0.5f, -0.5f, 0.0f, // bottom right
-0.5f, -0.5f, 0.0f, // bottom left
-0.5f, 0.5f, 0.0f // top left
};//顶点坐标
unsigned int indices[] = {
// 注意索引从0开始!
// 此例的索引(0,1,2,3)就是顶点数组vertices的下标,
// 这样可以由下标代表顶点组合成矩形
0, 1, 3, // 第一个三角形
1, 2, 3 // 第二个三角形
};//索引
unsigned int VBO,VAO,EBO;//声明一个VBO,VAO,EBO
glGenVertexArrays(1, &VAO);//生成一个VAO,并将其ID存储在VAO中,此时VAO拥有了其唯一的id
glGenBuffers(1, &VBO);//生成一个VBO,并将其ID存储在VBO中,此时VBO拥有了其唯一的id
glGenBuffers(1, &EBO);//生成一个EBO,并将其ID存储在EBO中,此时EBO拥有了其唯一的id

glBindVertexArray(VAO);//绑定VAO到当前活动的缓冲区,此时VAO将成为活动缓冲区

glBindBuffer(GL_ARRAY_BUFFER, VBO);//绑定VBO到GL_ARRAY_BUFFER目标,GL_ARRAY_BUFFER是VBO(顶点缓冲对象)的缓冲类型
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);//将顶点数据复制到缓冲中,并指定数据用以静态访问

glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);//绑定EBO到GL_ELEMENT_ARRAY_BUFFER目标,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);//启用顶点属性数组

//渲染循环
while(!glfwWindowShouldClose(window))//窗口应该关闭时结束循环
{
//输入
ProcessInput(window);//处理输入

//渲染
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);//设置清空屏幕使用的颜色,是一个状态设置函数
glClear(GL_COLOR_BUFFER_BIT);//清空颜色缓冲,是一个状态使用函数,获取状态后执行

//绘制
glUseProgram(shaderProgramObject);//激活着色器程序对象
glBindVertexArray(VAO);//绑定VAO到当前活动的缓冲区
glDrawElements(GL_TRIANGLES,6,GL_UNSIGNED_INT,0);//绘制三角形,第一个参数为绘制模式,第二个参数为起始索引,第三个参数为绘制数量

//交换缓冲,检查并调用事件回调函数
glfwSwapBuffers(window); //交换颜色缓冲
glfwPollEvents(); //检查是否触发事件(输入),更新窗口状态,调用对应回调函数
}

glfwTerminate(); //终止glfw
}