本文章内容代码可在这里找到,如果此代码对您有帮助,烦请动动您的手指,点个
Star
,谢谢!欢迎访问我的个人主页Orient。
创建窗口
1、首先我们引入必要的头文件:
#include "glad.h"
#include <GLFW/glfw3.h>
请确保GLAD头文件的引入在GLFW之前,GLAD的头文件包含了正确的OpenGL头文件(例如GL/gl.h
),所以需要在其他依赖于OpenGL的头文件之前引入GLAD
2、实例化GLFW窗口
int main()
{
glfwInit();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 1);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
// Mac必须添加此行,Windows忽略
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
return 0;
}
前两行代码指定了OpenGL的主版本和次版本号(4.1),第三行代表着使用核心模式(Core-profile),意味着我们只能使用OpenGL功能的一个子集(没有我们不再需要的向后兼容特性)。
3、接下来创建一个窗口对象,它存放了所有和窗口相关的数据,而且会被GLFW的其他函数频繁调用
GLFWwindow* window = glfwCreateWindow(800, 600, "LearnOpenGL", NULL, NULL);
if(window == NULL)
{
std::cout << "Failed to create GLFW window" << std::endl;
glfwTerminate();
return -1;
}
glfwMakeContexCurrent(window);
glfwCreateWindow函数,前两个参数是窗口的宽高,第三个参数是这个窗口的命名,后两个暂时忽略,返回了一个GLFWwindow对象。glfwMakeContexCurrent函数告诉GLFW将窗口的上下文设置为当前线程的主上下文。
4、GLAD是用来管理OpenGL的函数指针的,所以调用任何OpenGL函数之前需要初始化GLAD
if(!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
{
std::cout << "Failed to initialize GLAD" << std::endl;
return -1;
}
我们给GLAD传入了用来加载系统相关的OpenGL函数指针地址的函数。GLFW给我们的是glfwGetProcAdress
,它根据我们编译的系统定义了正确的函数。
5、视口
在开始渲染之前必须告诉OpenGL渲染窗口(Viewport)的尺寸大小,这样OpenGL才能知道怎样根据窗口大小显示数据和坐标。
// 此函数设置窗口的维度(Dimension)
glViewport(0, 0, 800, 600);
前两个参数控制窗口左下角位置,后两个控制渲染窗口的宽高(像素)。也可将视口维度设置比GLFW窗口维度小,这样子之后所有的OpenGL渲染将会在一个更小的窗口中显示,这样子的话我们也可以将一些其它元素显示在OpenGL视口之外。
6、对窗口注册回调函数(CallbackFunction)
函数注册后会在每次窗口大小改变的时候调用,视口也会随之调整
函数原型如下:
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
glViewport(0, 0, width, height);
}
进行注册,告诉GLFW每当窗口调整时调用此函数:
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
7、为了使图像能够持续显示而不是一闪即逝,我们需要写一个渲染循环,使得GLFW在退出之前一直保持运行
while(!glfwWindowShouldClose(window))
{
glfwSwapBuffers(window);
glfwPollEvents();
}
glfwWindowShouldClose
函数在每次循环开始前检查一次GLFW是否被要求退出,是的话返回true
,循环结束
glfwPollEvents
函数检查是否有触发事件,比如键盘、鼠标等信号输入,然后更新窗口状态,调用相应的回调函数(可通过回调方法手动设置)。
glfwSwapBuffers
函数会交换颜色缓冲
8、渲染结束后释放所有资源
glfwTerminate();
return 0;
至此,窗口创建完成
接下来我们进行一些完善工作
9、接下来我们添加一个触发时间,当用户按下Esc
键时关闭窗口。
void processInput(GLFWwindow *window)
{
if(glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
glfwSetWindowShouldClose(window, true);
}
glfwGetKey
函数需要一个窗口以及一个按键作为输入。这个函数将会返回这个案件是否正在被按下,我们将其定义在processInput
函数当中
接下来在渲染循环的每一个迭代中调用processInput
:
while (!glfwWindowShouldClose(window))
{
processInput(window);
// 这里是渲染指令
...
glfwSwapBuffers(window);
glfwPollEvents();
}
10、我们使用一个自定义的颜色清空屏幕,使得在每个新的渲染迭代开始后清除上一次渲染结果,并显示我们自定义的颜色
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
渲染一个三角形
开始绘制之前,我们需要给OpenGL输入一些顶点数据(范围在[-1, 1],需要自行进行坐标变换)。我们需要渲染一个三角形,因此我们需要三个顶点位置,将它定义为一个float
数组:
// 由于我们绘制的是一个2D三角形,因此,将其顶点的z坐标都设置为0
float vertices[] = {
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
0.0f, 0.5f, 0.0f
};
接下来使用glGenBuffers
函数和一个缓冲ID生成一个VBO对象,并使用glBindBuffer
函数把新创建的缓冲绑定到GL_ARRAY_BUFFER
目标上:
unsigned int VBO;
glGenBuffers(1, &VBO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
从这一刻起,我们使用的任何(在GL_ARRAY_BUFFER
目标上的)缓冲调用都会用来配置当前绑定的缓冲(VBO)。然后我们可以调用glBufferData函数,它会把之前定义的顶点数据复制到缓冲的内存中:
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
第一个参数是目标缓冲的类型,顶点缓冲对象当前绑定到GL_ARRAY_BUFFER
目标上。
第二个参数指定传输数据大小。
第三个参数是我们实际发送的数据。
第四个参数指定了显卡管理数据的方式,有一下三种形式:
GL_STATIC_DRAW
:数据不会或几乎不改变。
GL_DYNAMIC_DRAW
:数据会改变很多。
GL_STREAM_DRAW
:数据每次绘制都会改变。
顶点着色器
首先用GLSL
(OoenGL Shading Language)编写顶点着色器,然后编译这个着色器。下面给出一个非常基础的顶点着色器源代码:
#version 410 core
layout (location = 0) in vec3 aPos;
void main()
{
gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);
}
首先申明OpenGL版本4.1(对应410)。
接下来使用in
关键字,在顶点着色器中声明所有的输入顶点属性(Input Vertex Attribute)。现在我们只关心位置(Position)数据,所以我们只需要一个顶点属性。GLSL有一个向量数据类型,它包含1到4个float
分量,包含的数量可以从它的后缀数字看出来。由于每个顶点都有一个3D坐标,我们就创建一个vec3
输入变量aPos
。我们同样也通过layout (location = 0)
设定了输入变量的位置值(Location)你后面会看到为什么我们会需要这个位置值。
编译着色器
先创建一个着色器对象,注意还是用ID来引用。所以我们储存这个顶点着色器为unsigned int
,然后用glCreateShader
创建这个着色器:
unsigned int vertexShader;
vertexShader = glCreateShader(GL_VERTEX_SHADER);
我们把需要创建的着色器类型以参数形式提供给glCreateShader
。由于我们正在创建一个顶点着色器,传递的参数是GL_VERTEX_SHADER
。
接下来把着色器源码附加到着色器对象上,并编译它:
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
glCompileShader(vertexShader);
glShaderSource
函数把要编译的着色器对象作为第一个参数。第二参数指定了传递的源码字符串数量,这里只有一个。第三个参数是顶点着色器真正的源码,第四个参数我们先设置为NULL
。
片段着色器
先给出片段着色器源码:
#version 410 core
out vec4 FragColor;
void main()
{
FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);
}
片段着色器只需要一个输出变量,这个变量是一个4分量向量,它表示的是最终的输出颜色,我们应该自己将其计算出来。我们可以用out
关键字声明输出变量,这里我们命名为FragColor
。下面,我们将一个alpha值为1.0(1.0代表完全不透明)的橘黄色的vec4
赋值给颜色输出。
编译片段着色器的过程与顶点着色器类似,只不过我们使用GL_FRAGMENT_SHADER
常量作为着色器类型:
unsigned int fragmentShader;
fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
glCompileShader(fragmentShader);
着色器程序
着色器程序对象(Shader Program Object)是多个着色器合并之后并最终链接完成的版本。如果要使用刚才编译的着色器我们必须把它们链接(Link)为一个着色器程序对象,然后在渲染对象的时候激活这个着色器程序。已激活着色器程序的着色器将在我们发送渲染调用的时候被使用。
创建程序对象:
unsigned int shaderProgram;
shaderProgram = glCreateProgram();
glCreateProgram
函数创建一个程序,并返回新创建程序对象的ID引用。现在我们需要把之前编译的着色器附加到程序对象上,然后用glLinkProgram
链接它们:
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);
最后调用glUseProgram
函数,激活程序:
glUseProgram(shaderProgram);
着色器对象链接到程序对象以后,需要删除着色器对象:
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
链接顶点属性
顶点着色器允许我们指定任何以顶点属性为形式的输入。这使其具有很强的灵活性的同时,它还的确意味着我们必须手动指定输入数据的哪一个部分对应顶点着色器的哪一个顶点属性。所以,我们必须在渲染前指定OpenGL该如何解释顶点数据。
我们的顶点缓冲数据会被解析为下面这样子:
因此使用glVertexAttribPointer
函数告诉OpenGL该如何解析顶点数据:
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
第一个参数指定我们要配置的顶点属性
第二个参数指定顶点属性的大小。
第三个参数指定数据的类型
第四个参数定义我们是否希望数据被标准化(Normalize)。如果我们设置为GL_TRUE
,所有数据都会被映射到0(对于有符号型signed数据是-1)到1之间。
第五个参数是步长,它告诉我们在连续的顶点属性组之间的间隔。
最后一个参数表示位置数据在缓冲中起始位置的偏移量(Offset)。
接下来使用glEnableVertexAttribArray
函数,以顶点属性位置值作为参数,启用顶点属性。
代码最终大概长这样:
// 0. 复制顶点数组到缓冲中供OpenGL使用
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 1. 设置顶点属性指针
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// 2. 当我们渲染一个物体时要使用着色器程序
glUseProgram(shaderProgram);
// 3. 绘制物体
someOpenGLFunctionThatDrawsOurTriangle();
顶点数组对象
Vertex Array Object(VAO)可以像顶点缓冲对象那样被绑定,任何随后的顶点属性调用都会储存在这个VAO中。
OpenGL的核心模式要求我们使用VAO,所以它知道该如何处理我们的顶点输入。如果我们绑定VAO失败,OpenGL会拒绝绘制任何东西。
一个顶点数组对象会储存以下这些内容:
glEnableVertexAttribArray
和glDisableVertexAttribArray
的调用。
通过glVertexAttribPointer
设置的顶点属性配置。
通过glVertexAttribPointer
调用与顶点属性关联的顶点缓冲对象。
VAO的创建类似VBO:
unsigned int VAO;
glGenVertexArrays(1, &VAO);
要使用VAO,只需使用glBindVertexArray
绑定VAO。从绑定之后起,我们应该绑定和配置对应的VBO和属性指针,之后解绑VAO供之后使用。当我们打算绘制一个物体的时候,我们只要在绘制物体前简单地把VAO绑定到希望使用的设定上就行了。
代码大概是这样的:
// ..:: 初始化代码(只运行一次 (除非你的物体频繁改变)) :: ..
// 1. 绑定VAO
glBindVertexArray(VAO);
// 2. 把顶点数组复制到缓冲中供OpenGL使用
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 3. 设置顶点属性指针
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
[...]
// ..:: 绘制代(渲染循环中) :: ..
// 4. 绘制物体
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
someOpenGLFunctionThatDrawsOurTriangle();
绘制三角形
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 3);
glDrawArrays
函数第一个参数是打算绘制的图元的类型。第二个参数制订了顶点数组的起始索引,第三个参数指定我们打算绘制的顶点个数。
最终三角形是长这样的: