Vulkan Tutorial 教程翻译(四) 绘制三角形 2.2 呈现

呈现

窗口表面

由于Vulkan 是一个平台无关的API,它本身并没有提供与窗口直接交互的接口,为了建立窗口与Vulkan的连接以将最终的渲染结果呈现到屏幕上,我们需要使用WSI扩展(窗口集成系统),在本章,首先要讨论的是 VK_KHR_surface. 它对外暴露了一个 VkSurfaceKHR 的对象,其代表的是一个呈现渲染结果的抽象概念,在我们的程序中,这个表面的后端是通过GLFW打开的窗口。

这个 VK_KHR_surface 是一个实例级的扩展,实际上我们已经启用它了,因为它的名字被包含在 glfwGetRequiredInstanceExtensions 返回的列表中.这个列表还包含了其他的WSI扩展,我们会在稍后使用到。

窗口表面需要在实例创建之后被创建出来,因为它可以影响到物理设备的选择,我们推迟到现在才介绍这个主题,是因为渲染目标和呈现渲染结果是一个庞大的主题,会让之前的基础安装章节显得过于杂乱。也应当注意到,窗口表面对 Vulkan来说是一个完全可选的组件,如果你仅需要离屏渲染的功能,Vukan就不必像OpenGL那样再创建一个不可见的窗口了。

窗口表面的创建

首先在调试对象回调的下面新增一个 surface 成员类

c++ 复制代码
VkSurfaceKHR surface;

尽管 VkSurfaceKHR 对象它的使用是平台无关的,但是它的创建却不是这样的,因为创建时需要依赖操作系统窗口的许多细节处理,例如在 Windows 中,需要 HWND 和 HMODULE 句柄,因此需要一个平台相关的扩展,在Windows上叫做 VK_KHR_win32_surface,这个也是自动被包含在glfwGetRequiredInstanceExtensions 返回的列表中。

我将展示如何在Windows上使用特定平台的扩展去创建表面,但并不会在教程中实际去使用它,没有必要在使用了GLFW库的情况下再去编写平台相关的处理代码,GLFW库提供了 glfwCreateWindowSurface 来帮助我们处理平台之间的差异性。虽然这么说,但在我们开始前使用它之前,还是需要了解背后的原理。

为了访问平台原生的函数,你需要更新以下顶部头文件:

c++ 复制代码
#define VK_USE_PLATFORM_WIN32_KHR
#define GLFW_INCLUDE_VULKAN
#include <GLFW/glfw3.h>
#define GLFW_EXPOSE_NATIVE_WIN32
#include <GLFW/glfw3native.h>

因为surface是一个Vulkan对象, 需要在创建时填写结构体 VkWin32SurfaceCreateInfoKHR,其有两个重要的参数,hwnd 和 hinstance,它们是窗口及进程的句柄。

c++ 复制代码
VkWin32SurfaceCreateInfoKHR createInfo{};
createInfo.sType = VK_STRUCTURE_TYPE_WIN32_SURFACE_CREATE_INFO_KHR;
createInfo.hwnd = glfwGetWin32Window(window);
createInfo.hinstance = GetModuleHandle(nullptr);

glfwGetWin32Window 函数用于从GLFW的window中获取最原始的句柄。GetModuleHandle 函数的返回当前的进程句柄。

在这之后 surface 就可以通过vkCreateWin32SurfaceKHR被创建了,这个函数包含的参数有实例,创建的信息,自定义的分配器和用于接收创建对象的返回值.尽管这是一个 WSI的扩展,但因为它太常用了,所以Vulkan会在载入时候直接包含它,不必像其他扩展函数一样再显示的去载入它。

c++ 复制代码
if (vkCreateWin32SurfaceKHR(instance, &createInfo, nullptr, &surface) != VK_SUCCESS) {
    throw std::runtime_error("failed to create window surface!");
}

在其他平台,例如Linux上,处理过程也是类似的。vkCreateXcbSurfaceKHR 会使用xcb连接和窗口,它们都是基于X11 (libx11)来创建出来的。

glfwCreateWindowSurface 函数屏蔽了不同平台间的差异,我们现在将它引入到我们的程序中来,添加一个 createSurface 函数,在initVulkan中,instance创建后调用它.

c++ 复制代码
void initVulkan(){
    createInstance();
    setupDebugMessenger();
    createSurface();

    pickPhysicalDevice();
    createLogicalDevice();
}

void createSurface() {

}

GLFW 的函数接收更简单的参数,而不是结构体,createSurface的实现更加简单。

