1. 简介
为什么要在游戏中使用 Vulkan?
Vulkan 是 Android 上的主要底层图形 API。对于实现自己的游戏引擎和渲染程序的游戏,Vulkan 可以为其实现更高的性能。
Vulkan 从 Android 7.0(API 级别 24)起在 Android 上提供。从 Android 10.0 开始,新 64 位 Android 设备必须支持 Vulkan 1.1。2022 年 Android 基准配置文件也将 Vulkan API 的最低版本设置为 1.1。
由于在 OpenGL ES 中进行绘制调用的成本较高,因此进行大量绘制调用并使用 OpenGL ES 的游戏可能会产生高昂的驱动程序开销。这些游戏可能会因在图形驱动程序中花费大量帧时间而受到 CPU 限制。通过从 OpenGL ES 切换到 Vulkan,游戏还可以显著降低 CPU 用量和功耗。如果您的游戏场景较为复杂,导致无法通过有效利用实例化来减少绘制调用次数,那么这一点尤其适用。
构建内容
在此 Codelab 中,您将使用一个基本的 C++ Android 应用,并添加代码来设置 Vulkan 渲染流水线。然后,您将实现使用 Vulkan 在屏幕上渲染带纹理的旋转三角形的代码。
所需条件
- Android Studio Iguana 或更高版本。
- 一部搭载 Android 10.0 或更高版本的 Android 设备,该设备已连接到计算机,且已启用开发者选项和 USB 调试功能。
2. 准备工作
设置开发环境
如果您之前从未在 Android Studio 中使用过原生项目,则可能需要安装 Android NDK 和 CMake。如果您已安装这些工具,请继续设置项目。
确认是否已安装相关 SDK、NDK 和 CMake
启动 Android Studio。当系统显示“Welcome to Android Studio”窗口时,打开“Configure”下拉菜单,然后选择“SDK Manager”选项。
如果您已打开现有项目,则可以通过“Tools”菜单打开 SDK 管理器。点击 Tools 菜单,然后选择 SDK Manager,即可打开 SDK 管理器窗口。
在边栏中依次选择:Appearance & Behavior > System Settings > Android SDK。在 Android SDK 窗格中选择 SDK Platforms 标签页,即可显示已安装工具选项的列表。确保已安装 Android SDK 12.0 或更高版本。
接下来,选择 SDK Tools 标签页,并确保已安装 NDK 和 CMake。
注意:我们对版本没有严格要求,只要使用较新的版本即可,但我们目前使用的是 NDK 26.1.10909125 和 CMake 3.22.1。默认安装的 NDK 版本将随日后陆续推出的 NDK 版本而更改。如果您需要安装特定版本的 NDK,请按照 Android Studio 参考文档中安装特定版本的 NDK 部分中的安装 NDK 相关说明操作。
勾选所需的所有工具后,点击窗口底部的 Apply 按钮进行安装。然后,可以点击 OK 按钮关闭 Android SDK 窗口。
设置项目
我们已在 Git 仓库中为您设置了从 C++ 模板派生的起始项目。该起始项目实现了应用初始化和事件处理,但尚未进行任何图形设置或渲染。
克隆仓库
在命令行中,切换到您希望包含游戏根目录的目录,然后从 GitHub 克隆该目录:
git clone -b codelab/start https://github.com/android/getting-started-with-vulkan-on-android-codelab.git --recurse-submodules
确保从名为 [codelab] start: empty app
的仓库的初始提交内容开始。
使用 Android Studio 打开并构建项目,然后在连接的设备上运行该项目。该项目启动后会显示一个无内容的黑屏,供您添加在后面几部分渲染的图形。
3. 创建 Vulkan 实例和设备
若要初始化 Vulkan API 以供使用,第一步就是创建 Vulkan 实例对象 (VkInstance)。
VkInstance 对象代表应用的 Vulkan 运行时实例。它是 Vulkan API 的根对象,用于检索相关信息,以及实例化 Vulkan 设备对象和任何要激活的层。
当应用创建 VkInstance 时,必须提供其自身的相关信息,如名称、版本和所需的 Vulkan 实例扩展。
Vulkan API 设计包含一个提供某种机制的层系统,该机制可在 API 调用到达 GPU 驱动程序之前拦截和处理这些调用。在创建 VkInstance 时,应用可以指定要激活的层。最常用的层是 Vulkan 验证层,它能够对 API 使用情况进行运行时分析,以发现错误或次优的性能做法。
创建 VkInstance 后,应用可以使用它查询系统中可用的物理设备、创建逻辑设备,并创建渲染目标 Surface。
VkInstance 通常在应用启动时创建一次,并在结束时销毁。不过,也可以在同一个应用中创建多个 VkInstance,例如,应用需要使用多个 GPU 或创建多个窗口。
// CODELAB: hellovk.h
void HelloVK::createInstance() {
VkApplicationInfo appInfo{};
appInfo.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO;
appInfo.pApplicationName = "Hello Triangle";
appInfo.applicationVersion = VK_MAKE_VERSION(1, 0, 0);
appInfo.pEngineName = "No Engine";
appInfo.engineVersion = VK_MAKE_VERSION(1, 0, 0);
appInfo.apiVersion = VK_API_VERSION_1_0;
VkInstanceCreateInfo createInfo{};
createInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
createInfo.pApplicationInfo = &appInfo;
createInfo.enabledExtensionCount = (uint32_t)requiredExtensions.size();
createInfo.ppEnabledExtensionNames = requiredExtensions.data();
createInfo.pApplicationInfo = &appInfo;
createInfo.enabledLayerCount = 0;
createInfo.pNext = nullptr;
VK_CHECK(vkCreateInstance(&createInfo, nullptr, &instance));
}
}
VkPhysicalDevice 是一个 Vulkan 对象,代表系统上的物理 Vulkan 设备。大多数 Android 设备只会返回一个代表 GPU 的 VkPhysicalDevice。但是,PC 或 Android 设备可以枚举多个实体设备。例如,一台计算机既包含独立的 GPU,又包含集成 GPU。
您可以查询 VkPhysicalDevice 的属性,例如名称、供应商、驱动程序版本和支持的功能。此信息可用于为特定应用选择最佳实体设备。
选择 VkPhysicalDevice 后,应用便可据此创建逻辑设备。逻辑设备代表特定于应用的物理设备。它具有自己的状态和资源,并且独立于可能基于同一物理设备创建的其他逻辑设备。
不同的队列系列会产生不同类型的队列,而每个队列系列只允许使用特定子集的命令。例如,可能存在一个仅允许处理计算命令的队列系列,也可能存在一个仅允许处理与内存传输相关命令的队列系列。
VkPhysicalDevice 可以枚举所有可用的队列系列类型。这里我们只对图形队列感兴趣,不过,可能还存在仅支持 COMPUTE
或 TRANSFER
的其他队列。队列系列没有自己的类型,而是通过其父对象 (VkPhysicalDevice) 内的数字索引类型 uint32_t 来表示。
VkPhysicalDevice 自身可以创建多个逻辑设备。这对于需要使用多个 GPU 或创建多个窗口的应用非常有用。
VkDevice 是一个 Vulkan 对象,代表一个逻辑 Vulkan 设备。它是实体设备之上的轻量级抽象化,提供了创建和管理 Vulkan 资源所需的所有功能,例如缓冲区、图像和着色器。
VkDevice 通过 VkPhysicalDevice 创建,并且特定于创建它的应用。它具有自己的状态和资源,并且独立于可能基于同一物理设备创建的其他逻辑设备。
一个 VkSurfaceKHR 对象代表一个可作为渲染操作目标的 Surface。如需在设备屏幕上显示图形,您需要使用对应用窗口对象的引用来创建 Surface。创建 VkSurfaceKHR 对象后,应用可以使用该对象来创建 VkSwapchainKHR 对象。
VkSwapchainKHR 对象代表一种基础架构,该基础架构拥有我们能在屏幕上可视化之前要渲染到的缓冲区。它本质上是一个等待呈现在屏幕上的图像队列。我们将获取此类图像以对其进行绘制,然后将其返回队列。队列的具体工作方式以及从队列中呈现图像的条件取决于交换链的设置方式,但交换链的一般用途是使图像的呈现与屏幕的刷新率同步。
// CODELAB: hellovk.h - Data Types
struct QueueFamilyIndices {
std::optional<uint32_t> graphicsFamily;
std::optional<uint32_t> presentFamily;
bool isComplete() {
return graphicsFamily.has_value() && presentFamily.has_value();
}
};
struct SwapChainSupportDetails {
VkSurfaceCapabilitiesKHR capabilities;
std::vector<VkSurfaceFormatKHR> formats;
std::vector<VkPresentModeKHR> presentModes;
};
struct ANativeWindowDeleter {
void operator()(ANativeWindow *window) { ANativeWindow_release(window); }
};
如果您需要调试应用,可以设置验证层支持。您还可以查看游戏可能需要的特定扩展。
// CODELAB: hellovk.h
bool HelloVK::checkValidationLayerSupport() {
uint32_t layerCount;
vkEnumerateInstanceLayerProperties(&layerCount, nullptr);
std::vector<VkLayerProperties> availableLayers(layerCount);
vkEnumerateInstanceLayerProperties(&layerCount, availableLayers.data());
for (const char *layerName : validationLayers) {
bool layerFound = false;
for (const auto &layerProperties : availableLayers) {
if (strcmp(layerName, layerProperties.layerName) == 0) {
layerFound = true;
break;
}
}
if (!layerFound) {
return false;
}
}
return true;
}
std::vector<const char *> HelloVK::getRequiredExtensions(
bool enableValidationLayers) {
std::vector<const char *> extensions;
extensions.push_back("VK_KHR_surface");
extensions.push_back("VK_KHR_android_surface");
if (enableValidationLayers) {
extensions.push_back(VK_EXT_DEBUG_UTILS_EXTENSION_NAME);
}
return extensions;
}
找到合适的设置并创建 VkInstance 后,请创建代表渲染目标窗口的 VkSurface。
// CODELAB: hellovk.h
void HelloVK::createSurface() {
assert(window != nullptr); // window not initialized
const VkAndroidSurfaceCreateInfoKHR create_info{
.sType = VK_STRUCTURE_TYPE_ANDROID_SURFACE_CREATE_INFO_KHR,
.pNext = nullptr,
.flags = 0,
.window = window.get()};
VK_CHECK(vkCreateAndroidSurfaceKHR(instance, &create_info,
nullptr /* pAllocator */, &surface));
}
枚举可用的实体设备 (GPU),并选择第一个合适的可用设备。
// CODELAB: hellovk.h
void HelloVK::pickPhysicalDevice() {
uint32_t deviceCount = 0;
vkEnumeratePhysicalDevices(instance, &deviceCount, nullptr);
assert(deviceCount > 0); // failed to find GPUs with Vulkan support!
std::vector<VkPhysicalDevice> devices(deviceCount);
vkEnumeratePhysicalDevices(instance, &deviceCount, devices.data());
for (const auto &device : devices) {
if (isDeviceSuitable(device)) {
physicalDevice = device;
break;
}
}
assert(physicalDevice != VK_NULL_HANDLE); // failed to find a suitable GPU!
}
如需检查设备是否合适,我们需要找到一个支持 GRAPHICS
队列的设备。
// CODELAB: hellovk.h
bool HelloVK::isDeviceSuitable(VkPhysicalDevice device) {
QueueFamilyIndices indices = findQueueFamilies(device);
bool extensionsSupported = checkDeviceExtensionSupport(device);
bool swapChainAdequate = false;
if (extensionsSupported) {
SwapChainSupportDetails swapChainSupport = querySwapChainSupport(device);
swapChainAdequate = !swapChainSupport.formats.empty() &&
!swapChainSupport.presentModes.empty();
}
return indices.isComplete() && extensionsSupported && swapChainAdequate;
}
// CODELAB: hellovk.h
bool HelloVK::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();
}
// CODELAB: hellovk.h
QueueFamilyIndices HelloVK::findQueueFamilies(VkPhysicalDevice device) {
QueueFamilyIndices indices;
uint32_t queueFamilyCount = 0;
vkGetPhysicalDeviceQueueFamilyProperties(device, &queueFamilyCount, nullptr);
std::vector<VkQueueFamilyProperties> queueFamilies(queueFamilyCount);
vkGetPhysicalDeviceQueueFamilyProperties(device, &queueFamilyCount,
queueFamilies.data());
int i = 0;
for (const auto &queueFamily : queueFamilies) {
if (queueFamily.queueFlags & VK_QUEUE_GRAPHICS_BIT) {
indices.graphicsFamily = i;
}
VkBool32 presentSupport = false;
vkGetPhysicalDeviceSurfaceSupportKHR(device, i, surface, &presentSupport);
if (presentSupport) {
indices.presentFamily = i;
}
if (indices.isComplete()) {
break;
}
i++;
}
return indices;
}
一旦确定要使用的 PhysicalDevice 后,就可以创建逻辑设备(称为 VkDevice)了。这代表已初始化的 Vulkan 设备,它已准备好创建应用要使用的所有其他对象。
// CODELAB: hellovk.h
void HelloVK::createLogicalDeviceAndQueue() {
QueueFamilyIndices indices = findQueueFamilies(physicalDevice);
std::vector<VkDeviceQueueCreateInfo> queueCreateInfos;
std::set<uint32_t> uniqueQueueFamilies = {indices.graphicsFamily.value(),
indices.presentFamily.value()};
float queuePriority = 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);
}
VkPhysicalDeviceFeatures deviceFeatures{};
VkDeviceCreateInfo createInfo{};
createInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;
createInfo.queueCreateInfoCount =
static_cast<uint32_t>(queueCreateInfos.size());
createInfo.pQueueCreateInfos = queueCreateInfos.data();
createInfo.pEnabledFeatures = &deviceFeatures;
createInfo.enabledExtensionCount =
static_cast<uint32_t>(deviceExtensions.size());
createInfo.ppEnabledExtensionNames = deviceExtensions.data();
if (enableValidationLayers) {
createInfo.enabledLayerCount =
static_cast<uint32_t>(validationLayers.size());
createInfo.ppEnabledLayerNames = validationLayers.data();
} else {
createInfo.enabledLayerCount = 0;
}
VK_CHECK(vkCreateDevice(physicalDevice, &createInfo, nullptr, &device));
vkGetDeviceQueue(device, indices.graphicsFamily.value(), 0, &graphicsQueue);
vkGetDeviceQueue(device, indices.presentFamily.value(), 0, &presentQueue);
}
在这一步结束时,您只能看到没有渲染任何内容的黑色窗口,这是因为设置流程还未结束。如果出现任何问题,您可以将自己的版本与 [codelab] step: create instance and device
仓库的提交内容进行比较。
4. 创建交换链并同步对象
VkSwapchain 是一个 Vulkan 对象,代表可呈现到显示屏上的一个图像队列。它用于实现双重缓冲或三重缓冲,从而减少画面撕裂并提升性能。
如需创建 VkSwapchain,应用必须先创建 VkSurfaceKHR 对象。在创建实例步骤中设置窗口时,我们已经创建了 VkSurfaceKHR 对象。
VkSwapchainKHR 对象将有许多关联图像。这些图像用于存储渲染的场景。应用可以从 VkSwapchainKHR 对象获取图像,对其进行渲染,然后在显示屏上呈现出来。
图像一旦在显示屏上显示,便无法再供应用使用。应用必须先从 VkSwapchainKHR 对象获取另一幅图像,然后才能再次渲染。
VkSwapchain 通常在应用启动时创建一次,并在结束时销毁。不过,也可以在同一个应用中创建和销毁多个 VkSwapchain,例如,如果应用需要使用多个 GPU 或创建多个窗口。
同步对象是用于同步的对象。Vulkan 具有 VkFence、VkSemaphore 和 VkEvent,用于控制多个队列之间的资源访问权限。如果您使用多个队列和渲染通道,就需要这些对象,但在我们的简单示例中,我们不会使用这些对象。
// CODELAB: hellovk.h
void HelloVK::createSyncObjects() {
imageAvailableSemaphores.resize(MAX_FRAMES_IN_FLIGHT);
renderFinishedSemaphores.resize(MAX_FRAMES_IN_FLIGHT);
inFlightFences.resize(MAX_FRAMES_IN_FLIGHT);
VkSemaphoreCreateInfo semaphoreInfo{};
semaphoreInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO;
VkFenceCreateInfo fenceInfo{};
fenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
fenceInfo.flags = VK_FENCE_CREATE_SIGNALED_BIT;
for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) {
VK_CHECK(vkCreateSemaphore(device, &semaphoreInfo, nullptr,
&imageAvailableSemaphores[i]));
VK_CHECK(vkCreateSemaphore(device, &semaphoreInfo, nullptr,
&renderFinishedSemaphores[i]));
VK_CHECK(vkCreateFence(device, &fenceInfo, nullptr, &inFlightFences[i]));
}
}
// CODELAB: hellovk.h
void HelloVK::createSwapChain() {
SwapChainSupportDetails swapChainSupport =
querySwapChainSupport(physicalDevice);
auto 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];
};
VkSurfaceFormatKHR surfaceFormat =
chooseSwapSurfaceFormat(swapChainSupport.formats);
// Please check
// https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VkPresentModeKHR.html
// for a discourse on different present modes.
//
// VK_PRESENT_MODE_FIFO_KHR = Hard Vsync
// This is always supported on Android phones
VkPresentModeKHR presentMode = VK_PRESENT_MODE_FIFO_KHR;
uint32_t imageCount = swapChainSupport.capabilities.minImageCount + 1;
if (swapChainSupport.capabilities.maxImageCount > 0 &&
imageCount > swapChainSupport.capabilities.maxImageCount) {
imageCount = swapChainSupport.capabilities.maxImageCount;
}
pretransformFlag = swapChainSupport.capabilities.currentTransform;
VkSwapchainCreateInfoKHR createInfo{};
createInfo.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR;
createInfo.surface = surface;
createInfo.minImageCount = imageCount;
createInfo.imageFormat = surfaceFormat.format;
createInfo.imageColorSpace = surfaceFormat.colorSpace;
createInfo.imageExtent = displaySizeIdentity;
createInfo.imageArrayLayers = 1;
createInfo.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;
createInfo.preTransform = pretransformFlag;
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;
createInfo.pQueueFamilyIndices = nullptr;
}
createInfo.compositeAlpha = VK_COMPOSITE_ALPHA_INHERIT_BIT_KHR;
createInfo.presentMode = presentMode;
createInfo.clipped = VK_TRUE;
createInfo.oldSwapchain = VK_NULL_HANDLE;
VK_CHECK(vkCreateSwapchainKHR(device, &createInfo, nullptr, &swapChain));
vkGetSwapchainImagesKHR(device, swapChain, &imageCount, nullptr);
swapChainImages.resize(imageCount);
vkGetSwapchainImagesKHR(device, swapChain, &imageCount,
swapChainImages.data());
swapChainImageFormat = surfaceFormat.format;
swapChainExtent = displaySizeIdentity;
}
// CODELAB: hellovk.h
SwapChainSupportDetails HelloVK::querySwapChainSupport(
VkPhysicalDevice device) {
SwapChainSupportDetails details;
vkGetPhysicalDeviceSurfaceCapabilitiesKHR(device, surface,
&details.capabilities);
uint32_t formatCount;
vkGetPhysicalDeviceSurfaceFormatsKHR(device, surface, &formatCount, nullptr);
if (formatCount != 0) {
details.formats.resize(formatCount);
vkGetPhysicalDeviceSurfaceFormatsKHR(device, surface, &formatCount,
details.formats.data());
}
uint32_t presentModeCount;
vkGetPhysicalDeviceSurfacePresentModesKHR(device, surface, &presentModeCount,
nullptr);
if (presentModeCount != 0) {
details.presentModes.resize(presentModeCount);
vkGetPhysicalDeviceSurfacePresentModesKHR(
device, surface, &presentModeCount, details.presentModes.data());
}
return details;
}
您还需要做好准备,以防在设备丢失上下文后重新创建交换链。例如,当用户切换到其他应用时。
// CODELAB: hellovk.h
void HelloVK::reset(ANativeWindow *newWindow, AAssetManager *newManager) {
window.reset(newWindow);
assetManager = newManager;
if (initialized) {
createSurface();
recreateSwapChain();
}
}
void HelloVK::recreateSwapChain() {
vkDeviceWaitIdle(device);
cleanupSwapChain();
createSwapChain();
}
在这一步结束时,您只能看到没有渲染任何内容的黑色窗口,这是因为设置流程还未结束。如果出现任何问题,您可以将自己的版本与 [codelab] step: create swapchain and sync objects
仓库的提交内容进行比较。
5. 创建渲染通道和帧缓冲区
VkImageView 是一个用于描述如何访问 VkImage 的 Vulkan 对象。它指定了要访问的图像的子资源范围、要使用的像素格式以及要应用于通道的调配。
VkRenderPass 是一个用于描述 GPU 应如何渲染场景的 Vulkan 对象。它指定了将要使用的附件、附件的渲染顺序,以及在渲染流水线各个阶段的使用方式。
VkFramebuffer 是一个 Vulkan 对象,代表一组将在渲染通道执行期间用作附件的图像视图。换言之,它会将实际的图像附件绑定到渲染通道。
// CODELAB: hellovk.h
void HelloVK::createImageViews() {
swapChainImageViews.resize(swapChainImages.size());
for (size_t i = 0; i < swapChainImages.size(); i++) {
VkImageViewCreateInfo createInfo{};
createInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
createInfo.image = swapChainImages[i];
createInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
createInfo.format = swapChainImageFormat;
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;
createInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
createInfo.subresourceRange.baseMipLevel = 0;
createInfo.subresourceRange.levelCount = 1;
createInfo.subresourceRange.baseArrayLayer = 0;
createInfo.subresourceRange.layerCount = 1;
VK_CHECK(vkCreateImageView(device, &createInfo, nullptr,
&swapChainImageViews[i]));
}
}
Vulkan 中的附件通常称为渲染目标,一般是用作渲染输出的图像。此处只需描述格式,例如,渲染通道可能会输出特定颜色格式或深度模板格式。您还需要指定附件的内容在通道开始时是应该保持不变、舍弃,还是清除。
// CODELAB: hellovk.h
void HelloVK::createRenderPass() {
VkAttachmentDescription colorAttachment{};
colorAttachment.format = swapChainImageFormat;
colorAttachment.samples = VK_SAMPLE_COUNT_1_BIT;
colorAttachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
colorAttachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
colorAttachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
colorAttachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
colorAttachment.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
colorAttachment.finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
VkAttachmentReference colorAttachmentRef{};
colorAttachmentRef.attachment = 0;
colorAttachmentRef.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
VkSubpassDescription subpass{};
subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
subpass.colorAttachmentCount = 1;
subpass.pColorAttachments = &colorAttachmentRef;
VkSubpassDependency dependency{};
dependency.srcSubpass = VK_SUBPASS_EXTERNAL;
dependency.dstSubpass = 0;
dependency.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
dependency.srcAccessMask = 0;
dependency.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
dependency.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
VkRenderPassCreateInfo renderPassInfo{};
renderPassInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO;
renderPassInfo.attachmentCount = 1;
renderPassInfo.pAttachments = &colorAttachment;
renderPassInfo.subpassCount = 1;
renderPassInfo.pSubpasses = &subpass;
renderPassInfo.dependencyCount = 1;
renderPassInfo.pDependencies = &dependency;
VK_CHECK(vkCreateRenderPass(device, &renderPassInfo, nullptr, &renderPass));
}
帧缓冲区表示指向可用于附件(渲染目标)的实际图像的链接。您可以指定渲染通道和图像视图集,借此创建帧缓冲区对象。
// CODELAB: hellovk.h
void HelloVK::createFramebuffers() {
swapChainFramebuffers.resize(swapChainImageViews.size());
for (size_t i = 0; i < swapChainImageViews.size(); i++) {
VkImageView attachments[] = {swapChainImageViews[i]};
VkFramebufferCreateInfo framebufferInfo{};
framebufferInfo.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO;
framebufferInfo.renderPass = renderPass;
framebufferInfo.attachmentCount = 1;
framebufferInfo.pAttachments = attachments;
framebufferInfo.width = swapChainExtent.width;
framebufferInfo.height = swapChainExtent.height;
framebufferInfo.layers = 1;
VK_CHECK(vkCreateFramebuffer(device, &framebufferInfo, nullptr,
&swapChainFramebuffers[i]));
}
}
在这一步结束时,您只能看到没有渲染任何内容的黑色窗口,这是因为设置流程还未结束。如果出现任何问题,您可以将自己的版本与 [codelab] step: create renderpass and framebuffer
仓库的提交内容进行比较。
6. 创建着色器和流水线
VkShaderModule 是一个代表可编程着色器的 Vulkan 对象。着色器用于对图形数据执行各种操作,例如转换顶点、给像素着色和计算全局效果。
VkPipeline 是一个 Vulkan 对象,代表可编程的图形流水线。它是一组状态对象,用于描述 GPU 应如何渲染场景。
VkDescriptorSetLayout 是 VkDescriptorSet 的模板,而后者又是一组描述符。描述符是让着色器能够访问资源(例如缓冲区、图像或采样器)的句柄。
// CODELAB: hellovk.h
void HelloVK::createDescriptorSetLayout() {
VkDescriptorSetLayoutBinding uboLayoutBinding{};
uboLayoutBinding.binding = 0;
uboLayoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
uboLayoutBinding.descriptorCount = 1;
uboLayoutBinding.stageFlags = VK_SHADER_STAGE_VERTEX_BIT;
uboLayoutBinding.pImmutableSamplers = nullptr;
VkDescriptorSetLayoutCreateInfo layoutInfo{};
layoutInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;
layoutInfo.bindingCount = 1;
layoutInfo.pBindings = &uboLayoutBinding;
VK_CHECK(vkCreateDescriptorSetLayout(device, &layoutInfo, nullptr,
&descriptorSetLayout));
}
定义 createShaderModule
函数,将着色器加载到 VkShaderModule 对象中
// CODELAB: hellovk.h
VkShaderModule HelloVK::createShaderModule(const std::vector<uint8_t> &code) {
VkShaderModuleCreateInfo createInfo{};
createInfo.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO;
createInfo.codeSize = code.size();
// Satisfies alignment requirements since the allocator
// in vector ensures worst case requirements
createInfo.pCode = reinterpret_cast<const uint32_t *>(code.data());
VkShaderModule shaderModule;
VK_CHECK(vkCreateShaderModule(device, &createInfo, nullptr, &shaderModule));
return shaderModule;
}
创建加载简单顶点和片段着色器的图形流水线。
// CODELAB: hellovk.h
void HelloVK::createGraphicsPipeline() {
auto vertShaderCode =
LoadBinaryFileToVector("shaders/shader.vert.spv", assetManager);
auto fragShaderCode =
LoadBinaryFileToVector("shaders/shader.frag.spv", assetManager);
VkShaderModule vertShaderModule = createShaderModule(vertShaderCode);
VkShaderModule fragShaderModule = createShaderModule(fragShaderCode);
VkPipelineShaderStageCreateInfo vertShaderStageInfo{};
vertShaderStageInfo.sType =
VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
vertShaderStageInfo.stage = VK_SHADER_STAGE_VERTEX_BIT;
vertShaderStageInfo.module = vertShaderModule;
vertShaderStageInfo.pName = "main";
VkPipelineShaderStageCreateInfo fragShaderStageInfo{};
fragShaderStageInfo.sType =
VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
fragShaderStageInfo.stage = VK_SHADER_STAGE_FRAGMENT_BIT;
fragShaderStageInfo.module = fragShaderModule;
fragShaderStageInfo.pName = "main";
VkPipelineShaderStageCreateInfo shaderStages[] = {vertShaderStageInfo,
fragShaderStageInfo};
VkPipelineVertexInputStateCreateInfo vertexInputInfo{};
vertexInputInfo.sType =
VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO;
vertexInputInfo.vertexBindingDescriptionCount = 0;
vertexInputInfo.pVertexBindingDescriptions = nullptr;
vertexInputInfo.vertexAttributeDescriptionCount = 0;
vertexInputInfo.pVertexAttributeDescriptions = nullptr;
VkPipelineInputAssemblyStateCreateInfo inputAssembly{};
inputAssembly.sType =
VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO;
inputAssembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
inputAssembly.primitiveRestartEnable = VK_FALSE;
VkPipelineViewportStateCreateInfo viewportState{};
viewportState.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO;
viewportState.viewportCount = 1;
viewportState.scissorCount = 1;
VkPipelineRasterizationStateCreateInfo rasterizer{};
rasterizer.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO;
rasterizer.depthClampEnable = VK_FALSE;
rasterizer.rasterizerDiscardEnable = VK_FALSE;
rasterizer.polygonMode = VK_POLYGON_MODE_FILL;
rasterizer.lineWidth = 1.0f;
rasterizer.cullMode = VK_CULL_MODE_BACK_BIT;
rasterizer.frontFace = VK_FRONT_FACE_CLOCKWISE;
rasterizer.depthBiasEnable = VK_FALSE;
rasterizer.depthBiasConstantFactor = 0.0f;
rasterizer.depthBiasClamp = 0.0f;
rasterizer.depthBiasSlopeFactor = 0.0f;
VkPipelineMultisampleStateCreateInfo multisampling{};
multisampling.sType =
VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO;
multisampling.sampleShadingEnable = VK_FALSE;
multisampling.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
multisampling.minSampleShading = 1.0f;
multisampling.pSampleMask = nullptr;
multisampling.alphaToCoverageEnable = VK_FALSE;
multisampling.alphaToOneEnable = VK_FALSE;
VkPipelineColorBlendAttachmentState colorBlendAttachment{};
colorBlendAttachment.colorWriteMask =
VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT |
VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT;
colorBlendAttachment.blendEnable = VK_FALSE;
VkPipelineColorBlendStateCreateInfo colorBlending{};
colorBlending.sType =
VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO;
colorBlending.logicOpEnable = VK_FALSE;
colorBlending.logicOp = VK_LOGIC_OP_COPY;
colorBlending.attachmentCount = 1;
colorBlending.pAttachments = &colorBlendAttachment;
colorBlending.blendConstants[0] = 0.0f;
colorBlending.blendConstants[1] = 0.0f;
colorBlending.blendConstants[2] = 0.0f;
colorBlending.blendConstants[3] = 0.0f;
VkPipelineLayoutCreateInfo pipelineLayoutInfo{};
pipelineLayoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
pipelineLayoutInfo.setLayoutCount = 1;
pipelineLayoutInfo.pSetLayouts = &descriptorSetLayout;
pipelineLayoutInfo.pushConstantRangeCount = 0;
pipelineLayoutInfo.pPushConstantRanges = nullptr;
VK_CHECK(vkCreatePipelineLayout(device, &pipelineLayoutInfo, nullptr,
&pipelineLayout));
std::vector<VkDynamicState> dynamicStateEnables = {VK_DYNAMIC_STATE_VIEWPORT,
VK_DYNAMIC_STATE_SCISSOR};
VkPipelineDynamicStateCreateInfo dynamicStateCI{};
dynamicStateCI.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO;
dynamicStateCI.pDynamicStates = dynamicStateEnables.data();
dynamicStateCI.dynamicStateCount =
static_cast<uint32_t>(dynamicStateEnables.size());
VkGraphicsPipelineCreateInfo pipelineInfo{};
pipelineInfo.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO;
pipelineInfo.stageCount = 2;
pipelineInfo.pStages = shaderStages;
pipelineInfo.pVertexInputState = &vertexInputInfo;
pipelineInfo.pInputAssemblyState = &inputAssembly;
pipelineInfo.pViewportState = &viewportState;
pipelineInfo.pRasterizationState = &rasterizer;
pipelineInfo.pMultisampleState = &multisampling;
pipelineInfo.pDepthStencilState = nullptr;
pipelineInfo.pColorBlendState = &colorBlending;
pipelineInfo.pDynamicState = &dynamicStateCI;
pipelineInfo.layout = pipelineLayout;
pipelineInfo.renderPass = renderPass;
pipelineInfo.subpass = 0;
pipelineInfo.basePipelineHandle = VK_NULL_HANDLE;
pipelineInfo.basePipelineIndex = -1;
VK_CHECK(vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, &pipelineInfo,
nullptr, &graphicsPipeline));
vkDestroyShaderModule(device, fragShaderModule, nullptr);
vkDestroyShaderModule(device, vertShaderModule, nullptr);
}
在这一步结束时,您只能看到没有渲染任何内容的黑色窗口,这是因为设置流程还未结束。如果出现任何问题,您可以将自己的版本与 [codelab] step: create shader and pipeline
仓库的提交内容进行比较。
7. DescriptorSet 和统一缓冲区
VkDescriptorSet 是一个 Vulkan 对象,代表一系列描述符资源。描述符资源用于提供着色器输入,例如统一缓冲区、图像采样器和存储缓冲区。若要创建 VkDescriptorSet,我们就需要创建 VkDescriptorPool。
VkBuffer 是一种内存缓冲区,用于在 GPU 和 CPU 之间共享数据。当用作统一缓冲区时,它会将数据作为统一变量传递给着色器。统一变量是流水线中所有着色器均可访问的常量。
// CODELAB: hellovk.h
void HelloVK::createDescriptorPool() {
VkDescriptorPoolSize poolSize{};
poolSize.type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
poolSize.descriptorCount = static_cast<uint32_t>(MAX_FRAMES_IN_FLIGHT);
VkDescriptorPoolCreateInfo poolInfo{};
poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
poolInfo.poolSizeCount = 1;
poolInfo.pPoolSizes = &poolSize;
poolInfo.maxSets = static_cast<uint32_t>(MAX_FRAMES_IN_FLIGHT);
VK_CHECK(vkCreateDescriptorPool(device, &poolInfo, nullptr, &descriptorPool));
}
创建从指定的 VkDescriptorPool 分配的 VkDescriptorSet。
// CODELAB: hellovk.h
void HelloVK::createDescriptorSets() {
std::vector<VkDescriptorSetLayout> layouts(MAX_FRAMES_IN_FLIGHT,
descriptorSetLayout);
VkDescriptorSetAllocateInfo allocInfo{};
allocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
allocInfo.descriptorPool = descriptorPool;
allocInfo.descriptorSetCount = static_cast<uint32_t>(MAX_FRAMES_IN_FLIGHT);
allocInfo.pSetLayouts = layouts.data();
descriptorSets.resize(MAX_FRAMES_IN_FLIGHT);
VK_CHECK(vkAllocateDescriptorSets(device, &allocInfo, descriptorSets.data()));
for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) {
VkDescriptorBufferInfo bufferInfo{};
bufferInfo.buffer = uniformBuffers[i];
bufferInfo.offset = 0;
bufferInfo.range = sizeof(UniformBufferObject);
VkWriteDescriptorSet descriptorWrite{};
descriptorWrite.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
descriptorWrite.dstSet = descriptorSets[i];
descriptorWrite.dstBinding = 0;
descriptorWrite.dstArrayElement = 0;
descriptorWrite.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
descriptorWrite.descriptorCount = 1;
descriptorWrite.pBufferInfo = &bufferInfo;
vkUpdateDescriptorSets(device, 1, &descriptorWrite, 0, nullptr);
}
}
指定“统一缓冲区”结构体,并创建统一缓冲区。请务必使用 vkAllocateMemory 从 VkDeviceMemory 分配内存,并使用 vkBindBufferMemory 将缓冲区绑定到该内存。
// CODELAB: hellovk.h
struct UniformBufferObject {
std::array<float, 16> mvp;
};
void HelloVK::createUniformBuffers() {
VkDeviceSize bufferSize = sizeof(UniformBufferObject);
uniformBuffers.resize(MAX_FRAMES_IN_FLIGHT);
uniformBuffersMemory.resize(MAX_FRAMES_IN_FLIGHT);
for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) {
createBuffer(bufferSize, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT,
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT |
VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
uniformBuffers[i], uniformBuffersMemory[i]);
}
}
// CODELAB: hellovk.h
void HelloVK::createBuffer(VkDeviceSize size, VkBufferUsageFlags usage,
VkMemoryPropertyFlags properties, VkBuffer &buffer,
VkDeviceMemory &bufferMemory) {
VkBufferCreateInfo bufferInfo{};
bufferInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
bufferInfo.size = size;
bufferInfo.usage = usage;
bufferInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
VK_CHECK(vkCreateBuffer(device, &bufferInfo, nullptr, &buffer));
VkMemoryRequirements memRequirements;
vkGetBufferMemoryRequirements(device, buffer, &memRequirements);
VkMemoryAllocateInfo allocInfo{};
allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
allocInfo.allocationSize = memRequirements.size;
allocInfo.memoryTypeIndex =
findMemoryType(memRequirements.memoryTypeBits, properties);
VK_CHECK(vkAllocateMemory(device, &allocInfo, nullptr, &bufferMemory));
vkBindBufferMemory(device, buffer, bufferMemory, 0);
}
您可以使用辅助函数找出正确的内存类型。
// CODELAB: hellovk.h
/*
* Finds the index of the memory heap which matches a particular buffer's memory
* requirements. Vulkan manages these requirements as a bitset, in this case
* expressed through a uint32_t.
*/
uint32_t HelloVK::findMemoryType(uint32_t typeFilter,
VkMemoryPropertyFlags properties) {
VkPhysicalDeviceMemoryProperties memProperties;
vkGetPhysicalDeviceMemoryProperties(physicalDevice, &memProperties);
for (uint32_t i = 0; i < memProperties.memoryTypeCount; i++) {
if ((typeFilter & (1 << i)) && (memProperties.memoryTypes[i].propertyFlags &
properties) == properties) {
return i;
}
}
assert(false); // failed to find a suitable memory type!
return -1;
}
在这一步结束时,您只能看到没有渲染任何内容的黑色窗口,这是因为设置流程还未结束。如果出现任何问题,您可以将自己的版本与 [codelab] step: descriptorset and uniform buffer
仓库的提交内容进行比较。
8. 命令缓冲区 - 创建、记录和绘制
VkCommandPool 是一个用于分配命令缓冲区的简单对象。它已连接到特定队列系列。
VkCommandBuffer 是一个 Vulkan 对象,代表 GPU 将执行的命令列表。它是一个低层级对象,用于对 GPU 进行精细控制。
// CODELAB: hellovk.h
void HelloVK::createCommandPool() {
QueueFamilyIndices queueFamilyIndices = findQueueFamilies(physicalDevice);
VkCommandPoolCreateInfo poolInfo{};
poolInfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;
poolInfo.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
poolInfo.queueFamilyIndex = queueFamilyIndices.graphicsFamily.value();
VK_CHECK(vkCreateCommandPool(device, &poolInfo, nullptr, &commandPool));
}
// CODELAB: hellovk.h
void HelloVK::createCommandBuffer() {
commandBuffers.resize(MAX_FRAMES_IN_FLIGHT);
VkCommandBufferAllocateInfo allocInfo{};
allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
allocInfo.commandPool = commandPool;
allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
allocInfo.commandBufferCount = commandBuffers.size();
VK_CHECK(vkAllocateCommandBuffers(device, &allocInfo, commandBuffers.data()));
}
在这一步结束时,您只能看到没有渲染任何内容的黑色窗口,这是因为设置流程还未结束。如果出现任何问题,您可以将自己的版本与 [codelab] step: create command pool and command buffer
仓库的提交内容进行比较。
更新统一缓冲区、记录命令缓冲区和绘制
Vulkan 中的命令(如绘制操作和内存传输)不会通过函数调用直接执行。相反,所有待执行的操作都会记录在命令缓冲区对象中。这样做的好处是,当我们准备好告诉 Vulkan 需要执行什么操作时,所有命令都会一起提交,并且由于所有命令都可用,Vulkan 可以更高效地处理这些命令。此外,这还允许根据需要在多个线程中进行命令记录。
在 Vulkan 中,所有渲染都是在渲染通道内进行的。在我们的示例中,渲染通道将在我们之前设置的帧缓冲区中渲染。
// CODELAB: hellovk.h
void HelloVK::recordCommandBuffer(VkCommandBuffer commandBuffer,
uint32_t imageIndex) {
VkCommandBufferBeginInfo beginInfo{};
beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
beginInfo.flags = 0;
beginInfo.pInheritanceInfo = nullptr;
VK_CHECK(vkBeginCommandBuffer(commandBuffer, &beginInfo));
VkRenderPassBeginInfo renderPassInfo{};
renderPassInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
renderPassInfo.renderPass = renderPass;
renderPassInfo.framebuffer = swapChainFramebuffers[imageIndex];
renderPassInfo.renderArea.offset = {0, 0};
renderPassInfo.renderArea.extent = swapChainExtent;
VkViewport viewport{};
viewport.width = (float)swapChainExtent.width;
viewport.height = (float)swapChainExtent.height;
viewport.minDepth = 0.0f;
viewport.maxDepth = 1.0f;
vkCmdSetViewport(commandBuffer, 0, 1, &viewport);
VkRect2D scissor{};
scissor.extent = swapChainExtent;
vkCmdSetScissor(commandBuffer, 0, 1, &scissor);
static float grey;
grey += 0.005f;
if (grey > 1.0f) {
grey = 0.0f;
}
VkClearValue clearColor = {grey, grey, grey, 1.0f};
renderPassInfo.clearValueCount = 1;
renderPassInfo.pClearValues = &clearColor;
vkCmdBeginRenderPass(commandBuffer, &renderPassInfo,
VK_SUBPASS_CONTENTS_INLINE);
vkCmdBindPipeline(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS,
graphicsPipeline);
vkCmdBindDescriptorSets(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS,
pipelineLayout, 0, 1, &descriptorSets[currentFrame],
0, nullptr);
vkCmdDraw(commandBuffer, 3, 1, 0, 0);
vkCmdEndRenderPass(commandBuffer);
VK_CHECK(vkEndCommandBuffer(commandBuffer));
}
您可能还需要更新统一缓冲区,因为我们正在对渲染的所有顶点使用相同的转换矩阵。
// CODELAB: hellovk.h
void HelloVK::updateUniformBuffer(uint32_t currentImage) {
SwapChainSupportDetails swapChainSupport =
querySwapChainSupport(physicalDevice);
UniformBufferObject ubo{};
getPrerotationMatrix(swapChainSupport.capabilities, pretransformFlag,
ubo.mvp);
void *data;
vkMapMemory(device, uniformBuffersMemory[currentImage], 0, sizeof(ubo), 0,
&data);
memcpy(data, &ubo, sizeof(ubo));
vkUnmapMemory(device, uniformBuffersMemory[currentImage]);
}
现在该渲染了!获取您已组合的命令缓冲区,并将其提交到队列中。
// CODELAB: hellovk.h
void HelloVK::render() {
if (orientationChanged) {
onOrientationChange();
}
vkWaitForFences(device, 1, &inFlightFences[currentFrame], VK_TRUE,
UINT64_MAX);
uint32_t imageIndex;
VkResult result = vkAcquireNextImageKHR(
device, swapChain, UINT64_MAX, imageAvailableSemaphores[currentFrame],
VK_NULL_HANDLE, &imageIndex);
if (result == VK_ERROR_OUT_OF_DATE_KHR) {
recreateSwapChain();
return;
}
assert(result == VK_SUCCESS ||
result == VK_SUBOPTIMAL_KHR); // failed to acquire swap chain image
updateUniformBuffer(currentFrame);
vkResetFences(device, 1, &inFlightFences[currentFrame]);
vkResetCommandBuffer(commandBuffers[currentFrame], 0);
recordCommandBuffer(commandBuffers[currentFrame], imageIndex);
VkSubmitInfo submitInfo{};
submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
VkSemaphore waitSemaphores[] = {imageAvailableSemaphores[currentFrame]};
VkPipelineStageFlags waitStages[] = {
VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT};
submitInfo.waitSemaphoreCount = 1;
submitInfo.pWaitSemaphores = waitSemaphores;
submitInfo.pWaitDstStageMask = waitStages;
submitInfo.commandBufferCount = 1;
submitInfo.pCommandBuffers = &commandBuffers[currentFrame];
VkSemaphore signalSemaphores[] = {renderFinishedSemaphores[currentFrame]};
submitInfo.signalSemaphoreCount = 1;
submitInfo.pSignalSemaphores = signalSemaphores;
VK_CHECK(vkQueueSubmit(graphicsQueue, 1, &submitInfo,
inFlightFences[currentFrame]));
VkPresentInfoKHR presentInfo{};
presentInfo.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR;
presentInfo.waitSemaphoreCount = 1;
presentInfo.pWaitSemaphores = signalSemaphores;
VkSwapchainKHR swapChains[] = {swapChain};
presentInfo.swapchainCount = 1;
presentInfo.pSwapchains = swapChains;
presentInfo.pImageIndices = &imageIndex;
presentInfo.pResults = nullptr;
result = vkQueuePresentKHR(presentQueue, &presentInfo);
if (result == VK_SUBOPTIMAL_KHR) {
orientationChanged = true;
} else if (result == VK_ERROR_OUT_OF_DATE_KHR) {
recreateSwapChain();
} else {
assert(result == VK_SUCCESS); // failed to present swap chain image!
}
currentFrame = (currentFrame + 1) % MAX_FRAMES_IN_FLIGHT;
}
通过重新创建交换链来处理屏幕方向变化。
// CODELAB: hellovk.h
void HelloVK::onOrientationChange() {
recreateSwapChain();
orientationChanged = false;
}
集成到应用生命周期中。
// CODELAB: vk_main.cpp
/**
* Called by the Android runtime whenever events happen so the
* app can react to it.
*/
static void HandleCmd(struct android_app *app, int32_t cmd) {
auto *engine = (VulkanEngine *)app->userData;
switch (cmd) {
case APP_CMD_START:
if (engine->app->window != nullptr) {
engine->app_backend->reset(app->window, app->activity->assetManager);
engine->app_backend->initVulkan();
engine->canRender = true;
}
case APP_CMD_INIT_WINDOW:
// The window is being shown, get it ready.
LOGI("Called - APP_CMD_INIT_WINDOW");
if (engine->app->window != nullptr) {
LOGI("Setting a new surface");
engine->app_backend->reset(app->window, app->activity->assetManager);
if (!engine->app_backend->initialized) {
LOGI("Starting application");
engine->app_backend->initVulkan();
}
engine->canRender = true;
}
break;
case APP_CMD_TERM_WINDOW:
// The window is being hidden or closed, clean it up.
engine->canRender = false;
break;
case APP_CMD_DESTROY:
// The window is being hidden or closed, clean it up.
LOGI("Destroying");
engine->app_backend->cleanup();
default:
break;
}
}
/*
* Entry point required by the Android Glue library.
* This can also be achieved more verbosely by manually declaring JNI functions
* and calling them from the Android application layer.
*/
void android_main(struct android_app *state) {
VulkanEngine engine{};
vkt::HelloVK vulkanBackend{};
engine.app = state;
engine.app_backend = &vulkanBackend;
state->userData = &engine;
state->onAppCmd = HandleCmd;
android_app_set_key_event_filter(state, VulkanKeyEventFilter);
android_app_set_motion_event_filter(state, VulkanMotionEventFilter);
while (true) {
int ident;
int events;
android_poll_source *source;
while ((ident = ALooper_pollAll(engine.canRender ? 0 : -1, nullptr, &events,
(void **)&source)) >= 0) {
if (source != nullptr) {
source->process(state, source);
}
}
HandleInputEvents(state);
engine.app_backend->render();
}
}
在这一步结束时,您最终将在屏幕上看到一个彩色的三角形!
检查情况是否如此,如果出现任何问题,您可以将自己的版本与 [codelab] step: update uniform buffer, record command buffer and draw
仓库的提交内容进行比较。
9. 旋转三角形
为了旋转三角形,我们需要先将旋转应用到 MVP 矩阵,然后再将矩阵传递给着色器。这是为了防止对模型中的每个顶点重复计算相同矩阵。
若要在应用端计算 MVP 矩阵,则需要一个旋转转换矩阵。GLM 库是一个 C++ 数学库,可用于根据 GLSL 规范编写图形软件;并且该库具有构建应用了旋转的矩阵所需的 rotate 函数。
// CODELAB: hellovk.h
// Additional includes to make our lives easier than composing
// transformation matrices manually
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
// change our UniformBufferObject construct to use glm::mat4
struct UniformBufferObject {
glm::mat4 mvp;
};
// CODELAB: hellovk.h
/*
* getPrerotationMatrix handles screen rotation with 3 hardcoded rotation
* matrices (detailed below). We skip the 180 degrees rotation.
*/
void getPrerotationMatrix(const VkSurfaceCapabilitiesKHR &capabilities,
const VkSurfaceTransformFlagBitsKHR &pretransformFlag,
glm::mat4 &mat, float ratio) {
// mat is initialized to the identity matrix
mat = glm::mat4(1.0f);
// scale by screen ratio
mat = glm::scale(mat, glm::vec3(1.0f, ratio, 1.0f));
// rotate 1 degree every function call.
static float currentAngleDegrees = 0.0f;
currentAngleDegrees += 1.0f;
if ( currentAngleDegrees >= 360.0f ) {
currentAngleDegrees = 0.0f;
}
mat = glm::rotate(mat, glm::radians(currentAngleDegrees), glm::vec3(0.0f, 0.0f, 1.0f));
}
在这一步结束时,您会看到三角形在屏幕上旋转!检查情况是否如此,如果出现任何问题,您可以将自己的版本与 [codelab] step: rotate triangle
仓库的提交内容进行比较。
10. 应用纹理
如需对三角形应用纹理,首先需要在内存中加载未压缩格式的图像文件。此步骤使用 stb 图像库将图像数据加载并解码到 RAM,然后将该 RAM 复制到 Vulkan 缓冲区 (VkBuffer)。
// CODELAB: hellovk.h
void HelloVK::decodeImage() {
std::vector<uint8_t> imageData = LoadBinaryFileToVector("texture.png",
assetManager);
if (imageData.size() == 0) {
LOGE("Fail to load image.");
return;
}
unsigned char* decodedData = stbi_load_from_memory(imageData.data(),
imageData.size(), &textureWidth, &textureHeight, &textureChannels, 0);
if (decodedData == nullptr) {
LOGE("Fail to load image to memory, %s", stbi_failure_reason());
return;
}
size_t imageSize = textureWidth * textureHeight * textureChannels;
VkBufferCreateInfo createInfo{};
createInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
createInfo.size = imageSize;
createInfo.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT;
createInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
VK_CHECK(vkCreateBuffer(device, &createInfo, nullptr, &stagingBuffer));
VkMemoryRequirements memRequirements;
vkGetBufferMemoryRequirements(device, stagingBuffer, &memRequirements);
VkMemoryAllocateInfo allocInfo{};
allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
allocInfo.allocationSize = memRequirements.size;
allocInfo.memoryTypeIndex = findMemoryType(memRequirements.memoryTypeBits,
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT);
VK_CHECK(vkAllocateMemory(device, &allocInfo, nullptr, &stagingMemory));
VK_CHECK(vkBindBufferMemory(device, stagingBuffer, stagingMemory, 0));
uint8_t *data;
VK_CHECK(vkMapMemory(device, stagingMemory, 0, memRequirements.size, 0,
(void **)&data));
memcpy(data, decodedData, imageSize);
vkUnmapMemory(device, stagingMemory);
stbi_image_free(decodedData);
}
接下来,找到在上一步中填充了图像数据的 VkBuffer,从中创建 VkImage。
VkImage 是保存实际纹理数据的对象。它会将像素数据存储在纹理的主内存中,但并不包含关于数据读取方式的大量信息。因此,我们需要在下一部分中创建 VkImageView。
// CODELAB: hellovk.h
void HelloVK::createTextureImage() {
VkImageCreateInfo imageInfo{};
imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
imageInfo.imageType = VK_IMAGE_TYPE_2D;
imageInfo.extent.width = textureWidth;
imageInfo.extent.height = textureHeight;
imageInfo.extent.depth = 1;
imageInfo.mipLevels = 1;
imageInfo.arrayLayers = 1;
imageInfo.format = VK_FORMAT_R8G8B8_UNORM;
imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
imageInfo.usage =
VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
imageInfo.samples = VK_SAMPLE_COUNT_1_BIT;
imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
VK_CHECK(vkCreateImage(device, &imageInfo, nullptr, &textureImage));
VkMemoryRequirements memRequirements;
vkGetImageMemoryRequirements(device, textureImage, &memRequirements);
VkMemoryAllocateInfo allocInfo{};
allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
allocInfo.allocationSize = memRequirements.size;
allocInfo.memoryTypeIndex = findMemoryType(memRequirements.memoryTypeBits,
VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
VK_CHECK(vkAllocateMemory(device, &allocInfo, nullptr, &textureImageMemory));
vkBindImageMemory(device, textureImage, textureImageMemory, 0);
}
// CODELAB: hellovk.h
void HelloVK::copyBufferToImage() {
VkImageSubresourceRange subresourceRange{};
subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
subresourceRange.baseMipLevel = 0;
subresourceRange.levelCount = 1;
subresourceRange.layerCount = 1;
VkImageMemoryBarrier imageMemoryBarrier{};
imageMemoryBarrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
imageMemoryBarrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
imageMemoryBarrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
imageMemoryBarrier.image = textureImage;
imageMemoryBarrier.subresourceRange = subresourceRange;
imageMemoryBarrier.srcAccessMask = 0;
imageMemoryBarrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
imageMemoryBarrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED;
imageMemoryBarrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
VkCommandBuffer cmd;
VkCommandBufferAllocateInfo cmdAllocInfo{};
cmdAllocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
cmdAllocInfo.commandPool = commandPool;
cmdAllocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
cmdAllocInfo.commandBufferCount = 1;
VK_CHECK(vkAllocateCommandBuffers(device, &cmdAllocInfo, &cmd));
VkCommandBufferBeginInfo beginInfo{};
beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
vkBeginCommandBuffer(cmd, &beginInfo);
vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_HOST_BIT,
VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0,
nullptr, 1, &imageMemoryBarrier);
VkBufferImageCopy bufferImageCopy{};
bufferImageCopy.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
bufferImageCopy.imageSubresource.mipLevel = 0;
bufferImageCopy.imageSubresource.baseArrayLayer = 0;
bufferImageCopy.imageSubresource.layerCount = 1;
bufferImageCopy.imageExtent.width = textureWidth;
bufferImageCopy.imageExtent.height = textureHeight;
bufferImageCopy.imageExtent.depth = 1;
bufferImageCopy.bufferOffset = 0;
vkCmdCopyBufferToImage(cmd, stagingBuffer, textureImage,
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
1, &bufferImageCopy);
imageMemoryBarrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
imageMemoryBarrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
imageMemoryBarrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
imageMemoryBarrier.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT,
VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, 0, nullptr,
0, nullptr, 1, &imageMemoryBarrier);
vkEndCommandBuffer(cmd);
VkSubmitInfo submitInfo{};
submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
submitInfo.commandBufferCount = 1;
submitInfo.pCommandBuffers = &cmd;
VK_CHECK(vkQueueSubmit(graphicsQueue, 1, &submitInfo, VK_NULL_HANDLE));
vkQueueWaitIdle(graphicsQueue);
}
接下来,创建 VkImageView 和 VkSampler,片段着色器可以使用它们对每个渲染像素的颜色进行采样。
VkImageView 是 VkImage 之上的封装容器,其中包含有关如何解读纹理数据的信息,例如,是否只想访问某个区域或层,以及是否想以特定方式打乱像素通道顺序。
VkSampler 用于存储特定着色器访问纹理所需的数据,其中包含有关如何混合像素或如何进行 mipmap 的信息。采样器与描述符中的 VkImageView 一起使用。
// CODELAB: hellovk.h
void HelloVK::createTextureImageViews() {
VkImageViewCreateInfo createInfo{};
createInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
createInfo.image = textureImage;
createInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
createInfo.format = VK_FORMAT_R8G8B8_UNORM;
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;
createInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
createInfo.subresourceRange.baseMipLevel = 0;
createInfo.subresourceRange.levelCount = 1;
createInfo.subresourceRange.baseArrayLayer = 0;
createInfo.subresourceRange.layerCount = 1;
VK_CHECK(vkCreateImageView(device, &createInfo, nullptr, &textureImageView));
}
我们还需要创建一个采样器,以传递给着色器
// CODELAB: hellovk.h
void HelloVK::createTextureSampler() {
VkSamplerCreateInfo createInfo{};
createInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO;
createInfo.magFilter = VK_FILTER_LINEAR;
createInfo.minFilter = VK_FILTER_LINEAR;
createInfo.addressModeU = VK_SAMPLER_ADDRESS_MODE_REPEAT;
createInfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_REPEAT;
createInfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_REPEAT;
createInfo.anisotropyEnable = VK_FALSE;
createInfo.maxAnisotropy = 16;
createInfo.borderColor = VK_BORDER_COLOR_INT_OPAQUE_BLACK;
createInfo.unnormalizedCoordinates = VK_FALSE;
createInfo.compareEnable = VK_FALSE;
createInfo.compareOp = VK_COMPARE_OP_ALWAYS;
createInfo.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR;
createInfo.mipLodBias = 0.0f;
createInfo.minLod = 0.0f;
createInfo.maxLod = VK_LOD_CLAMP_NONE;
VK_CHECK(vkCreateSampler(device, &createInfo, nullptr, &textureSampler));
}
最后,修改着色器,对图像进行采样,而不是使用顶点颜色。纹理坐标是浮点位置,可将纹理上的位置映射到几何表面上的位置。在我们的示例中,此流程是通过将 vTexCoords
定义为顶点着色器的输出来完成的,我们有一个归一化(大小为 {1, 1})的三角形,因此直接用顶点的 texCoords
进行填充。
// CODELAB: shader.vert
#version 450
// Uniform buffer containing an MVP matrix.
// Currently the vulkan backend only sets the rotation matrix
// required to handle device rotation.
layout(binding = 0) uniform UniformBufferObject {
mat4 MVP;
} ubo;
vec2 positions[3] = vec2[](
vec2(0.0, 0.577),
vec2(-0.5, -0.289),
vec2(0.5, -0.289)
);
vec2 texCoords[3] = vec2[](
vec2(0.5, 1.0),
vec2(0.0, 0.0),
vec2(1.0, 0.0)
);
layout(location = 0) out vec2 vTexCoords;
void main() {
gl_Position = ubo.MVP * vec4(positions[gl_VertexIndex], 0.0, 1.0);
vTexCoords = texCoords[gl_VertexIndex];
}
使用采样器和纹理的片段着色器。
// CODELAB: shader.frag
#version 450
layout(location = 0) in vec2 vTexCoords;
layout(binding = 1) uniform sampler2D samp;
// Output colour for the fragment
layout(location = 0) out vec4 outColor;
void main() {
outColor = texture(samp, vTexCoords);
}
在这一步结束时,您会看到旋转的三角形有纹理了!
检查情况是否如此,如果出现任何问题,您可以将自己的版本与 [codelab] step: apply texture
仓库的提交内容进行比较。
11. 添加验证层
验证层是可选组件,可接入 Vulkan 函数调用来应用其他操作,例如:
- 验证参数值,以检测滥用情况
- 跟踪对象的创建和销毁情况,以发现资源泄露问题
- 检查线程安全
- 记录调用,以便分析和重放
由于验证层的下载量相当大,因此我们选择不在 APK 中提供该验证层。因此,若要启用验证层,请按照下面的简单步骤操作:
从以下地址下载最新的 Android 二进制文件:https://github.com/KhronosGroup/Vulkan-ValidationLayers/releases
将这些文件放入其各自的 ABI 文件夹中,路径为:app/src/main/jniLibs
按照以下步骤启用验证层
// CODELAB: hellovk.h
void HelloVK::createInstance() {
assert(!enableValidationLayers ||
checkValidationLayerSupport()); // validation layers requested, but not available!
auto requiredExtensions = getRequiredExtensions(enableValidationLayers);
VkApplicationInfo appInfo{};
appInfo.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO;
appInfo.pApplicationName = "Hello Triangle";
appInfo.applicationVersion = VK_MAKE_VERSION(1, 0, 0);
appInfo.pEngineName = "No Engine";
appInfo.engineVersion = VK_MAKE_VERSION(1, 0, 0);
appInfo.apiVersion = VK_API_VERSION_1_0;
VkInstanceCreateInfo createInfo{};
createInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
createInfo.pApplicationInfo = &appInfo;
createInfo.enabledExtensionCount = (uint32_t)requiredExtensions.size();
createInfo.ppEnabledExtensionNames = requiredExtensions.data();
createInfo.pApplicationInfo = &appInfo;
if (enableValidationLayers) {
VkDebugUtilsMessengerCreateInfoEXT debugCreateInfo{};
createInfo.enabledLayerCount =
static_cast<uint32_t>(validationLayers.size());
createInfo.ppEnabledLayerNames = validationLayers.data();
populateDebugMessengerCreateInfo(debugCreateInfo);
createInfo.pNext = (VkDebugUtilsMessengerCreateInfoEXT *)&debugCreateInfo;
} else {
createInfo.enabledLayerCount = 0;
createInfo.pNext = nullptr;
}
VK_CHECK(vkCreateInstance(&createInfo, nullptr, &instance));
uint32_t extensionCount = 0;
vkEnumerateInstanceExtensionProperties(nullptr, &extensionCount, nullptr);
std::vector<VkExtensionProperties> extensions(extensionCount);
vkEnumerateInstanceExtensionProperties(nullptr, &extensionCount,
extensions.data());
LOGI("available extensions");
for (const auto &extension : extensions) {
LOGI("\t %s", extension.extensionName);
}
}
12. 恭喜
恭喜,您已成功设置 Vulkan 渲染流水线,可以开始开发游戏了!
我们将为 Android 添加更多 Vulkan 功能,敬请期待。
如需详细了解如何开始在 Android 上使用 Vulkan,请参阅开始在 Android 上使用 Vulkan。