参考资料:
https://learnopengl.com/
https://learnopengl-cn.github.io/
上篇实现了OpenGL窗口的创建和打开,这次实现在窗口中绘制两个三角形。
首先,顶点数据。每个顶点由一个三维空间坐标表示,如果要绘制两个紧贴的三角形组成四边形,正常来说需要准备6个顶点(其中有两对顶点是重合的),GPU以三角形为单位绘制,一幅完整的图像中会包含大量的三角形,这会导致大量的资源浪费。更好的解决方案是只储存不同的顶点,并设定绘制这些顶点的顺序。这样子我们只要储存4个顶点就能绘制矩形了,之后只要指定绘制的顺序就行了。EBO的作用就是实现这个目的,后面会写到。因此要画由两个三角形组成的四边形,可以先声明四个顶点:
//顶点数据
float vertices[] = {
-0.5f, -0.5f, 0.0f, //0
0.5f, -0.5f, 0.0f, //1
0.0f, 0.5f, 0.0f, //2
//0.5f, -0.5f, 0.0f,
//0.0f, 0.5f, 0.0f,
0.8f, 0.8f, 0.0f //3
};
之后由顶点索引告诉OpenGL这两个三角形分别由哪几个顶点组成:
//顶点索引
unsigned int indices[] = {
0, 1, 2, //第一个三角形使用的顶点
2, 1, 3 //第二个三角形使用的顶点
};
如果要实现渲染功能,现代OpenGL需要我们至少设置一个顶点和一个片元着色器。这两个着色器需要使用GLSL语言编写,为了能够让OpenGL使用它,我们必须在运行时动态编译它的源代码,因此使用C风格的字符串硬编码形式来声明。以顶点着色器为例,#version 330 core表示使用GLSL3.3版本,与OpenGL版本匹配,以及使用核心模式。layout(location = 0)设定输入变量的位置值。in表示输入顶点属性,vec3表示输入顶点以三维向量表示,aPos为变量名。gl_Position是预定义好的顶点着色器输出变量,它是一个四维变量,因此前三个维度由aPos的x、y、z分量占据,最后一位给个固定值1.0。片元着色器中,out用于声明输出变量,我们要输出一个RGBA四维的颜色向量,在main函数中可以为输出颜色值赋值。
//顶点着色器编码
const char* vertexShaderSource =
"#version 330 core \n "
"layout(location = 0) in vec3 aPos; \n "
"void main() { \n "
" gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0); \n "
"} \n ";
//片元着色器编码
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 "
"} \n ";
接下来,需要对顶点着色器和片元着色器的硬编码进行编译。首先定义一个unsigned int类型变量,使用glCreateShader创建着色器,它的参数要求指定要创建着色器的类型,GL_VERTEX_SHADER则表示创建顶点Shader,GL_FRAGMENT_SHADER表示创建片元Shader。创建好对应的Shader后,用glShaderSource方法把着色器源码附加到着色器对象上,参数1是要编译的着色器对象,参数2是指定传递的源码字符串数量,参数3是顶点着色器真正的源码,参数4是指定字符串长度的数组,为nullptr表示每个字符串都以null结尾。最后用glCompileShader传入着色器对象进行编译。
//编译顶点着色器
unsigned int vertexShader; //定义
vertexShader = glCreateShader(GL_VERTEX_SHADER); //创建着色器,类型为顶点Shader,返回给定义的vertexShader
glShaderSource(vertexShader, 1, &vertexShaderSource, nullptr); //把着色器源码附加到着色器对象上,参数1是要编译的着色器对象,参数2是指定传递的源码字符串数量,参数3是顶点着色器真正的源码,参数4是指定字符串长度的数组,为nullptr表示每个字符串都以null结尾
glCompileShader(vertexShader); //编译Shader
//编译片元着色器
unsigned int fragmentShader; //定义
fragmentShader = glCreateShader(GL_FRAGMENT_SHADER); //创建着色器,类型为片元Shader,返回给定义的fragmentShader
glShaderSource(fragmentShader, 1, &fragmentShaderSource, nullptr); //把着色器源码附加到着色器对象上
glCompileShader(fragmentShader); //编译Shader
接下来需要介绍几个概念。顶点缓冲对象(Vertex Buffer Objects, VBO),它会在GPU内存中储存大量顶点,使我们可以一次性的发送一大批数据到显卡上,而不是每个顶点发送一次,因此它的存在很有必要。具体到代码中,先用unsigned int声明VBO对象,接着使用glGenBuffers生成缓冲区对象,第一个参数是要生成的缓冲对象的数量,第二个是要输入用来存储缓冲对象名称的数组,由于只需创建一个VBO,因此不需要用数组形式。接下来用glBindBuffer把新创建的缓冲绑定到GL_ARRAY_BUFFER目标上。OpenGL有很多缓冲对象类型,顶点缓冲对象的缓冲类型是GL_ARRAY_BUFFER。OpenGL允许我们同时绑定多个缓冲,只要它们是不同的缓冲类型。将VBO绑定到GL_ARRAY_BUFFER上后,最后一步是要把定义的数据复制到当前绑定的缓冲。使用glBufferData方法,参数1:目标缓冲类型,即GL_ARRAY_BUFFER,参数2:指定传输数据的大小(以字节为单位),即顶点数组vertices的大小,参数3:我们希望发送的实际数据,即顶点数组,参数4:指定显卡如何管理给定的数据,GL_STATIC_DRAW表示数据不会或几乎不会改变,可选的还有:GL_DYNAMIC_DRAW,表示数据会被改变很多;GL_STREAM_DRAW,表示数据每次绘制时都会改变。
//创建VBO(顶点缓冲对象)
unsigned int VBO;
glGenBuffers(1, &VBO); //生成缓冲区对象。第一个参数是要生成的缓冲对象的数量,第二个是要输入用来存储缓冲对象名称的数组,由于只需创建一个VBO,因此不需要用数组形式
glBindBuffer(GL_ARRAY_BUFFER, VBO); //把新创建的缓冲绑定到GL_ARRAY_BUFFER目标上,GL_ARRAY_BUFFER是一种顶点缓冲对象的缓冲类型。OpenGL允许同时绑定多个缓冲,只要它们是不同的缓冲类型。
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); //把定义的数据复制到当前绑定缓冲的函数。参数1:目标缓冲类型,参数2:指定传输数据的大小(以字节为单位),参数3:我们希望发送的实际数据,参数4:指定显卡如何管理给定的数据,GL_STATIC_DRAW表示数据不会或几乎不会改变。
顶点数组对象(Vertex Array Object, VAO),它封装了与顶点处理器有关的所有数据,它仅仅是记录顶点缓存区和索引缓冲区的引用,以及顶点的各种属性的布局而不是实际的数据。VAO可以像VBO那样被绑定,任何随后的顶点属性调用都会储存在这个VAO中。这样的好处就是,当配置顶点属性指针时,你只需要将那些调用执行一次,之后再绘制物体的时候只需要绑定相应的VAO就行了。这使在不同顶点数据和属性配置之间切换变得非常简单,只需要绑定不同的VAO就行了。刚刚设置的所有状态都将存储在VAO中。以OpenGL核心模式编码时,要求我们使用VAO。
与创建VBO相似,先声明,后创建,再绑定,只是所用到的函数有所不同,是顶点数组对象相关的创建函数glGenVertexArrays和绑定函数glBindVertexArray。
//创建VAO(顶点数组对象)
unsigned int VAO;
glGenVertexArrays(1, &VAO); //生成一个顶点数组对象
glBindVertexArray(VAO); //绑定VAO
元素缓冲对象(Element Buffer Object,EBO),也叫索引缓冲对象(Index Buffer Object,IBO)。通过使用EBO,可以实现开头说到的节省顶点数、以顶点索引方式绘制三角形的目的。它的创建方式与VBO基本一样,只不过要把缓冲的类型定义为GL_ELEMENT_ARRAY_BUFFER。
//创建EBO(元素缓冲对象/索引缓冲对象)
unsigned int EBO;
glGenBuffers(1, &EBO);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
接下来要创建着色器程序对象。声明后,使用glCreateProgram方法创建一个程序对象,返回一个ID引用,赋给声明的shaderProgram。之后用glAttachShader把之前编译的顶点、片元着色器分别附加到程序对象上,最后通过glLinkProgram链接为一个着色器程序对象。
//创建着色器程序对象
unsigned int shaderProgram;
shaderProgram = glCreateProgram(); //创建程序对象
glAttachShader(shaderProgram, vertexShader); //把之前编译的顶点着色器附加到程序对象上
glAttachShader(shaderProgram, fragmentShader); //把之前编译的片元着色器附加到程序对象上
glLinkProgram(shaderProgram); //链接为一个着色器程序对象
之后还需链接顶点属性。顶点着色器允许我们指定任何以顶点属性为形式的输入。这使其具有很强的灵活性的同时,它还的确意味着我们必须手动指定输入数据的哪一个部分对应顶点着色器的哪一个顶点属性。所以,我们必须在渲染前指定OpenGL该如何解释顶点数据。
顶点缓冲数据是如下形式:
它的特点是,位置数据被储存为32位(4字节)浮点值,每个位置包含3个这样的值,在这3个值之间没有空隙(或其他值)。这几个值在数组中紧密排列(Tightly Packed),数据中第一个值在缓冲开始的位置。不同顶点之间还可以塞其他数据,如UV,但是现在以单纯的画三角形为目的,只需要顶点坐标数据即可。对于告诉OpenGL如何解析顶点数据,需要使用以下两行代码。glVertexAttribPointer:告诉OpenGL该如何解析顶点数据(应用到逐个顶点属性上)。第一个参数是要配置的顶点属性,由于之前在硬编码顶点着色器时,声明了layout(location = 0),故填0;第二个参数是指定顶点属性的大小,由于顶点是vec3类型,由三个值组成,故为3;第三个参数是指定数据的类型,GLSL中vec*都是由浮点数值组成的,故为GL_FLOAT;第四个参数为是否希望数据被标准化,即Normalize,这里用GL_FALSE;第五个参数是在连续的顶点属性组之间的间隔,由于没有使用uv等信息,故间隔仅为3个float量;第六个参数表示位置数据在缓冲中起始位置的偏移量(Offset),由于位置数据在数组的开头,所以这里是0。最后用glEnableVertexAttribArray方法,以顶点属性位置值作为参数,启用顶点属性,由于前面声明了layout(location = 0),故为0,与glVertexAttribPointer方法中第一个参数一致。
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); //告诉OpenGL该如何解析顶点数据(应用到逐个顶点属性上)。参数1:要配置的顶点属性(layout(location = 0),故0),参数2:指定顶点属性的大小(vec3,故3),参数3:指定数据的类型,参数4:是否希望数据被标准化,参数5:在连续的顶点属性组之间的间隔,参数6:表示位置数据在缓冲中起始位置的偏移量(Offset)。
glEnableVertexAttribArray(0); //以顶点属性位置值作为参数,启用顶点属性,由于前面声明了layout(location = 0),故为0
最后,在循环渲染绘制的代码中,每次进行如下步骤:glUseProgram(shaderProgram),每个着色器调用和渲染调用都会使用这个程序对象;glBindVertexArray(VAO),当打算绘制多个物体时,首先要生成/配置所有的VAO(和必须的VBO及属性指针),然后储存它们供后面使用,当要绘制物体的时候就拿出相应的VAO,绑定它,绘制完物体后,再解绑VAO;glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO),绑定EBO;glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0),绘制,第一个参数为绘制模型,这里选择三角形,第二个参数为要绘制的顶点数,绘制两个三角形即为6个顶点,第三个参数为索引的类型GL_UNSIGNED_INT,第四个参数指定EBO中的偏移量,设为0.
glUseProgram(shaderProgram); //调用之后,每个着色器调用和渲染调用都会使用这个程序对象(也就是之前写的着色器)
glBindVertexArray(VAO); //绘制物体的时候就拿出相应的VAO,绑定它
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); //绑定EBO
//glDrawArrays(GL_TRIANGLES, 0, 6);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0); //绘制,参数1:绘制模式(三角形),参数2:绘制顶点数(两个三角形6个顶点),参数3:索引的类型,参数4:指定EBO中的偏移量。
这样就能够在OpenGL窗口中绘制两个三角形了,整体代码如下:
#define GLEW_STATIC
#include <GL/glew.h>
#include <GLFW/glfw3.h>
#include <iostream>
//顶点数据
float vertices[] = {
-0.5f, -0.5f, 0.0f, //0
0.5f, -0.5f, 0.0f, //1
0.0f, 0.5f, 0.0f, //2
//0.5f, -0.5f, 0.0f,
//0.0f, 0.5f, 0.0f,
0.8f, 0.8f, 0.0f //3
};
//顶点索引
unsigned int indices[] = {
0, 1, 2, //第一个三角形使用的顶点
2, 1, 3 //第二个三角形使用的顶点
};
//顶点着色器编码
const char* vertexShaderSource =
"#version 330 core \n "
"layout(location = 0) in vec3 aPos; \n "
"void main() { \n "
" gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0); \n "
"} \n ";
//片元着色器编码
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 "
"} \n ";
//检查输入函数
void processInput(GLFWwindow* window)
{
//按下ESC键时,将WindowShouldClose设为true,循环绘制将停止
if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
{
glfwSetWindowShouldClose(window, true);
}
}
//视口改变时的回调函数
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
glViewport(0, 0, width, height); //OpenGL渲染窗口的尺寸大小
}
int main()
{
glfwInit(); //初始化GLFW
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); //告诉GLFW要使用OpenGL的版本号
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); //主版本号、次版本号都为3,即3.3版本
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); //告诉GLFW使用核心模式(Core-profile)
//打开 GLFW Window
GLFWwindow* window = glfwCreateWindow(1600, 1200, "My OpenGL Game", nullptr, nullptr);
if (window == nullptr) //若窗口创建失败,打印错误信息,终止GLFW并return -1
{
printf("Open window failed.");
glfwTerminate();
return -1;
}
glfwMakeContextCurrent(window); //创建OpenGL上下文
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback); //用户改变窗口大小的时候,视口调用回调函数
//初始化GLEW
glewExperimental = true;
if (glewInit() != GLEW_OK) //若GLEW初始化失败,打印错误信息并终止GLFW窗口
{
printf("Init GLEW failed.");
glfwTerminate();
return -1;
}
//创建VAO(顶点数组对象)
unsigned int VAO;
glGenVertexArrays(1, &VAO); //生成一个顶点数组对象
glBindVertexArray(VAO); //绑定VAO
//创建VBO(顶点缓冲对象)
unsigned int VBO;
glGenBuffers(1, &VBO); //生成缓冲区对象。第一个参数是要生成的缓冲对象的数量,第二个是要输入用来存储缓冲对象名称的数组,由于只需创建一个VBO,因此不需要用数组形式
glBindBuffer(GL_ARRAY_BUFFER, VBO); //把新创建的缓冲绑定到GL_ARRAY_BUFFER目标上,GL_ARRAY_BUFFER是一种顶点缓冲对象的缓冲类型。OpenGL允许同时绑定多个缓冲,只要它们是不同的缓冲类型。
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); //把定义的数据复制到当前绑定缓冲的函数。参数1:目标缓冲类型,参数2:指定传输数据的大小(以字节为单位),参数3:我们希望发送的实际数据,参数4:指定显卡如何管理给定的数据,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);
//编译顶点着色器
unsigned int vertexShader; //定义
vertexShader = glCreateShader(GL_VERTEX_SHADER); //创建着色器,类型为顶点Shader,返回给定义的vertexShader
glShaderSource(vertexShader, 1, &vertexShaderSource, nullptr); //把着色器源码附加到着色器对象上,参数1是要编译的着色器对象,参数2是指定传递的源码字符串数量,参数3是顶点着色器真正的源码,参数4是指定字符串长度的数组,为nullptr表示每个字符串都以null结尾
glCompileShader(vertexShader); //编译Shader
//编译片元着色器
unsigned int fragmentShader; //定义
fragmentShader = glCreateShader(GL_FRAGMENT_SHADER); //创建着色器,类型为片元Shader,返回给定义的fragmentShader
glShaderSource(fragmentShader, 1, &fragmentShaderSource, nullptr); //把着色器源码附加到着色器对象上
glCompileShader(fragmentShader); //编译Shader
//创建着色器程序对象
unsigned int shaderProgram;
shaderProgram = glCreateProgram(); //创建程序对象
glAttachShader(shaderProgram, vertexShader); //把之前编译的顶点着色器附加到程序对象上
glAttachShader(shaderProgram, fragmentShader); //把之前编译的片元着色器附加到程序对象上
glLinkProgram(shaderProgram); //链接为一个着色器程序对象
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); //告诉OpenGL该如何解析顶点数据(应用到逐个顶点属性上)。参数1:要配置的顶点属性(layout(location = 0),故0),参数2:指定顶点属性的大小(vec3,故3),参数3:指定数据的类型,参数4:是否希望数据被标准化,参数5:在连续的顶点属性组之间的间隔,参数6:表示位置数据在缓冲中起始位置的偏移量(Offset)。
glEnableVertexAttribArray(0); //以顶点属性位置值作为参数,启用顶点属性,由于前面声明了layout(location = 0),故为0
//让程序在手动关闭之前不断绘制图像
while (!glfwWindowShouldClose(window))
{
//检测输入
processInput(window);
//渲染指令
glClearColor(0.f, 0.5f, 0.5f, 1.0f); //设置清空屏幕所用的颜色
glClear(GL_COLOR_BUFFER_BIT); //清空屏幕的颜色缓冲区
glUseProgram(shaderProgram); //调用之后,每个着色器调用和渲染调用都会使用这个程序对象(也就是之前写的着色器)
glBindVertexArray(VAO); //绘制物体的时候就拿出相应的VAO,绑定它
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); //绑定EBO
//glDrawArrays(GL_TRIANGLES, 0, 6);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0); //绘制,参数1:绘制模式(三角形),参数2:绘制顶点数(两个三角形6个顶点),参数3:索引的类型,参数4:指定EBO中的偏移量。
//检查并调用事件,交换缓冲区
glfwSwapBuffers(window); //交换颜色缓冲区,前缓冲区保存最终输出的图像,后缓冲区负责绘制渲染指令,当渲染指令执行完毕后,交换前后缓冲区,使完整图像呈现出来,避免逐像素绘制图案时的割裂感
glfwPollEvents(); //检查触发事件,如键盘输入、鼠标移动等
}
glfwTerminate(); //关闭GLFW并退出
return 0;
}