C++ 复制代码
void createSurface() {
    if (glfwCreateWindowSurface(instance, window, nullptr, &surface) != VK_SUCCESS) {
        throw std::runtime_error("failed to create window surface!");
    }
}

参数分别是 实例对象,GLFW创建出的窗口,自定义的分配器以及一个指向VkSurfaceKHR的指针,它只传递对应平台的调用运行结果。GLFW并没有提供一个专用的销毁surface的函数,因为这个操作Vulkan原始API就可以直接实现。

c++ 复制代码
void cleanup(){
     ...
    vkDestroySurfaceKHR(instance, surface, nullptr);
    vkDestroyInstance(instance, nullptr);
    ...
}

确保surface在 instance之前被销毁。

查询呈现操作关联的信息

尽管Vulkan实现了对窗口系统的集成,但这并不意味着每一个设备都支持。因此我们需要扩展 isDeviceSuitable 函数,去确保设备可以把我们创建出的图像显示到窗口上,由于呈现功能是与队列相关的属性,所以现在的问题就变成了,我们需要找到一个支持呈现功能可以将内容显示到表面上的队列簇。

的确有可能,队列簇提供绘制指令,但却不支持显示到surface。因此我们必须考虑到要最呈现层的队列做出区分,修改QueueFamilyIndices 结构体

C++ 复制代码
struct QueueFamilyIndices{
    std::optional<uint32_t> graphicsFamily;
    std::optional<uint32_t> presentFamily;

    bool isComplete() {
        return graphicsFamily.has_value() && presentFamily.has_value();
    }
};

下一步,要修改findQueueFamilies 函数,去找到一个可以将图像呈现到我们创建的表面的队列簇,利用函数 vkGetPhysicalDeviceSurfaceSupportKHR ,它接收的参数是,物理设备,队列簇索引,surface表面,最后的参数返回是否支持,在与查询图形支持的循环中调用它。

C++ 复制代码
VkBool32 presentMode = false;
vkGetPhysicalDeviceSurfaceSupportKHR(device, i, surface, &presentMode);

检查返回的布尔值,存储支持显示的队列簇索引。

C++ 复制代码
if(presentSupport){
    indices.presentFamily = i;
}

注意,可能最终我们会得到相同的队列簇,可我们为了保持统一的处理方式,程序中总是把它当作单独的队列来处理。尽管如此,你可以显示的假定物理设备就是同时支持图形渲染和显示命令的,从而提高效率。

创建呈现队列

接下来,在创建逻辑设备的时候,设置其也创建出一个呈现队列,并使用 VkQueue 句柄来保存它.新增一个成员变量

C++ 复制代码
VkQueue presentQueue;

接着,需要有多个 VkDeviceQueueCreateInfo 结构体,用来创建不同队列簇对应的队列,一个较为优雅的方式是为队列簇创建一个集合,并用此集合去创建队列:

C++ 复制代码
#include <set>
...

QueueFamilyIndices indices = findQueueFamily(physicalDevice);

std::vector<VkDeviceQueueCreateInfo> queueCreateInfos;
std::set<uint32_t> uniqueQueueFamilies = {indices.graphicsFamily.value(),indices.presentFamily.value()};

float queueProprity = 1.0f;
for(uint32_t queueFamily : uniqueQueueFamilies){
    VkDeviceQueueCreateInfo queueCreateInfo{};
    queueCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO;
    queueCreateInfo.queueFamilyIndex = queueFamily;
    queueCreateInfo.queueCount = 1;
    queueCreateInfo.pQueuePriorities = &queuePriority;
    queueCreateInfos.push_back(queueCreateInfo);
}

修改 VkDeviceCreateInfo 的信息,指向上面创建出的 queueCreateInfos 容器。

C++ 复制代码
createInfo.queueCreateInfoCount = static_cast<uint32_t>(queueCreateInfos.size());
createInfo.pQueueCreateInfos = queueCreateInfos.data();

如果队列簇相同,只需要传递一次它的索引,最后增加调用去接收队列句柄

C++ 复制代码
vkGetDeviceQueue(device, indices.presentFamily.value(), 0, &presentQueue);

在队列簇相同的情况下,这两个队列句柄其实是同一个。在下一章,我们会研究交换链,看到它是怎样将图像显示到表面上的。

交换链

Vulkan并没有默认帧缓冲区的概念,因此需要这样的基础设施,它拥有这样的缓冲区接收我们提交的渲染结果,这个基础设施就是交换链,在Vulkan中它必须被显式地创建。交换链地本质,是一队等待被显示到屏幕上地图像,我们的应用程序会从交换链请求一张图像,在上面进行绘制,在绘制完成后返还给队列。怎样让这个队列正确的工作,如何从队列中显示出图像,这依赖于交换链是如何被创建的。通常交换链用于同步要呈现的图像与屏幕刷新率

