Ch1-1 创建GLFW窗口

本章的示例代码参见:EasyVulkan_Ch1

关于本节,如果你倾向于直接使用WindowsAPI来创建窗口,你也可以参考示例代码中的WinGeneral.hpp

包含并链接必要的文件

在一切开始之前,来创建一个用于包含各种库的头文件,名称随你喜欢,我就决定叫它EasyVKStart.h
然后加入以下代码:

#pragma once
//可能会用上的C++标准库
#include <iostream>
#include <fstream>
#include <sstream>
#include <vector>
#include <stack>
#include <map>
#include <unordered_map>
#include <span>
#include <memory>
#include <functional>
#include <concepts>
#include <format>
#include <chrono>
#include <numeric>
#include <numbers>

//GLM
#define GLM_FORCE_DEPTH_ZERO_TO_ONE
//如果你惯用左手坐标系,在此定义GLM_FORCE_LEFT_HANDED
#include <glm.hpp>
#include <gtc/matrix_transform.hpp>

//stb_image.h
#include <stb_image.h>

//Vulkan
#ifdef _WIN32                        //考虑平台是Windows的情况(请自行解决其他平台上的差异)
#define VK_USE_PLATFORM_WIN32_KHR    //在包含vulkan.h前定义该宏,会一并包含vulkan_win32.h和windows.h
#define NOMINMAX                     //定义该宏可避免windows.h中的min和max两个宏与标准库中的函数名冲突
#pragma comment(lib, "vulkan-1.lib") //链接编译所需的静态存根库
#endif
#include <vulkan/vulkan.h>
  • GLM本为是OpenGL设计的,在OpenGL中,NDC(标准化设备坐标系)的深度范围为[-1, 1],而Vulkan中这个范围为[0, 1],因此我们必须用宏GLM_FORCE_DEPTH_ZERO_TO_ONE来指定深度范围,这样才能获得正确的投影矩阵。

由于你以后可能会想要将你的程序移植到其他平台,出于通用性的考虑,GLFW相关的内容会被单独拿出来。

接着创建VKBase.h,该文件会用来写Vulkan相关的基本内容。
VKBase.h中加入以下代码:

#pragma once
#include "EasyVKStart.h"
//定义vulkan命名空间,之后会把Vulkan中一些基本对象的封装写在其中
namespace vulkan {

}

最后创建一个GlfwGeneral.hpp,顾名思义,这个hpp用来写一些GLFW的通用内容及相关的全局变量。
GlfwGeneral.hpp中加入以下代码:

#include "VKBase.h"
#define GLFW_INCLUDE_VULKAN
#include <GLFW/glfw3.h>
#pragma comment(lib, "glfw3.lib") //链接编译所需的静态库
  • 要将GLFW用于Vulkan,必须在包含GLFW之前,定义GLFW_INCLUDE_VULKAN宏。

  • 如果你在编译时使用静态链接,则这里的glfw3.lib改为glfw3_mt.lib

想必以上代码无需多做解释,全部抄完后,如果VS提示你有任何文件打不开,检查一下你的附加包含目录和附加库目录。

创建窗口

GlfwGeneral.hpp添加以下全局变量:

//窗口的指针,全局变量自动初始化为NULL
GLFWwindow* pWindow;
//显示器信息的指针
GLFWmonitor* pMonitor;
//窗口标题
const char* windowTitle = "EasyVK";

接着来写两个函数

bool InitializeWindow(VkExtent2D size, bool fullScreen = false, bool isResizable = true, bool limitFrameRate = true) {
/*待后续填充*/
}
void TerminateWindow() {
/*待后续填充*/
}

InitializeWindow(...)用于初始化窗口,我打算让它在初始化成功时返回true,否则返回false

参数说明

size

窗口大小

fullScreen

指定是否以全屏初始化窗口

isResizable

指定窗口是否可拉伸,游戏窗口通常是不可任意拉伸的

limitFrameRate

指定是否将帧数限制到不超过屏幕刷新率,在本节先不实现这个参数的作用

TerminateWindow(...)用于终止窗口时,清理GLFW。

