[Vulkan]绘制一个三角形/设置/基本代码

通用结构

上一节,我们已经启动了第一个黑色的窗口,它的基本代码如下:

  1. #include <vulkan/vulkan.h>
  2. #include <iostream>
  3. #include <stdexcept>
  4. #include <cstdlib>
  5. class HelloTriangleApplication {
  6. public:
  7. void run() {
  8. initVulkan();
  9. mainLoop();
  10. cleanup();
  11. }
  12. private:
  13. void initVulkan() {
  14. }
  15. void mainLoop() {
  16. }
  17. void cleanup() {
  18. }
  19. };
  20. int main() {
  21. HelloTriangleApplication app;
  22. try {
  23. app.run();
  24. } catch (const std::exception& e) {
  25. std::cerr << e.what() << std::endl;
  26. return EXIT_FAILURE;
  27. }
  28. return EXIT_SUCCESS;
  29. }

最上面我们包含了LunarG SDK提供的Vulkan头文件vulkan/vulkan.h,引入了函数、结构体和枚举。stdexceptiostream用来使用异常和打印日志。cstdlib引入EXIT_SUCCESSEXIT_FAILURE

我们将程序封装为一个类,将Vulkan对象作为其私有成员,同时创建一些函数初始化它们,在initVulkan中调用它。所有都准备好后,我们就进入主循环来渲染画面。我们在mainLoop中创建一个无限循环,直到窗口退出。窗口退出,mainLoop返回之后,我们使用cleanup函数来释放资源。

运行时发生任何异常,我们就抛出一个附带描述性信息的std::runtime_error异常。它会被main函数收到并打印出来。我们用std::exception接受类型更全面的异常类型。我们很快就会接触到一个异常的例子,它在程序未找到需要的扩展时产生。

资源管理

mallocfree一样,每个我们创建的Vulkan对象,在不需要时都需要被显式销毁。在C++中,你可以使用RAII机制或memory提供的智能指针来完成资源管理工作。不过,在本教程中,我们选择显式分配或销毁Vulkan对象,毕竟,Vulkan重要的特性就是显式操作以避免出错。所以,明白Vulkan对象的生命周期对于学习API如何工作非常有利。

本教程之后,你可以实现自动的内存管理机制。比如,编写自定义类型,在构造函数时获取Vulkan对象,在析构函数时释放对象。又比如,你可以使用std::unique_ptrstd::shared_ptr。这取决与你的需求。RAII是大型Vulkan程序的推荐模型。不过对于学习而言,了解幕后发生的事情总是有好处的。

Vulkan对象要么是通过vkCreateXXX直接创建的,要么是通过其它对象使用vkAllocateXXX分配的。当一个对象不再使用后,你应该使用对应的vkDestroyXXXvkFreeXXX销毁它。这些函数的参数一般是不同的,但它们都有一个共同的参数:pAllocator。它是一个可选的参数,它允许你自定义内存分配器。在本教程中,我们会忽略它并始终传入nullptr

集成GLFW

如果您想将 Vulkan 用于离屏渲染,则无需创建窗口即可完美运行。但实际显示内容更让人兴奋。我们将#include <vulkan/vulkan.h>替换为:

  1. #define GLFW_INCLUDE_VULKAN
  2. #include <GLFW/glfw3.h>
  3. 12

这样,GLFW会引入自己的定义和Vulkan的头文件。添加一个initWindow函数并在run中调用。我们会用这个函数初始化GLFW并创建一个窗口。

  1. void run() {
  2. initWindow();
  3. initVulkan();
  4. mainLoop();
  5. cleanup();
  6. }
  7. private:
  8. void initWindow() {
  9. }

initWindow中,我们首先调用glfwInit()来初始化GLFW库。GLFW一开始是用来创建OpenGL上下文的,我们需要告诉它不要这样做:

  1. glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
  2. 1

改变窗口大小需要我们有一些特别的操作,之后会讨论,我们现在先禁止窗口改变大小:

  1. glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE);
  2. 1

接下来就是创建一个实际的窗口。添加一个私有成员变量GLFWwindow* window;保存窗口指针,初始化:

  1. window = glfwCreateWindow(800, 600, "Vulkan", nullptr, nullptr);
  2. 1

参数依次是窗口的宽、高、名称和显示器,最后一个参数和OpenGL有关系。

我们最好是建些常量来保存窗口宽高,因为它们之后还会被用到。在HelloTriangleApplication类前定义:

  1. const uint32_t WIDTH = 800;
  2. const uint32_t HEIGHT = 600;
  3. 12

创建窗口改为:

  1. window = glfwCreateWindow(WIDTH, HEIGHT, "Vulkan", nullptr, nullptr);
  2. 1

现在,initWindow函数大概是这样的:

  1. void initWindow() {
  2. glfwInit();
  3. glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
  4. glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE);
  5. window = glfwCreateWindow(WIDTH, HEIGHT, "Vulkan", nullptr, nullptr);
  6. }
  7. 12345678

程序应一直保持运行,除非发生错误或窗口被关闭。我们需要添加一个事件循环到mainLoop

  1. void mainLoop() {
  2. while (!glfwWindowShouldClose(window)) {
  3. glfwPollEvents();
  4. }
  5. }
  6. 12345

这段代码不言自明。它检查诸如键盘事件等直到窗口被用户关闭。这里也是我们调用绘制一帧函数的地方。

窗口关闭之后,我们需要清理资源,结束GLFW,现在的cleanup是这样的:

  1. void cleanup() {
  2. glfwDestroyWindow(window);
  3. glfwTerminate();
  4. }
  5. 12345

现在运行程序的话,会显示一个叫Vulkan的窗口。程序会一直运行,直到窗口关闭。现在,Vulkan程序的基本结构已经有了,接下来就是创建第一个Vulkan对象了。