检查交换链的支持

并不是所有的显卡都支持将图像直接显示到屏幕上,例如服务端设备或者没用输出的外设,另外图像的呈现也重度依赖于图形系统和用于显示的窗口,这也并不是Vulkan的核心,所以在查询完设备的支持后你必须启用 VK_KHR_swapchain 设备扩展。

因此我们首先完善 isDeviceSuitable 函数,检查这个扩展是否支持,之前已经看到如何通过 VkPhysicalDevice 列出支持的扩展,所以做法很简单。注意Vulkan 的头文件提供了一个宏 VK_KHR_SWAPCHAIN_EXTENSION_NAME 定义为 "VK_KHR_swapchain",防止拼写上的错误。

首先定义一个需要的设备扩展列表,与打开验证层的方式类似。

C++ 复制代码
const std::vector<const char *> deviceExtensions = {
    VK_KHR_SWAPCHAIN_EXTENSION_NAME
};

接下来,创建一个新函数 checkDeviceExtensionSupport(), 它在 isDeviceSuitable 中被调用,实现一个附件的检查。

C++ 复制代码
bool isDeviceSuitable(VkPhysicalDevice device) {
    QueueFamilyIndices indices = findQueueFamilies(device);

    bool extensionsSupported = checkDeviceExtensionSupport(device);
    return indices.isComplete() && extensionsSupported;
}

bool checkDeviceExtensionSupport(VkPhysicalDevice device) {
    return true;
}

修改函数体,列出所有的设备扩展,检查请求的这个扩展是否在列表中。

C++ 复制代码
bool checkDeviceExtensionSupport(VkPhysicalDevice device){
    uint32_t extensionCount;
    vkEnumerateDeviceExtensionProperties(device, nullptr, &extensionCount, nullptr);

    std::vector<VkExtensionProperties> availableExtensions(extensionCount);
    vkEnumerateDeviceExtensionProperties(device, nullptr, &extensionCount, availableExtensions.data());

    std::set<std::string> requiredExtensions(deviceExtensions.begin(), deviceExtensions.end());

    for (const auto& extension : availableExtensions) {
        requiredExtensions.erase(extension.extensionName);
    }

    return requiredExtensions.empty();
}

我们这里选择一个字符串集去代表要请求的还未被确认的扩展,它可以很容易地当被列出可用扩展后被剔除掉,当然也可以像 checkValidationLayerSupport 里面一样,使用双层循环来判断,在性能上并没有多大差异,现在可以运行你的代码,去验证你显卡的确可以创建交换链。需要注意,其实我们在前一章,已经检查了呈现队列可用,这会隐式的指出创建交换链的扩展也是可用的,然而显式地检测扩展是否支持,一直是个好地习惯。

打开设备扩展

创建交换链,首先需要打开 VK_KHR_swapchain ,这仅需要在逻辑设备创建时,做一个小的修改

C++ 复制代码
createInfo.enabledExtensionCount = static_cast<uint32_t>(deviceExtensions.size());
createInfo.ppEnabledExtensionNames = deviceExtensions.data();

确保替换之前的 createInfo.enabledExtensionCount = 0;.

查询支持的交换链细节信息

只检查交换链是否可用还不够,也许它并不能真正和窗口兼容。创建交换链涉及到的东西也会比 instance 和 device 更多,所以在创建之前,我们还需要查询更多的细节数据。

我们需要检测基础的三种属性:

  • 交换链的基础能力 (最大/最小的交换链图像数量, 图像的最大/最小宽高)
  • 表面格式 (像素格式,颜色空间)
  • 可用的呈现模式

与 findQueueFamilies 类似,我们需要一个结构体去接收这些查询到的信息,在结构体里声明出上面提到的三个属性类型。

C++ 复制代码
struct SwapChainSupportDetails{
    VkSurfaceCapabilitiesKHR capabilities;
    std::vector<VkSurfaceFormatKHR> formats;
    std::vector<VkPresentModeKHR> presentModes;
};

创建新的函数 querySwapChainSupport,用于填充这个查询的结构体.

C++ 复制代码
SwapChainSupportDetails querySwapChainSupport(){
    SwapChainSupportDetails details;

    return details;
}

本章会介绍如何查询这些细节数据,这些结构体包含的数据的含义,会在下一章说明。

首先,我们来看基础的surface能力查询,这些属性很容易查询到,并通过一个 VkSurfaceCapabilitiesKHR 结构体返回

