1. 简介
为何在我的游戏中使用 Vulkan?
Vulkan 是 Android 上主要的底层图形 API。Vulkan 可为实现自定义游戏引擎和渲染器的游戏提供更高的性能。
Vulkan 可在 Android 7.0(API 级别 24)及更高版本上使用。对于从 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 或更高版本,并且已启用开发者选项和USB 调试的 Android 设备。
2. 设置
设置开发环境
如果您之前未在 Android Studio 中处理过原生项目,则可能需要安装 Android NDK 和 CMake。如果您已安装它们,请继续进行设置项目。
检查是否已安装 SDK、NDK 和 CMake
启动 Android Studio。显示欢迎使用 Android Studio 窗口时,打开 Configure 下拉菜单并选择 SDK Manager 选项。
如果您已经打开了现有项目,也可以通过 Tools 菜单打开 SDK Manager。点击 Tools 菜单并选择 SDK Manager,SDK Manager 窗口将打开。
在侧边栏中,按顺序选择: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 窗口。
设置项目
一个从 C++ 模板衍生的起始项目已在 Git 代码库中为您设置好。该起始项目实现了应用初始化和事件处理,但尚未进行任何图形设置或渲染。
克隆代码库
从命令行,切换到您希望包含项目根目录的目录,然后从 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 后,应用可以使用它来查询系统上可用的物理设备、创建逻辑设备以及创建用于渲染的表面。
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 后,应用可以从中创建一个逻辑设备。逻辑设备是物理设备的一种表示,特定于该应用。它有自己的状态和资源,并且独立于可能从同一物理设备创建的其他逻辑设备。
存在不同类型的队列,它们源自不同的队列族 (Queue Families),每个队列族只允许一部分命令。例如,可能有一个队列族只允许处理计算命令,或者只允许处理与内存传输相关的命令。
VkPhysicalDevice 可以枚举所有可用类型的队列族 (Queue Families)。我们在这里只关注图形队列,但也可能有其他队列只支持 COMPUTE
或 TRANSFER
。队列族没有自己的类型。相反,它在其父对象(VkPhysicalDevice)内部由数字索引类型 uint32_t 表示。
一个 VkPhysicalDevice 可以从中创建多个逻辑设备。这对于需要使用多个 GPU 或创建多个窗口的应用非常有用。
VkDevice 是一个 Vulkan 对象,代表逻辑 Vulkan 设备。它是物理设备的一个薄层抽象,提供了创建和管理 Vulkan 资源所需的所有功能,例如缓冲区、图像和着色器。
VkDevice 是从 VkPhysicalDevice 创建的,并且特定于创建它的应用。它有自己的状态和资源,并且独立于可能从同一物理设备创建的其他逻辑设备。
VkSurfaceKHR 对象代表可以作为渲染操作目标的表面。要在设备屏幕上显示图形,您将使用应用窗口对象的引用来创建表面。创建 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 是一个 Vulkan 对象,描述如何访问 VkImage。它指定要访问的图像的子资源范围、要使用的像素格式以及要应用于通道的混排。
VkRenderPass 是一个 Vulkan 对象,描述 GPU 应如何渲染场景。它指定将使用的附件、渲染它们的顺序以及它们在渲染流水线每个阶段的使用方式。
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));
}
帧缓冲区 (Framebuffer) 代表与可用作附件(渲染目标)的实际图像的链接。通过指定渲染通道和图像视图集来创建帧缓冲区对象。
// 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 的模板,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. 描述符集和 Uniform Buffer
VkDescriptorSet 是一个 Vulkan 对象,代表描述符资源的集合。描述符资源用于提供着色器输入,例如 uniform buffer、图像采样器和存储缓冲区。要创建 VkDescriptorSet,我们需要创建一个 VkDescriptorPool。
VkBuffer 是用于在 GPU 和 CPU 之间共享数据的内存缓冲区。当用作 Uniform buffer 时,它将数据作为 uniform 变量传递到着色器。Uniform 变量是可以由流水线中的所有着色器访问的常量。
// 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 分配的 VkDescriptorSets。
// 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);
}
}
指定我们的 Uniform Buffer 结构体并创建 uniform buffer。请记住使用 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 是一个简单对象,用于分配 CommandBuffers。它连接到特定的队列族 (Queue Family)。
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
的提交进行比较。
更新 uniform buffer,记录命令缓冲区并绘制
Vulkan 中的命令(如绘制操作和内存传输)不是直接通过函数调用执行的。相反,所有待处理的操作都记录在命令缓冲区对象中。这样做的好处是,当我们准备好告诉 Vulkan 我们想做什么时,所有命令会一起提交,并且由于所有命令都可用,Vulkan 可以更有效地处理这些命令。此外,如果需要,这允许在多个线程中进行命令记录。
在 Vulkan 中,所有渲染都发生在 RenderPasses 内部。在我们的示例中,RenderPass 将渲染到我们之前设置的 FrameBuffer 中。
// 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));
}
您可能还需要更新 Uniform Buffer,因为我们对所有正在渲染的顶点使用相同的变换矩阵。
// 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 Library 是一个基于 GLSL 规范的 C++ 数学库,用于编写图形软件,它具有构建应用了旋转的矩阵所需的 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 image library 将图像数据加载并解码到 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 包含着色器特定访问纹理的数据。它包含关于如何混合像素或如何进行 mipmapping 的信息。采样器与 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));
}
我们还需要创建一个 Sampler 传递给我们的着色器
// 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
定义为顶点着色器的输出,然后直接用顶点的 texCoords
填充它来完成此过程,因为我们有一个归一化的 ({1, 1} 大小的) 三角形。
// 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];
}
使用 Sampler 和纹理的片段着色器。
// 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
将它们放在 app/src/main/jniLibs 中各自的 ABI 文件夹下
按照以下步骤启用验证层
// 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。