本帖最后由 chelsea 于 2024-5-9 22:12 编辑
本帖最后由 chelsea 于 2024-5-9 22:12 编辑
本帖最后由 chelsea 于 2024-5-9 22:09 编辑
本帖最后由 chelsea 于 2024-5-9 22:05 编辑
本帖最后由 chelsea 于 2024-5-9 21:48 编辑
Setup OpenGL in Visual Studio 2022(安装GLFW的,很好用,我没遇到问题)
Setup GLEW and GLFW in Visual Studio(根据LearnOpenGL文档,我添加的是glew32s.lib)
本文是LearnOpenGL文档的前三节内容《OpenGL》、《Creating a window》和《Hello Window》的学习笔记。
什么是OpenGL
OpenGL被认为是一个API(Application Programming Interface, 应用程序接口),包含了一系列可以操作图形、图像的函数。本质上,OpenGL自身是一个巨大的状态机(State Machine):一系列的变量描述OpenGL此刻应当如何运行。OpenGL的状态通常被称为OpenGL上下文(Context)。
对象
对象(object)是指一些选项的集合,它代表OpenGL状态的一个子集。可以把对象看做一个C风格的结构体(Struct):
struct object_name {
GLfloat option1;
GLuint option2;
GLchar[] name;
};
当我们使用一个对象时,通常看起来像下面一样(把OpenGL上下文看作一个大的结构体):
// OpenGL的状态
struct OpenGL_Context
{
...
object* object_Window_Target;
...
};
// 创建对象
GLuint objectId = 0;
glGenObject(1, &objectId);
// 绑定对象至上下文
glBindObject(GL_WINDOW_TARGET, objectId);
// 设置当前绑定到 GL_WINDOW_TARGET 的对象的一些选项
glSetObjectOption(GL_WINDOW_TARGET, GL_OPTION_WINDOW_WIDTH, 800);
glSetObjectOption(GL_WINDOW_TARGET, GL_OPTION_WINDOW_HEIGHT, 600);
// 将上下文对象设回默认
glBindObject(GL_WINDOW_TARGET, 0);
这段代码是使用OpenGL时常见的workflow:首先创建一个对象,然后用一个id保存它的引用(实际数据被储存在后台)。然后我们将对象绑定至上下文的目标位置(例子中,窗口对象目标的位置被定义成GL_WINDOW_TARGET)。接下来设置窗口的选项。最后将目标位置的对象id设回0,解绑这个对象。设置的选项将被保存在objectId所引用的对象中,一旦我们重新绑定这个对象到GL_WINDOW_TARGET位置,这些选项就会重新生效。
注:绑定一个对象,对它进行一系列操作,然后解绑。这可以防止后续的渲染操作错误的修改了对象。
创建窗口
GLFW
创建main函数,在这个函数中我们将会实例化GLFW窗口:
int main()
{
glfwInit();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); //主版本号
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); //次版本号,OpenGL 3.3
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); //使用的是核心模式(Core-profile)
glfwWindowHint(GLFW_RESIZABLE, GL_FALSE); //不允许调整显示窗口大小
return 0;
}
首先,我们在main函数中调用glfwInit函数来初始化GLFW,然后使用glfwWindowHint函数来配置GLFW。glfwWindowHint函数的第一个参数代表选项的名称,我们可以从很多以GLFW_开头的枚举值中选择;第二个参数接受一个整形,用来设置这个选项的值。
接下来,创建一个窗口对象,存放所有和窗口相关的数据,而且会被GLFW的其他函数频繁地用到。
GLFWwindow* window = glfwCreateWindow(800, 600, "LearnOpenGL", nullptr, nullptr);
if (window == nullptr)
{
std::cout << "Failed to create GLFW window" << std::endl;
glfwTerminate();
return -1;
}
glfwMakeContextCurrent(window);
返回值是GLFWwindow对象的指针,会在其他的GLFW操作中使用到。创建完窗口我们就可以通知GLFW将我们窗口的上下文设置为当前线程的主上下文了。
GLEW
GLEW是用来管理OpenGL的函数指针的,所以在调用任何OpenGL的函数之前,我们需要初始化GLEW。
glewExperimental = GL_TRUE;
if (glewInit() != GLEW_OK)
{
std::cout << "Failed to initialize GLEW" << std::endl;
return -1;
}
Viewport
此外,渲染前还需要设置渲染窗口的大小,可以通过调用glViewport函数来设置窗口的维度(Dimension):
int width, height;
glfwGetFramebufferSize(window, &width, &height);
glViewport(0, 0, width, height);
glViewport函数前两个参数控制窗口左下角的位置。第三、四个参数控制渲染窗口的宽度和高度(像素),这里我们是直接从GLFW中获取的。实际上,也可以将视口的维度设置为比GLFW的维度小,这样之后所有的OpenGL渲染将会在一个更小的窗口中显示,可以将一些其它元素显示在OpenGL视口(viewport)之外。
Game loop
我们不希望只绘制一个图像之后,应用程序就立即退出并关闭窗口。我们希望程序在我们明确地关闭它之前不断绘制图像并能够接受用户输入。因此,我们需要在程序中添加一个while循环,我们可以把它称之为游戏循环(Game Loop),它能在我们让GLFW退出前一直保持运行。下面几行的代码就实现了一个简单的游戏循环:
while(!glfwWindowShouldClose(window))
{
glfwPollEvents();
glfwSwapBuffers(window);
}
glfwWindowShouldClose:在循环开始前检查一次GLFW是否被要求退出。
glfwPollEvents:检查有没有触发什么事件(比如键盘输入、鼠标移动等),然后调用对应的回调函数(可以通过回调方法手动设置)。我们一般在游戏循环的开始调用事件处理函数。
glfwSwapBuffers:交换颜色缓冲(它是一个储存着GLFW窗口每一个像素颜色的大缓冲),它被用来绘制,并且将会作为输出显示在屏幕上。
双缓冲(Double Buffer)
应用程序使用单缓冲绘图时可能会存在图像闪烁的问题。 这是因为生成的图像不是一下子被绘制出来的,而是按照从左到右,由上而下逐像素地绘制而成的。最终图像不是在瞬间显示给用户,而是通过一步一步生成的,这会导致渲染的结果很不真实。为了规避这些问题,我们应用双缓冲渲染窗口应用程序。前缓冲保存着最终输出的图像,它会在屏幕上显示;而所有的的渲染指令都会在后缓冲上绘制。当所有的渲染指令执行完毕后,我们交换(Swap)前缓冲和后缓冲,这样图像就立即呈显出来,之前提到的不真实感就消除了。
最后
当游戏循环结束后,需要正确释放/删除之前的分配的所有资源。可以在main函数的最后调用glfwTerminate函数来释放GLFW分配的内存。
glfwTerminate();
return 0;
完整的代码(运行成功!)
键盘控制——输入
键盘控制可以通过使用GLFW的回调函数(Callback Function)来完成。回调函数事实上是一个函数指针,当我们设置好后,GLWF会在合适的时候调用它。按键回调(KeyCallback)是众多回调函数中的一种。当我们设置了按键回调之后,GLFW会在用户有键盘交互时调用它。该回调函数的原型如下所示:
按键回调函数接受一个GLFWwindow指针作为它的第一个参数;第二个整形参数用来表示按下的按键;action参数表示这个按键是被按下还是释放;最后一个整形参数表示是否有Ctrl、Shift、Alt、Super等按钮的操作。GLFW会在合适的时候调用它,并为各个参数传入适当的值。
void key_callback(GLFWwindow* window, int key, int scancode, int action, int mode)
{
// 当用户按下ESC键,我们设置window窗口的WindowShouldClose属性为true
// 关闭应用程序,即GLFW
if(key == GLFW_KEY_ESCAPE && action == GLFW_PRESS)
glfwSetWindowShouldClose(window, GL_TRUE);
}
然后,通过GLFW注册我们的函数至合适的回调:
glfwSetKeyCallback(window, key_callback);
我们可以在创建窗口之后,开始游戏循环之前注册各种回调函数。
渲染
所有的渲染(Rendering)操作都应该在game loop中,因为我们想让这些渲染指令在每次游戏循环迭代的时候都能被执行:
// 程序循环
while(!glfwWindowShouldClose(window))
{
// 检查事件
glfwPollEvents();
// 渲染指令
...
// 交换缓冲
glfwSwapBuffers(window);
}
例如,我们可以通过调用glClear函数来清空屏幕的颜色缓冲,它接受一个缓冲位(Buffer Bit)来指定要清空的缓冲,可能的缓冲位有GL_COLOR_BUFFER_BIT,GL_DEPTH_BUFFER_BIT和GL_STENCIL_BUFFER_BIT。
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
我们使用glClearColor来设置清空屏幕所用的颜色。当调用glClear清除颜色缓冲之后,整个颜色缓冲都会被填充为glClearColor里所设置的颜色。