C++ 复制代码
vkGetPhysicalDeviceSurfaceCapabilitiesKHR(device, surface, &details.capabilities);

这个函数传入物理设备和表面,以查询表面支持的能力.所有的查询函数都需要这两个参数,因为它们是交换链的核心组件。

下一步是去查询表面支持的格式,因为查询到的是一个结构体列表,我们再次采用经典的两次调用方式

C++ 复制代码
uint32_t formatCount;
vkGetPhysicalDeviceSurfaceFormatKHR(device , surface , &formatCount , nullptr);

if(formatCount != 0){
    details.formats.resize(formatCount);
    vkGetPhysicalDeviceSurfaceFormatKHR(device , surface, &formatCount, details.formats.data());
}

确保向量容器大小适配返回的格式数量。最后使用函数 vkGetPhysicalDeviceSurfacePresentModesKHR 查询支持的呈现模式,调用方式与之前类似。

C++ 复制代码
uint32_t presentModeCount;
vkGetPhysicalDeviceSurfacePresentModesKHR(device, surface, &presentModeCount, nullptr);

if (presentModeCount != 0) {
    details.presentModes.resize(presentModeCount);
    vkGetPhysicalDeviceSurfacePresentModesKHR(device, surface, &presentModeCount, details.presentModes.data());
}

所有的细节信息,现在都在结构体了,再次去修改 isDeviceSuitable 函数,检查交换链是否满足应用程序的需求。本教程程序的需求是,至少一个支持的图像格式,且窗口表面支持一个呈现模式。

C++ 复制代码
bool isSwapChainAdequate = false;
if(extensionsSupported){
    SwapChainSupportDetails swapChainSupport = querySwapChainSupport(device);
    swapChainAdequate = !swapChainSupport.formats.empty() && !swapChainSupport.presentModes.empty();
}  

我只会在验证了扩展可用的前提下,去查询交换链的支持情况,这一点很重要。将最后一行的函数改成

C++ 复制代码
return indices.isComplete() && extensionSupported &&  isSwapChainAdequate;

选择正确的交换链设置

尽管 swapChainAdequate 满足需求返回true了,仍然有许多可选的所谓最优模式。我们会去写一些函数,以找到交换链最好的设置配置,有三种类型的设置

  • 表面的格式 (颜色 深度)
  • 呈现模式 (图像被置换到屏幕上的条件是什么?)
  • 交换链的尺寸大小(交换链图像的分辨率)

对于其上每一个设置,我们都有一个最优值,如果有直接使用最优值,如果没有,我们会写一些逻辑找到最适配的一个。

表面格式

创建函数 chooseSwapSurfaceFormat,讲查询到格式列表传入

C++ 复制代码
VkSurfaceFormatKHR chooseSwapChainFormat(const std::vector<VkSurfaceFormatKHR> &aavailableFormats){

}

每一个VkSurfaceFormatKHR 实体都有一个 format 和一个 colorSpace 成员,format 对象显示出了颜色的通道和种类,例如VK_FORMAT_B8G8R8A8_SRGB 表示像素点有B,G,R和ALPHA通道,每一个都是8位的无符号整型,共同组成了一个32位的整型数据. colorSpace 表示色彩空间,例如它可以通过 VK_COLOR_SPACE_SRGB_NONLINEAR_KHR 指示RGB的颜色是否是标准色彩空间,带非线性 gamma 校正。

我们使用sRGB的色彩空间,这是在显示和打印中被广泛支持的一个设置,例如后面要介绍的纹理。VK_FORMAT_B8G8R8A8_SRGB 是最常用的颜色表示。

现在我们遍历列表查询首选的组合是否可用

C++ 复制代码
for (const auto& availableFormat : availableFormats) {
    if (availableFormat.format == VK_FORMAT_B8G8R8A8_SRGB && availableFormat.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR) {
        return availableFormat;
    }
}

如果没有找到最优值,我们也可以对候选项做排序,不过一般情况下,都是以列表的第一个格式来返回

C++ 复制代码
VkSurfaceFormatKHR chooseSwapSurfaceFormat(const std::vector<VkSurfaceFormatKHR>& availableFormats) {
    for (const auto& availableFormat : availableFormats) {
        if (availableFormat.format == VK_FORMAT_B8G8R8A8_SRGB && availableFormat.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR) {
            return availableFormat;
        }
    }

    return availableFormats[0];
}
呈现模式

