本文仅关注GLFW创建窗口和OpenGL上下文,以及处理窗口事件。关于OpenGL渲染不涉及。
初始化
在创建窗口之前需要初始化GLFW,使用glfwInit函数。 初始化成功时,返回GLFW_TRUE。如果发生错误,则返回GLFW_FALSE。所以应先判断是否初始化成功。
if(!glfwInit())
{
// 这里使用exit是因为考虑可能会在非main函数退出程序
// 效果等同于在main函数里 return 1;
exit(EXIT_FAILURE);
}
创建窗口和上下文
GLFW创建窗口使用glfwCreateWindow函数
如
GLFWwindow* window = glfwCreateWindow(640,480,"title",NULL,NULL);
同样如果创建失败则返回NULL,所以需要判断是否创建成功。
并且还需要调用glfwTerminate,用于销毁剩余窗口和游标,释放资源等。在程序退出之前都应调用glfwTerminate函数。
注意:如果初始化失败就不需要调用glfwTerminate,因为它在返回失败之前由glfwInit调用。
所以上面初始化判断不需要手动调用glfwTerminate。
if(!window)
{
glfwTerminate();
exit(EXIT_FAILURE);
}
设置上下文为当前
在之前我们使用glfwCreateWindow已经创建了窗口和上下文。 但使用中还需设置创建的上下文为当前上下文,才能使用该上下文。
使用 glfwMakeContextCurrent函数使指定窗口的OpenGL或OpenGL ES上下文在调用线程上是当前的。
glfwMakeContextCurrent(window);
让窗口显示
如果运行上面的代码,会发现窗口并没有显示出来(或者说窗口显示了,但程序太快退出)。并且终端显示进程已退出,代码为0,也可以看出程序正常退出。
我们可以使用
system("pause");
或者其它等待输入的语句来暂停,那么就可以显示窗口了。
但显然更好的是让程序进入循环,直到手动关闭或其它错误导致的关闭。
while(true)
{
}
但 GLFW 给我们提供了更好的函数glfwWindowShouldClose,该函数传入要查询的窗口,返回窗口的关闭标志值。
所以代码改写为以下
while(!glfwWindowShouldClose(window))
{
// opengl渲染代码
}
让窗口支持事件
再次运行会发现窗口可以正常显示,但是窗口的右上角最小,最大,关闭均无法使用,而且窗口也不能拖动。只能通过关闭终端来关闭窗口。
使用glfwPollEvents函数,该函数只处理已经在事件队列的事件。显然,应该将该函数放入循环中。
while(!glfwWindowShouldClose(window))
{
glfwPollEvents();
}
销毁窗口和上下文
当退出循环应销魂窗口和上下文,使用glfwDestroyWindow函数销毁窗口和上下文。
并如前文所说,在程序退出之前都应调用glfwTerminate函数,销魂窗口、资源等。
这样来看似乎glfwDestroyWindow和glfwTerminate函数差不多,但是glfwDestroyWindow是一次是销毁某一个窗口,而glfwTerminate是销魂所有窗口。当你的程序有一个主窗口+多个子窗口,相控制子窗口的销魂可以使用glfwDestroyWindow。对于我们的项目来说,只有一个主窗口,其实只调用其中一个函数也行。
glfwDestroyWindow(window);
glfwTerminate();
exit(EXIT_FAILURE);
当前所有的代码如下,基本上开启了一个窗口。
#include <GLFW/glfw3.h> // glfw头文件包含按你项目的配置
#include <cstdlib> // 为了包含exit函数
int main()
{
if(!glfwInit())
exit(EXIT_FAILURE);
GLFWwindow* window = glfwCreateWindow(800,600,"Title",NULL,NULL);
if(!window)
{
glfwTerminate();
exit(EXIT_FAILURE);
}
glfwMakeContextCurrent(window);
while(!glfwWindowShouldClose(window))
{
glfwPollEvents();
}
glfwDestroyWindow(window);
glfwTerminate();
exit(EXIT_FAILURE);
}
交换缓冲区
现在已经设置上下文为当前上下文,再次运行上述代码。
显示窗口,窗口中间是白色的,最大化或拉大窗口发现中间的白色框四周出现黑色框,这是为什么?
需要知道的是,默认情况下,GLFW窗口使用双缓冲。这意味着每个窗口都有两个渲染缓冲区;一个前缓冲,一个后缓冲。前缓冲区是要显示的缓冲区(可以理解为屏幕的内容),后缓冲区是要渲染的缓冲区(可以理解为内存中的数据)。
使用双缓冲好处是可以先在后缓冲区完成绘制,绘制完成后,再统一将完成的图形和前缓冲交换,图形便立即显示在屏幕上,实现快速绘图。
其实如果不渲染任何东西,glfw默认给我们提供的是一块黑色画布。而我们的窗口中间部分显示白色是因为后缓冲区渲染的内容其实并没有显示到屏幕上。
使用glfwSwapBuffers函数来交换缓冲区。
glfwSwapBuffers(window);
显然这个函数应该在我们的opengl渲染代码之后调用,且应该在循环中,即程序不关闭就要不同的交换缓冲区进行屏幕绘制。
当然使用双缓冲也会带来其它的问题:如在屏幕刷新期间交换缓冲区,导致屏幕画面撕裂。
所以通常使用glfwSwapInterval函数将交换间隔设置为1,即从调用glfwSwapBuffers开始等待1次屏幕更新再交换缓冲区并返回。
glfwSwapInterval(1);
这个函数放在循环外即可。
窗口提示
其实在前面创建窗口和上下文之前还可以设置许多窗口提示(可以理解为给予将创建的窗口设置属性)。
要设置窗口提示可以使用:
- glfwWindowHint 设置int类型
- glfwWindowHintString 设置string类型
如指定创建的上下文必须兼容的API版本,GLFW_CONTEXT_VERSION_MAJOR 和 GLFW_CONTEXT_VERSION_MINOR。
// 最常用的是OpenGL3.3,高版本的OpenGL都是在3.3的基础上扩展的
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR,3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR,3);
注意:这里的API版本和glfw版本不一样,在GLFW中定义了宏GL_VERSION_MAJOR和GLFW_VERSION_MINOR以及GLFW_VERSION_REVISION是头文件(glfw3.h)的API版本。
我们可以通过GpuCapsViewer或openGL Extensions Viewer查看电脑支持的OpenGL版本。
因为窗口提示有较多的属性,请查阅GLFW窗口提示 - 掘金 (juejin.cn)
事件处理
GLFW需要轮询窗口系统的事件,以便向应用程序提供输入,并向窗口系统证明应用程序没有锁定。事件处理通常在缓冲区交换后的每一帧完成。即使没有窗口,也需要进行事件轮询,以便接收监视器和操纵杆连接事件。
有三个函数用于处理挂起事件。glfwPollEvents,只处理那些已经接收到的事件,然后立即返回。
glfwPollEvents ();
当像大多数游戏一样持续渲染时,这是最好的选择。
如果您只需要在接收新输入时更新窗口的内容,那么glfwWaitEvents是更好的选择。
glfwWaitEvents ();
它将线程置于睡眠状态,直到至少接收到一个事件,然后处理所有接收到的事件。这节省了大量的CPU周期,并且对于编辑工具等非常有用。
如果你想等待事件,但有UI元素或其他需要定期更新的任务,glfwWaitEventsTimeout允许你指定一个超时。
glfwWaitEventsTimeout (0.7);
它将线程置于睡眠状态,直到至少接收到一个事件,或者直到指定的秒数过去。然后处理所有接收到的事件。
如果主线程在glfwWaitEvents中休眠,您可以通过使用glfwPostEmptyEvent向事件队列发送一个空事件来从另一个线程唤醒它。
glfwPostEmptyEvent ();
不要假设回调只会在响应上述函数时被调用。虽然有必要以上述一种或多种方式处理事件,但需要GLFW注册其自身回调的窗口系统可以将事件传递给GLFW以响应许多窗口系统函数调用。GLFW将在返回之前将这些事件传递给应用程序回调。
例如,按键回调函数,它的签名如下:
void function_name(GLFWwindow* window, int key, int scancode, int action, int mods)
举例,我们有一个按下Escape键关闭window的函数,如下:
void key_callback(GLFWwindow* window, int key, int scancode, int action, int mode)
{
if(key == GLFW_KEY_ESCAPE && action == GLFW_PRESS
glfwSetWindowShouldClose(window, GL_TRUE);
}
另外,我们还需要注册该回调事件,其签名如下
GLFWkeyfun glfwSetKeyCallback(GLFWwindow* window, GLFWkeyfun callback);
注册key_callback函数
glfwSetKeyCallback(window,key_callback);
鼠标输入
鼠标输入有多种形式,包括鼠标移动、按钮按压和滚动偏移。还可以更改光标的外观,将其更改为自定义图像或来自系统主题的标准光标形状。
光标位置
如果您希望在光标移动到窗口上时得到通知,请设置光标位置回调。
glfwSetCursorPosCallback(window,cursor_position_callback);
回调函数接收光标位置,以屏幕坐标测量,但相对于窗口内容区域的左上角。在提供它的平台上,传递完整的亚像素游标位置。
static void cursor_position_callback(GLFWwindow* window,double xpos,double ypos)
{
}
光标位置也会按每个窗口保存,并且可以使用glfwGetCursorPos轮询。
double xpos, ypos;
glfwgetcursor(window,&xpos, &ypos);
光标模式
输入模式为特殊形式的鼠标运动输入提供了几种光标模式。默认情况下,光标模式为,这意味着使用常规箭头光标(或使用glfwSetCursor设置的另一个光标),并且光标的移动不受限制。GLFW_CURSORGLFW_CURSOR_NORMAL
如果您希望实现基于鼠标运动的相机控制或其他需要无限制鼠标移动的输入方案,请将光标模式设置为.GLFW_CURSOR_DISABLED
glfwSetInputMode (window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
这将隐藏光标并将其锁定到指定的窗口。然后,GLFW将处理光标重新居中和偏移计算的所有细节,并为应用程序提供虚拟光标位置。这个虚拟位置通常通过游标位置回调和轮询提供。
请注意 您不应该使用GLFW的其他特性来实现此功能的自己版本。它不受支持,也不会像.GLFW_CURSOR_DISABLED那样健壮地工作 如果您只希望光标在窗口上方时隐藏,但仍希望其正常工作,请将光标模式设置为.GLFW_CURSOR_HIDDEN
glfwSetInputMode (window, GLFW_CURSOR, GLFW_CURSOR_HIDDEN);
此模式对光标的移动没有限制。
要退出这两种特殊模式,请恢复光标模式。GLFW_CURSOR_NORMAL
glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_NORMAL);
原始鼠标动作
当光标被禁用时,如果可用,可以启用原始(未缩放和未加速)鼠标运动。
原始鼠标运动更接近鼠标在表面上的实际运动。它不受应用于桌面光标运动的缩放和加速的影响。这种处理适合于光标,而原始运动更适合于控制,例如3D相机。因此,仅在禁用光标时才提供原始鼠标运动。
调用glfwRawMouseMotionSupported来检查当前机器是否提供原始运动,并设置输入模式以启用它。默认为关闭状态。GLFW_RAW_MOUSE_MOTION
if(glfwRawMouseMotionSupported ())
glfwsetputmode (window, GLFW_RAW_MOUSE_MOTION, GLFW_TRUE);
如果支持,原始鼠标移动可以在每个窗口和任何时间启用或禁用,但只有在禁用光标时才会提供。
游标对象
GLFW支持创建自定义和系统主题光标图像,封装为GLFWcursor对象。它们是用glfwCreateCursor或glfwCreateStandardCursor创建的,用glfwDestroyCursor或glfwTerminate销毁(如果有的话)。
自定义游标创建
使用glfwCreateCursor创建自定义游标,它返回创建的游标对象的句柄。例如,这将创建一个16 × 16的白色方形光标,热点位于左上角:
unsigned char pixels[16*16*4];
memset(pixels,0xff,sizeof(pixels));
GLFWimage image;
image.width = 16;
image.height = 16;
image.pixels = pixels;
GLFWcursor* cursor = glfwCreateCursor(&image,0,0);
如果游标创建失败,将返回NULL,因此有必要检查返回值。
图像数据是32位,小端,非预乘RGBA,即每个通道8位,首先是红色通道。像素按顺序排列,从左上角开始。
标准游标创建
可以使用glfwCreateStandardCursor创建具有当前系统游标主题的标准形状的游标。
GLFWcursor* cursor = glfwCreateStandardCursor(GLFW_HRESIZE_CURSOR);
这些游标对象的行为与使用glfwCreateCursor创建的游标对象完全相同,只是系统游标主题提供了实际的图像。
光标破坏 当不再需要游标时,使用glfwDestroyCursor销毁它。
glfwDestroyCursor(cursor);
销毁游标总是成功的。如果任何窗口的游标是当前的,那么该窗口将恢复为默认游标。这不会影响光标模式。当调用glfwTerminate时,所有剩余的游标将被销毁。
光标设置
可以使用glfwSetCursor将光标设置为当前窗口。
glfwSetCursor(window,cursor);
设置后,只要系统光标位于窗口的内容区域上方,并且将光标模式设置为GLFW_CURSOR_NORMAL,就会使用光标图像
可以为任意数量的窗口设置单个游标。
要恢复到默认游标,请将该窗口的游标设置为NULL
glfwSetCursor(window,NULL);
当一个游标被销毁时,任何设置了它的窗口都将恢复为默认游标。这不会影响光标模式。
光标进入/离开事件
如果您希望在光标进入或离开窗口的内容区域时得到通知,请设置光标进入/离开回调。
glfwSetCursorEnterCallback(window,cursor_enter_callback);
回调函数接收游标的新分类。
void cursor_enter_callback(GLFWwindow* window, int enter)
if(entered)
{
//光标进入窗口的内容区
}
else
{
//光标离开窗口的内容区
}
}
您可以查询游标当前是否在具有glfw_hoved窗口属性的窗口的内容区域内。
if (glfwGetWindowAttrib(window, glfw_hover))
{
highlight_interface();
}
鼠标按键输入
如果您希望在鼠标按钮被按下或释放时收到通知,请设置鼠标按钮回调。
glfwSetMouseButtonCallback(window,mouse_button_callback);
回调函数接收鼠标按钮、按钮动作和修饰符位。
void mouse_button_callback(GLFWwindow* window, int button, int action, int mods)
{
if (button == GLFW_MOUSE_BUTTON_RIGHT && action == GLFW_PRESS)
popup_menu ();
}
动作为GLFW_PRESS或GLFW_RELEASE中的一种。
命名按钮的鼠标按钮状态也保存在每个窗口状态数组中,可以使用glfwGetMouseButton进行轮询。
int state = glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_LEFT);
if (state == GLFW_PRESS)
{
upgrade_cow ();
}
返回状态为GLFW_PRESS或GLFW_RELEASE。
此函数只返回缓存的鼠标按钮事件状态。它不会轮询系统以获取鼠标按钮的当前状态。
无论何时轮询状态,都有可能错过正在寻找的状态更改。如果按下的鼠标按钮在轮询其状态之前再次被释放,那么您将错过按下按钮。推荐的解决方案是使用鼠标按钮回调,但也有GLFW_STICKY_MOUSE_BUTTONS输入模式。
glfwSetInputMode(window, GLFW_STICKY_MOUSE_BUTTONS, GLFW_TRUE);
当鼠标粘贴按钮模式被启用时,鼠标按钮的可轮询状态将保持GLFW_PRESS,直到该按钮的状态被glfwGetMouseButton轮询。一旦它被轮询,如果在此期间已经处理了一个鼠标按钮释放事件,则状态将重置为GLFW_RELEASE,否则它将保持GLFW_PRESS。
GLFW_MOUSE_BUTTON_LAST常量保存所有命名按钮的最大值。
滚动的输入
如果您希望在用户滚动时得到通知,无论是使用鼠标滚轮还是触摸板手势,请设置滚动回调。
glfwSetScrollCallback(window,scroll_callback);
回调函数接收二维滚动偏移量。
void scroll_callback(GLFWwindow* window,double xoffset,double yoffset)
{
}
普通的鼠标滚轮是垂直的,它提供沿y轴的偏移量。