在开始使用 Vulkan 和 Qt 开发游戏之前,您需要做一些准备工作。首先,您需要安装 Vulkan SDK。为此,请前往 https://www.lunarg.com/vulkan-sdk/,下载适用于您的操作系统的文件,然后执行或解压缩它。检查安装文件夹中 doc 子目录中的 index.html 文件以查看是否需要执行任何其他操作。
接下来,您需要一个支持 Vulkan 的 Qt 构建;它必须是 Qt 5.10 或更高版本。如果您已通过安装程序安装了可用的最新版本,则它可能已经适用。
要检查您的 Qt 版本是否支持 Vulkan,请创建一个新的 Qt 控制台应用程序,确保您选择与最近安装的 Qt 版本对应的套件。 Vulkan SDK 还要求您设置一些环境变量,例如 VULKAN_SDK、PATH、LD_LIBRARY_PATH 和 VK_LAYER_PATH(具体名称和值可能取决于操作系统,因此请参阅 SDK 文档)。您可以通过切换到 Qt Creator 的 Projects 窗格并展开 Build Environment 部分来编辑项目的环境变量。
与 OpenGL 不同,Vulkan 没有全局状态。与 Vulkan 的交互从 VkInstance 类型表示的实例对象开始。应用程序通常会创建一个包含应用程序范围状态的单个 VkInstance 对象。所有其他 Vulkan 对象只能从实例对象创建。在 Qt 中,对应的类是 QVulkanInstance。此类提供了一种配置 Vulkan 的便捷方法,然后使用给定的配置对其进行初始化。您还可以使用其 supportedExtensions() 和 supportedLayers() 函数在使用之前查询支持的功能。配置完成后,您应该调用 create() 函数,该函数实际触发加载 Vulkan 库并创建 VkInstance 对象。如果此函数返回 true,则 Vulkan 实例对象已准备好使用。
main.cpp
#include <QGuiApplication>
#include <QVulkanWindow>
#include <vulkan/vulkan.h>
#include "myrenderer.h"
#include <QVulkanInstance>
#include <QVulkanFunctions>
#include <QLoggingCategory>
#include "mywindow.h"
int main(int argc, char *argv[])
{
QGuiApplication app(argc, argv);
QLoggingCategory::setFilterRules(QStringLiteral("qt.vulkan=true"));
//应用程序通常会创建一个包含应用程序范围状态的单个 VkInstance 对象QVulkanInstance
QVulkanInstance vulkan;
vulkan.setLayers({ "VK_LAYER_LUNARG_standard_validation" });
if (!vulkan.create()) {
qFatal("Failed to create Vulkan instance: %d", vulkan.errorCode());
}
//创建一个能够进行 Vulkan 渲染的窗口
//这是通过继承 QVulkanWindow 类来完成的
//与 QOpenGLWindow 类似,QVulkanWindow 扩展了 QWindow 并提供了利用 Vulkan 功能所需的功能以及一些便利功能
//我们创建了一个将使用 Vulkan 渲染的窗口
MyWindow window;
window.resize(1024, 768);
//数初始化 Vulkan,创建一个窗口,将实例对象传递给该窗口,并将其显示在屏幕上
window.setVulkanInstance(&vulkan);
window.show();
//当窗口显示时,Qt 将调用窗口上的 createRenderer() 函数,并在您的此函数实现中创建一个新的渲染器对象。
return app.exec();
}
myrenderer.h
#include <QVulkanWindow>
//MyRenderer 的类应从 QVulkanWindowRenderer 派生
class MyRenderer : public QVulkanWindowRenderer
{
public:
MyRenderer(QVulkanWindow *w);
void initResources();
void startNextFrame() override;
protected:
//将 QVulkanWindow *m_window 私有字段添加到渲染器类
QVulkanWindow *m_window;
QVulkanDeviceFunctions *m_devFuncs;
float m_hue = 0;
};
myrenderer.cpp
#include "myrenderer.h"
#include <QVulkanFunctions>
#include <QFile>
MyRenderer::MyRenderer(QVulkanWindow *w)
: m_window(w)
{
}
void MyRenderer::initResources()
{
//现在您可以使用 m_devFuncs 访问 Vulkan API 函数
VkDevice device = m_window->device();
//添加并初始化 m_devFuncs 私有字段
m_devFuncs = m_window->vulkanInstance()->deviceFunctions(device);
}
//每次需要绘制窗口时,Qt 都会调用渲染器的 startNextFrame() 函数。
//与paintEvent() 的工作方式类似,默认情况下不会连续调用startNextFrame()
//它只会在显示窗口后调用一次
//如果需要连续渲染动态场景,调用m_window->frameReady()后调用m_window->requestUpdate()
void MyRenderer::startNextFrame()
{
//背景颜色的当前色调的浮动 m_hue 私有字段
//将其初始值设置为零
m_hue += 0.005f;
//我们增加 m_hue 变量并确保我们不会越界
if (m_hue > 1.0f) {
m_hue = 0.0f;
}
//然后,我们使用 QColor::fromHslF() 函数根据给定的色调、饱和度和亮度(每个范围从 0 到 1)构造一个 QColor值
QColor color = QColor::fromHslF(m_hue, 1, 0.5);
//使用这个颜色变量来构造一个 VkClearValue 数组,我们将使用它来设置背景颜色
VkClearValue clearValues[2];
memset(clearValues, 0, sizeof(clearValues));
clearValues[0].color = {
static_cast<float>(color.redF()),
static_cast<float>(color.greenF()),
static_cast<float>(color.blueF()),
1.0f
};
clearValues[1].depthStencil = { 1.0f, 0 };
//要在 Vulkan 中开始新的渲染
//我们需要初始化一个 VkRenderPassBeginInfo 结构
VkRenderPassBeginInfo info;
memset(&info, 0, sizeof(info));
//它需要大量数据,但幸运的是,QVulkanWindow 为我们提供了大部分数据
//我们只需要将它放入结构中并使用我们之前设置的 clearValues 数组
info.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
info.renderPass = m_window->defaultRenderPass();
info.framebuffer = m_window->currentFramebuffer();
const QSize imageSize = m_window->swapChainImageSize();
info.renderArea.extent.width = imageSize.width();
info.renderArea.extent.height = imageSize.height();
info.clearValueCount = 2;
//设置的 clearValues 数组
info.pClearValues = clearValues;
VkCommandBuffer commandBuffer = m_window->currentCommandBuffer();
//vkCmdBeginRenderPass() Vulkan API 函数将开始渲染,这将导致使用我们设置的颜色清除窗口。
m_devFuncs->vkCmdBeginRenderPass(commandBuffer, &info,
VK_SUBPASS_CONTENTS_INLINE);
//由于我们没有其他东西可绘制,因此我们立即使用 vkCmdEndRenderPass() 函数完成渲染过程。
m_devFuncs->vkCmdEndRenderPass(commandBuffer);
//每一帧的绘制都以调用 frameReady() 结束
//在调用此函数之前,无法完成帧的处理
//但是,不需要直接从 startNextFrame() 函数调用此函数。例如,如果您需要等待计算在单独的线程中完成,您可以延迟此调用。
m_window->frameReady();
m_window->requestUpdate();
}
mywindow.h
#ifndef MYWINDOW_H
#define MYWINDOW_H
#include <QObject>
#include <QVulkanWindow>
class MyWindow : public QVulkanWindow {
public:
QVulkanWindowRenderer *createRenderer() override;
};
#endif // MYWINDOW_H
mywindow.cpp
#include "mywindow.h"
#include "myrenderer.h"
QVulkanWindowRenderer *MyWindow::createRenderer() {
//渲染器是附加到窗口上的,会自动随它一起删除,所以不需要手动删除。
return new MyRenderer(this);
}