呈现模式可以说是交换链最重要的设置,因为它代表着显示图像到屏幕上需要满足的条件,Vulkan中有四个可选值:

  • VK_PRESENT_MODE_IMMEDIDATE_KHR : 应用提交的图像会被立即传输给屏幕,也许会导致画面撕裂.

  • VK_PRESENT_MODE_FIFO_KHR : 显示器会从交换链组织的一个队列的顶部取图像显示,应用程序会让渲染好的一帧图像插入到此队列的末尾,如果队列已满,程序就需要等待,这与现代游戏中的垂直同步是最相似的。显示器的刷新被称作"垂直空白".

  • VK_PRESENT_MODE_FIFO_RELAXED_KHR : 它与上面FIFO模式唯一的不同是,当程序的渲染速度不够,当队列为空时,会切为画面立即显示到屏幕的方式,此模式也会导致画面撕裂

  • VK_PRESENT_MODE_MAILBOX_KHR : 这是FIFO模式的第二个变种,代替队列满时阻塞应用程序,在队列中的图像会被更新的图像取代掉,这个模式可以尽可能快速的渲染帧图像,比起垂直同步有更小的延迟。通常也会称其为"三缓冲",尽管这单独的第三个缓冲,在显示帧率被锁的情况下是没有必要的。

只有 VK_PRESENT_MODE_FIFO_KHR 模式是保证必须可用的,所以我们再次用相似的方法实现最优呈现模式的选择。

c++ 复制代码
VkPresentModeKHR chooseSwapPresentMode(const std::vector<VkPresentModeKHR> &availablePresentModes){
    return VK_PRESENT_MODE_FIFO_KHR;
}

个人认为,如果不考虑能耗,VK_PRESENT_MODE_MAILBOX_KHR是一个比较好的选择,通过在垂直同步信号到来时,用更新的图像替换到之前的图像的方法,它既可以避免画面撕裂,也有一个很低的延时,再移动设备上,续航能力更加重要,可以考虑使用 VK_PRESENT_MODE_FIFO_KHR 模式.先在列表中查询是否有 VK_PRESENT_MODE_MAILBOX_KHR.

C++ 复制代码
VkPresentModeKHR chooseSwapPresentMode(const std::vector<VkPresentModeKHR>& availablePresentModes) {
    for (const auto& availablePresentMode : availablePresentModes) {
        if (availablePresentMode == VK_PRESENT_MODE_MAILBOX_KHR) {
            return availablePresentMode;
        }
    }

    return VK_PRESENT_MODE_FIFO_KHR;
}
交换链的尺寸

还剩下一个主要的属性,先添加一个选择函数

C++ 复制代码
VkExtent2D chooseSwapExtent(const VkSurfaceCapabilitiesKHR &capabilities){

}

交换链的尺寸指的是交换链图像的分辨率,它几乎总是等于窗口的分辨率(以像素为单位),可能的分辨率的范围被包含在结构体 VkSurfaceCapabilitiesKHR 中,Vulkan告诉我们通常在currentExtent 成员变量中设置窗口的分辨率,然而有些窗口管理系统却不一样,它在 currentExtent 里设置一个特殊的值(uint32_t类型的最大值)来标识这个情况,这种情况下,我们需要指定一个匹配窗口的大小,此值需要设定在 minImageExtent 与 maxImageExtent 之间。但是我们必须以正确的单位指定分辨率。

GLFW库使用了两种单位去测量尺寸,像素 和 屏幕坐标系,例如,我们创建窗口时所指定的窗口大小就是基于屏幕坐标系来指定的,可是,Vulkan是以像素为单位的,所以交换链的尺寸也必须以像素为单位来指定。不幸的是,如果你使用的是一个高分辨率的显示设备(如苹果的 retina 设备),屏幕坐标系的值就不能被自动纠正为像素大小,相反的,由于更高的屏幕像素解析度,窗口的实际像素大小值要大于坐标系统给定的值。所以,如果Vulkan并没有修复这个问题,我们需要自己给出原始的宽高.可以使用 glfwGetFramebufferSize 函数去查询实际窗口的像素大小值,然后找到匹配最大最小值的设定值。

C++ 复制代码
#include <cstdint> // Necessary for uint32_t
#include <limits> // Necessary for std::numeric_limits
#include <algorithm> // Necessary for std::clamp

...

VkExtent2D chooseSwapExtent(const VkSurfaceCapabilitiesKHR& capabilities) {
    if (capabilities.currentExtent.width != std::numeric_limits<uint32_t>::max()) {
        return capabilities.currentExtent;
    } else {
        int width, height;
        glfwGetFramebufferSize(window, &width, &height);

        VkExtent2D actualExtent = {
            static_cast<uint32_t>(width),
            static_cast<uint32_t>(height)
        };

        actualExtent.width = std::clamp(actualExtent.width, capabilities.minImageExtent.width, capabilities.maxImageExtent.width);
        actualExtent.height = std::clamp(actualExtent.height, capabilities.minImageExtent.height, capabilities.maxImageExtent.height);

        return actualExtent;
    }
}

