Link

导读

因为本章我们从一个已有的代码框架开始,所以我们将了解它实际的作用。

所有文件都存储在 project/src/ 文件夹中

  • vk_engine.h/cpp: 这将是引擎的主要类,教程的大部分代码将放在这里
  • vk_initializers.h/cpp: Vulkan 类型的初始化非常冗长,所以我们将在这里创建一些小的助手函数。它们有很多,所以它将成为独立的部分
  • vk_types.h: 随着教程的继续,我们将在这里添加“基本”类型,例如顶点定义。
  • main.cpp: 代码的入口点。除了调用 vk_engine 代码之外,没有其他内容

物理设计(项目如何布局)对于复杂项目非常重要。我们将一对匹配的 .h/.cpp 文件称为“模块”。它不一定必须只是一个类,或者根本不是一个类。我们将尽可能使它们通常是独立的。将它们称为模块的原因是,一旦 C++20 模块被广泛采用,它们很可能成为模块。

vk_types 是完全独立的(除了 Vulkan 之外不依赖任何东西),vk_initializers 组件也是如此。一旦它们增长,您可以安全地将它们作为您自己的小型抽象保留在您自己的项目中。vk_engine 将几乎成为一切的“终点”。它将依赖于项目的大部分。

在可能的情况下,我们将尝试使每个组件的头文件尽可能轻量级。头文件越轻,您的程序编译速度就越快,这在使用 C++ 时至关重要。

代码

main.cpp

#include <vk_engine.h>

int main(int argc, char* argv[])
{
	VulkanEngine engine;

	engine.init();

	engine.run();

	engine.cleanup();

	return 0;
}

我们从一些简单的东西开始,main.cpp。我们在这里什么都不做,只是立即调用 Vulkan 引擎方法。

将来,这可能是一个设置从命令行参数 argc/argv 或设置文件带来的配置参数的好地方。

vk_types.h 包含以下内容

#pragma once
#include <vulkan/vulkan.h>

我们包含了 Vulkan 的主头文件,您可以看到的是 <vulkan/vulkan.h>。这将包含我们需要的所有 Vulkan 函数定义和类型。#pragma once 是一个预处理器指令,告诉编译器永远不要在同一个文件中包含两次。它等效于 include guards,但更简洁。

vk_init.h 看起来像这样

#pragma once

#include <vk_types.h>

namespace vkinit {
}

我们包含了 vk_types 头文件,它引入了 Vulkan 本身(我们将需要它),并且我们为稍后将在此处添加的函数声明一个命名空间。

最后,我们进入 vk_engine.h,主类


#pragma once

#include <vk_types.h>

class VulkanEngine {
public:

	bool _isInitialized{ false };
	int _frameNumber {0};

	VkExtent2D _windowExtent{ 1700 , 900 };

    struct SDL_Window* _window{ nullptr };
	
	//initializes everything in the engine
	void init();

	//shuts down the engine
	void cleanup();

	//draw loop
	void draw();

	//run main loop
	void run();
};

与 vk_init 一样,我们包含了 vk_types。我们已经需要一个 Vulkan 类型 VkExtent2D。Vulkan 引擎将是我们将要做的所有事情的核心。我们有一个标志来知道引擎是否已初始化,一个帧号整数(非常有用!),以及我们要打开的窗口的大小,以像素为单位。

声明 struct SDL_Window* _window; 特别值得关注。请注意开头的 struct。这被称为前向声明,它允许我们在类中拥有 SDL_Window 指针,而无需在 Vulkan 引擎头文件中包含 SDL。此变量保存我们为应用程序创建的窗口。

看完头文件,让我们转到 cpp 文件。

vk_engine.cpp 第 1 行

#include "vk_engine.h"

#include <SDL.h>
#include <SDL_vulkan.h>

#include <vk_types.h>
#include <vk_initializers.h>

与其他文件不同,在这个文件中,我们包含了更多内容。我们同时包含了 <SDL.h><SDL_vulkan.h>。SDL.h 包含用于打开窗口和输入的主要 SDL 库数据,而 SDL_vulkan.h 包含用于打开 Vulkan 兼容窗口和其他 Vulkan 特定功能的 Vulkan 特定标志和功能。

vk_engine.cpp,第 10 行

void VulkanEngine::init()
{
    // We initialize SDL and create a window with it. 
	SDL_Init(SDL_INIT_VIDEO);

	SDL_WindowFlags window_flags = (SDL_WindowFlags)(SDL_WINDOW_VULKAN);
	
    //create blank SDL window for our application
	_window = SDL_CreateWindow(
		"Vulkan Engine", //window title
		SDL_WINDOWPOS_UNDEFINED, //window position x (don't care)
		SDL_WINDOWPOS_UNDEFINED, //window position y (don't care)
		_windowExtent.width,  //window width in pixels
		_windowExtent.height, //window height in pixels
		window_flags 
	);
	
	//everything went fine
	_isInitialized = true;
}

在这里我们看到了我们的第一个真正的代码,以创建 SDL 窗口的形式。我们做的第一件事是初始化 SDL 库。SDL 库包含很多东西,所以我们必须发送一个标志,说明我们想要使用什么。SDL_INIT_VIDEO 告诉 SDL 我们想要主要的窗口功能。这还包括基本的输入事件,如键盘或鼠标。

一旦 SDL 被初始化,我们就使用它来创建一个窗口。窗口存储在 _window 成员中以供以后使用。

因为 SDL 是一个 C 库,它不支持构造函数和析构函数,所以必须手动删除东西。

如果窗口被创建,那么也必须被销毁。

vk_engine.cpp,第 29 行

void VulkanEngine::cleanup()
{	
	if (_isInitialized) {
		SDL_DestroyWindow(_window);
	}
}

以类似于我们执行 SDL_CreateWindow 的方式,我们需要执行 SDL_DestroyWindow。这将销毁程序的窗口。随着时间的推移,我们将向此清理函数添加更多逻辑。虽然不完全需要正确清理,因为当程序终止时,操作系统无论如何都会为我们删除所有内容,但这样做是一个好习惯。

vk_engine.cpp,第 37 行

void VulkanEngine::draw()
{
	//nothing yet
}

我们的 draw 函数现在是空的,但这里是我们将添加渲染代码的地方。

vk_engine.cpp,第 42 行

void VulkanEngine::run()
{
	SDL_Event e;
	bool bQuit = false;

	//main loop
	while (!bQuit)
	{
		//Handle events on queue
		while (SDL_PollEvent(&e) != 0)
		{
			//close the window when user clicks the X button or alt-f4s
			if (e.type == SDL_QUIT) bQuit = true;
		}

		draw();
	}
}

这是我们的应用程序主循环。我们在 while() 中有一个无限循环,只有当 SDL 接收到 SDL_QUIT 事件时才会停止。

在内部循环的每次迭代中,我们都执行 SDL_PollEvent。这将向 SDL 询问操作系统在上一个帧期间发送给应用程序的所有事件。在这里,我们可以检查键盘事件、鼠标移动、窗口移动、最小化和许多其他事件。目前,我们只对 SDL_QUIT 事件感兴趣。当操作系统请求关闭窗口时,会调用此事件。

最后,在主循环的每次迭代中,我们都调用 draw();

现在我们已经看到了如何使用 SDL 打开窗口,基本上没有其他内容。

此时真正可以添加的只有一件事,那就是尝试使用 SDL 事件。

作为一个练习,阅读 SDL2 的文档,并尝试获取按键事件,使用 std::cout 记录它们。

现在我们可以继续进行第一章,并开始进行渲染循环。

下一步:初始化 Vulkan