接着来填充InitializeWindow(...)。
首先用glfwInit(...)初始化GLFW,该函数在执行成功时返回true

if (!glfwInit()) {
    std::cout << std::format("[ InitializeWindow ] ERROR\nFailed to initialize GLFW!\n");
    return false;
}
  • 这里用std::cout << std::format("...");输出字符串字面量是为了同带更多参数的格式化输出std::cout << std::format("...", ...);统一,而这是为了方便日后把std::cout << std::format(...);一次性替换成某个格式化输出函数(...);,比如会在C++23上线的print(...)。

然后,在创建窗口前,必须调用glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API)。GLFW同GLM一样最初是为OpenGL设计的,GLFW_CLIENT_API的默认设置是GLFW_OPENGL_API,这种情况下,GLFW会在创建窗口时创建OpenGL的上下文,这对于Vulkan而言是多余的,所以向GLFW说明不需要OpenGL的API。

glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);

接着用glfwWindowHint(GLFW_RESIZABLE, isResizable)指定窗口可否拉伸:

glfwWindowHint(GLFW_RESIZABLE, isResizable);

然后便能用glfwCreateWindow(...)创建窗口了,该函数在执行成功时返回一非空指针。

pWindow = glfwCreateWindow(size.width, size.height, windowTitle, nullptr, nullptr);

想必glfwCreateWindow(...)的前三个参数一目了然不必多说,其第四个参数用于指定全屏模式的显示器,若为nullptr则使用窗口模式,第五个参数可传入一个其他窗口的指针,用于与其他窗口分享内容。

如果你想实现全屏,则还需要用glfwGetPrimaryMonitor()取得当前显示器信息的指针,即便你在初始化时不想全屏,也有必要先取得它以备之后使用:

pMonitor = glfwGetPrimaryMonitor();

通常全屏时的图像区域应当跟屏幕分辨率一致,因此还需要使用glfwGetVideoMode(...)取得显示器当前的视频模式:

const GLFWvidmode* pMode = glfwGetVideoMode(pMonitor);
  • 视频模式可能因为用户的操作而在程序运行过程中发生变更,因此总是在需要时获取,而不将其存储到全局变量。

于是,若要实现全屏,根据fullScreen的值决定是否以全屏模式初始化窗口:

pWindow = fullScreen ?
    glfwCreateWindow(pMode->width, pMode->height, windowTitle, pMonitor, nullptr) :
    glfwCreateWindow(size.width, size.height, windowTitle, nullptr, nullptr);

验证pWindow的值,若窗口创建失败,用glfwTerminate()来清理GLFW并让函数返回false

if (!pWindow) {
    std::cout << std::format("[ InitializeWindow ]\nFailed to create a glfw window!\n");
    glfwTerminate();
    return false;
}

当然TerminateWindow()里也会调用glfwTerminate(),于是,至此为止InitializeWindow(...)和TerminateWindow()的内容总结如下:

bool InitializeWindow(VkExtent2D size, bool fullScreen = false, bool isResizable = true, bool limitFrameRate = true) {
    if (!glfwInit()) {
        std::cout << std::format("[ InitializeWindow ] ERROR\nFailed to initialize GLFW!\n");
        return false;
    }
    glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
    glfwWindowHint(GLFW_RESIZABLE, isResizable);
    pMonitor = glfwGetPrimaryMonitor();
    const GLFWvidmode* pMode = glfwGetVideoMode(pMonitor);
    pWindow = fullScreen ?
        glfwCreateWindow(pMode->width, pMode->height, windowTitle, pMonitor, nullptr) :
        glfwCreateWindow(size.width, size.height, windowTitle, nullptr, nullptr);
    if (!pWindow) {
        std::cout << std::format("[ InitializeWindow ]\nFailed to create a glfw window!\n");
        glfwTerminate();
        return false;
    }
    /*待Ch1-3和Ch1-4填充*/
    return true;
}
void TerminateWindow() {
    /*待Ch1-4填充*/
    glfwTerminate();
}

Important