clamp函数用于将值设定在 最大值与最小值之间,以此适配查询到的合适尺寸。

创建交换链

现在已经有了辅助查询的所有函数,我们可以利用这些查询到的信息去创建 swapChain 了.

新建一个 createSwapChain 函数,在initVulkan中 创建逻辑设备函数之调用它。

C++ 复制代码
void initVulkan() {
    createInstance();
    setupDebugMessenger();
    createSurface();
    pickPhysicalDevice();
    createLogicalDevice();
    createSwapChain();
}

void createSwapChain(){
    SwapChainSupportDetails swapChainSupport = querySwapChainSupport(physicalDevice);

    VkSurfaceFormatKHR surfaceFormat = chooseSwapSurfaceFormat(swapChainSupport.formats);
    VkPresentModeKHR presentMode = chooseSwapPresentMode(swapChainSupport.presentModes);
    VkExtent2D extent = chooseSwapExtent(swapChainSupport.capabilities);
}

除了以上的几个属性,我们还需要决定,想让交换链持有多少张图像,这个最小可以通过如下代码获得

C++ 复制代码
uint32_t imageCount = swapChainSupport.capabilities.minImageCount;

然而,简单的设置最小值,有时会出现,在我们可以获取另外一张可以渲染的图像前,却不得不等待驱动完成一些额外的工作。因此更推荐设置一个比最小值稍大的数量。

C++ 复制代码
uint32_t imageCount = swapChainSupport.capabilities.minImageCount + 1;

我们同时也需要确保这个值不能大于最大值,如果最大值是0,就说明没有限制。

C++ 复制代码
if (swapChainSupport.capabilities.maxImageCount > 0 && imageCount > swapChainSupport.capabilities.maxImageCount) {
    imageCount = swapChainSupport.capabilities.maxImageCount;
}

与之前创建Vulkan对象一样,创建交换链,要填写一个很大的结构体。

C++ 复制代码
VkSwapChainCreateInfoKHR createInfo{};
createInfo.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR;
createInfo.surface = surface;

指定完交换链的表面后,细节信息就可以继续被设置了

C++ 复制代码
createInfo.minImageCount = imageCount;
createInfo.imageFormat = surfaceFormat.format;
createInfo.imageColorSpace = surfaceFormat.colorSpace;
createInfo.imageExtent = extent;
createInfo.imageArrayLayers = 1;
createInfo.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;

imageArrayLayers 指定交换链的图像有多少层,除非你开发的VR 3D应用,否则这个值都是1. imageUsage 表示我们想让交换链中的图像使用哪一种操作,在此教程中,我们是想直接渲染到这个图像上,这便意味着我们将它们用作颜色附件。也有可能,你希望渲染到一个单独的图像中,以完成一些后处理的操作,这种情况下,需要修改设置为 VK_IMAGE_USAGE_TRANSFER_DST_BIT 来代替,并使用一个内存传输的操作将渲染出的图像传送给交换链的图像。

C++ 复制代码
QueueFamilyIndices indices = findQueueFamilies(physicalDevice);

uint32_t queueFamilyIndices[] = {indices.graphicsFamily.value(), indices.presentFamily.value()};

if (indices.graphicsFamily != indices.presentFamily) {
    createInfo.imageSharingMode = VK_SHARING_MODE_CONCURRENT;
    createInfo.queueFamilyIndexCount = 2;
    createInfo.pQueueFamilyIndices = queueFamilyIndices;
} else {
    createInfo.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE;
    createInfo.queueFamilyIndexCount = 0; // Optional
    createInfo.pQueueFamilyIndices = nullptr; // Optional
}

下一步,我们需要处理,交换链的图像被多个队列使用的场景,这种情况出现在呈现队列与图像队列是不同队列的情况下,我们会在图形队列中绘制出图像,再让图像提交给呈现队列。有两种方式去处理图像被多个队列访问的场景

  • VK_SHARING_MODE_EXCLUSIVE : 一张图像一次只能被一个队列所使用,在其他队列要使用时,需要显式地转移所有权。这个选项提供了最佳的性能。

  • VK_SHARING_MODE_CONCURRENT : 图像可以被跨队列使用,不用显式地转移所有权。

如果队列簇是不同的,我们就使用并发模式,来避免显式地操作所有权转移。并发模式需要你使用 queueFamilyIndexCount 和 pQueueFamilyIndices 参数来指明,需要在哪几个队列簇中共享图像.大多数硬件环境中,都是图形队列簇和呈现队列簇是一样的,我们就固定使用排他模式,因为并发模式需要你指定两个不同的队列簇。

C++ 复制代码
createInfo.preTransform = swapChainSupport.capabilities.currentTransform;

我们可以指定一个确定的转换应用到交换链中,前提是它是支持的,例如逆时针90旋转或者水平翻转。如果不想应用任何转换,最简单的方式是设置为 currentTransform.

C++ 复制代码
createInfo.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR;

compositeAlpha 字段设置,是否与其他窗口进行透明度的混合。几乎总是会最简单的忽视掉透明度混合,因此使用 VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR

C++ 复制代码
createInfo.presentMode = presentMode;
createInfo.clipped = VK_TRUE;

presentMode 前面已经介绍过了。clipped 成员被设置为 VK_TRUE,表示我们并不在意那些被遮挡住的像素点,例如另外一个窗口在你的应用之上,除非你的确需要读取这些像素获得一个可预测的结果,打开裁剪,可获得最好的性能.

C++ 复制代码
createInfo.oldSwapchain = VK_NULL_HANDLE;

还剩下最后一个oldSwapchain,当你的程序运行时,在Vulkan中是有可能swapchain会失效的,例如,窗口的大小改变了,这种情况下,交换链需要被重建,这个字段就需要保存老的交换链,我们会在后面讨论这个复杂的主题,现在先假定,只会使用创建出的这个交换链。

现在,新增一个类成员VkSwapchainKHR,去存储交换链对象

C++ 复制代码
VkSwapChainKHR swapchain;

调用 vkCreateSwapchainKHR 完成创建

C++ 复制代码
if(vkCreateSwapchainKHR(device, &createInfo, nullptr, &swapchain) != VK_SUCCESS){
    throw std::runtime_error("failed to create swap chain!");
}

参数是逻辑设备,创建信息,可选的分配器,和一个返回创建交换链对象的指针。毫无疑问的,也需要使用 vkDestroySwapchainKHR 在逻辑设备销毁前被清理掉

C++ 复制代码
void cleanup() {
    vkDestroySwapchainKHR(device, swapChain, nullptr);
    ...
}

现在运行程序,确保交换链被成功创建。试着移除 createInfo.imageExtent = extent; 这一行代码,并打开验证层,你将会看到验证层会立即捕获这个错误,并给出有用的信息。

接收交换链的图像

现在交换链已经被创建了,接下来的事便是接收里面包含的 VkImages ,我们将会在后面章节的渲染操作中,引用到它们。增加一个类成员,存储这些图像的句柄。

C++ 复制代码
std::vector<VkImage> swapChainImages;

交换链在创建时,这些图像也被一起创建了出来,因此在交换链被销毁时,它们也会一起被销毁掉,不用在cleanup中再添加清理的代码了。

在 createSwapChain 函数的末尾,添加接收图像的代码.接收它们的操作与之前接收Vulkan数组对象是十分相似的,记住,我们仅指定了交换链中最少的图像数量,所以真正的实现可能有更多数量的图像,所以我们首先要通过 vkGetSwapchainImagesKHR 查询一下实际的数量,然后调整接收容器的大小,最后再次调用 vkGetSwapchainImagesKHR 接收句柄。

C++ 复制代码
vkGetSwapchainImagesKHR(device, swapChain, &imageCount, nullptr);
swapChainImages.resize(imageCount);
vkGetSwapchainImagesKHR(device, swapChain, &imageCount, swapChainImages.data());

最后一件事,我们需要在成员变量中存储为交换链选择的格式和尺寸,会在后面的章节中用到它们。

C++ 复制代码
VkSwapchainKHR swapChain;
std::vector<VkImage> swapChainImages;
VkFormat swapChainImageFormat;
VkExtent2D swapChainExtent;

...

swapChainImageFormat = surfaceFormat.format;
swapChainExtent = extent;

现在我们拥有了一组图像,它们可以被用于渲染,可以被显示在窗口上。下一章,我们会将这些图像设置为渲染目标,然后开始学习渲染管线和绘制命令。

图像视图

在渲染管线中使用任何交换链提供的 VkImage对象,都需要创建 VkImageView 对象,其描述了怎样去访问图像,访问图像的哪一块数据,例如它是否需要被当成一个没有多层级的2D深度纹理。

本章,我们会创建一个 createImageView 函数,它为每一个VkImage创建一个 VkImagerView,以便于后面用它作为颜色输出的接收容器。

首先群成员中增加一个vector对象去接收VkImageView