注意,你不需要手动 delete pMode,GLFW的注释及官方文档中写到:
The returned array is allocated and freed by GLFW. You should not free it yourself. It is valid until the specified monitor is disconnected or the library is terminated.

在窗口标题上显示帧率

下面是一个用于在标题上显示帧率的函数,每帧调用一次。约每一秒更新一次帧率,其逻辑为:
记录时间点t0,若之后某次调用该函数时取得的时间t1已超过t0一秒,用t1t0的差除以这中间经历的帧数,得到帧率,并将t1赋值给t0
得到帧率后,将其转为字符串衔接在原标题后,然后用glfwSetWindowTitle(...)设置窗口标题。

void TitleFps() {
    static double time0 = glfwGetTime();
    static double time1;
    static double dt;
    static int dframe = -1;
    static std::stringstream info;
    time1 = glfwGetTime();
    dframe++;
    if ((dt = time1 - time0) >= 1) {
        info.precision(1);
        info << windowTitle << "    " << std::fixed << dframe / dt << " FPS";
        glfwSetWindowTitle(pWindow, info.str().c_str());
        info.str("");//别忘了在设置完窗口标题后清空所用的stringstream
        time0 = time1;
        dframe = 0;
    }
}

glfwGetTime()返回自GLFW初始化以来的时间,单位为秒。 我在WinGeneral.hpp中也提供了通过C++标准库函数来取得时间的方法。

试着运行一下吧!

main.cpp中书写主函数:

#include "GlfwGeneral.hpp"

int main() {
    if (!InitializeWindow({1280,720}))
        return -1;//来个你讨厌的返回值
    while (!glfwWindowShouldClose(pWindow)) {

        /*渲染过程,待填充*/

        glfwPollEvents();
        TitleFps();
    }
    TerminateWindow();
    return 0;
}
  • glfwPollEvents()用于处理GLFW相关的事件,如接收窗口变化的信息,及接受输入并执行回调函数。

  • glfwWindowShouldClose(...)返回true,说明在前一次中收到了来自操作系统的信息,告诉GLFW应当关闭窗口。

如果你在编译后看到了一个全白的窗口,并且帧率还炸了,那就算是一切顺利!

如果你在控制台中见到信息“libpng warning: iCCP: cHRM chunk does not match sRGB”,那是因为你正在使用QQ输入法(这真的很奇怪,所以我特地提一嘴)。

在全屏和窗口模式间切换

本节为非必要项,与今后的学习内容无关。

glfwSetWindowMonitor(...)可以在全屏和窗口模式间切换。

void glfwSetWindowMonitor(...) 的参数说明

GLFWwindow* window

窗口的指针

GLFWmonitor* monitor

显示器信息的指针,用于指定全屏模式的显示器

int xpos

图像区域左上角在屏幕上的横坐标

int ypos

图像区域左上角在屏幕上的纵坐标

int width

图像区域的宽度

int height

图像区域的高度

int refreshRate

屏幕刷新率

  • 所指图像区域不包括窗口的边框和标题栏。

  • 切换到全屏模式时,xposypos被无视。

  • 切换到窗口模式时,ypos应避免小于标题栏高度。

GlfwGeneral.hpp中定义函数MakeWindowFullScreen()和MakeWindowWindowed(...)分别用于从窗口变为全屏和从全屏变回窗口:

void MakeWindowFullScreen() {
    const GLFWvidmode* pMode = glfwGetVideoMode(pMonitor);
    glfwSetWindowMonitor(pWindow, pMonitor, 0, 0, pMode->width, pMode->height, pMode->refreshRate);
}
void MakeWindowWindowed(VkOffset2D position, VkExtent2D size) {
    const GLFWvidmode* pMode = glfwGetVideoMode(pMonitor);
    glfwSetWindowMonitor(pWindow, nullptr, position.x, position.y, size.width, size.height, pMode->refreshRate);
}

接着就是获取鼠标/键盘输入,在按下你期望的按键后调用上述函数了。
关于如何获取输入在此不做赘述,请自行参阅GLFW的Input guide