C++ 复制代码
std::vector<VkImageView> swapChainImageViews;

在swapchainCreate 函数调用后,调用新建的 createImageViews

C++ 复制代码
void initVulkan() {
    createInstance();
    setupDebugMessenger();
    createSurface();
    pickPhysicalDevice();
    createLogicalDevice();
    createSwapChain();
    createImageViews();
}

void createImageViews() {

}

首先重置一下容器的大小,适配交换链提供的图像的数量。

C++ 复制代码
void createImageViews(){
    swapChainImageViews.resize(swapChainImages.size());
}

下一步,用一个循环去迭代所有在交换链中的图像

C++ 复制代码
for(size_t i = 0; i < swapChainImages.size() ;i++){

}//end for i

创建ImageView需要指定 VkImageViewCreateInfo 结构体,首先设置意义很明显的两个参数

C++ 复制代码
VkImageViewCreateInfo createInfo{};
createInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
createInfo.image = swapChainImages[i];

viewType 和 format 参数指定了图像数据应该如何被解释,viewType参数允许你设置图像为 1D纹理 2D纹理 3D纹理 或者是立方体纹理。

C++ 复制代码
createInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
createInfo.format = swapChainImageFormat;

components 字段允许你自行调整颜色通道,举个例子,你可以映射所有的通道到颜色通道中,以实现一个单色纹理,或者,可以映射一个0和1的常量值到一个通道中,这里我们坚持使用默认设置。

C++ 复制代码
createInfo.components.r = VK_COMPONENT_SWIZZLE_IDENTITY;
createInfo.components.g = VK_COMPONENT_SWIZZLE_IDENTITY;
createInfo.components.b = VK_COMPONENT_SWIZZLE_IDENTITY;
createInfo.components.a = VK_COMPONENT_SWIZZLE_IDENTITY;

subresourceRange 字段描述了图像的意图,以及图像的哪一部分可以被访问到,我们的图像将会被用于显示颜色,不会做多级纹理或多层级的设置。

C++ 复制代码
createInfo.suresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
createInfo.suresourceRange.baseMipLevel = 0;
createInfo.suresourceRange.levelCount = 1;
createInfo.suresourceRange.baseArrayLayer = 0;
createInfo.suresourceRange.layerCount =1;

如果你正在开发的是立体3D应用(VR,裸眼3D...),就需要在创建交换链的时候指定多个层级,然后就可以创建多个图像视图,为左眼和右眼使用不同的层级。

现在就剩下调用 vkCreateImageView 去创建出 ImageView了

C++ 复制代码
if(vkCreateImageView(device , &createInfo , nullptr, &swapChainImages[i]) != VK_SUCCESS){
    throw std::runtime_error("failed to create image views!");
}

与图像不同,图像视图是我们显式创建出来的,所以在程序结束时,需要用一个类似的循环将其清理掉

C++ 复制代码
void cleanup(){
    for (auto imageView : swapChainImageViews) {
        vkDestroyImageView(device, imageView, nullptr);
    }

    ...
}

创建一个图像视图,是将图像作为纹理使用的前提,可是,现在还不能将它设为最终的渲染目标,还需要一个间接关联的 FrameBuffer 帧缓冲对象。下一章,我们将会开始设置图形管线。

相关推荐
crary,记忆1 小时前
Angular微前端架构:Module Federation + ngx-build-plus (Webpack)
前端·webpack·angular·angular.js
漂流瓶jz1 小时前
让数据"流动"起来!Node.js实现流式渲染/流式传输与背后的HTTP原理
前端·javascript·node.js
SamHou02 小时前
手把手 CSS 盒子模型——从零开始的奶奶级 Web 开发教程2
前端·css·web
我不吃饼干2 小时前
从 Vue3 源码中了解你所不知道的 never
前端·typescript
开航母的李大2 小时前
【中间件】Web服务、消息队列、缓存与微服务治理:Nginx、Kafka、Redis、Nacos 详解
前端·redis·nginx·缓存·微服务·kafka
Bruk.Liu2 小时前
《Minio 分片上传实现(基于Spring Boot)》
前端·spring boot·minio
鱼樱前端2 小时前
Vue3+d3-cloud+d3-scale+d3-scale-chromatic实现词云组件
前端·javascript·vue.js
zhangxingchao3 小时前
Flutter入门:Flutter开发必备Dart基础
前端
佚名猫3 小时前
vue3+vite+pnpm项目 使用monaco-editor常见问题
前端·vue3·vite·monacoeditor
满分观测网友z3 小时前
vue的<router-link>的to里面的query和params的区别
前端·javascript·vue.js