1. 简介
为什么在我的游戏中使用Vulkan?
Vulkan是Android上主要的低级图形API。对于实现自己的游戏引擎和渲染器的游戏,Vulkan能够实现更高的性能。
从Android 7.0(API级别24)开始,Android上即可使用Vulkan。从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。显示“欢迎使用Android Studio”窗口时,打开“配置”下拉菜单并选择“SDK Manager”选项。
如果您已经打开了现有项目,则可以通过“工具”菜单打开SDK Manager。单击**工具**菜单,然后选择**SDK Manager**,SDK Manager窗口将打开。
在侧边栏中,依次选择:**外观与行为 > 系统设置 > Android SDK**。在Android SDK窗格中选择**SDK平台**选项卡以显示已安装的工具选项列表。确保已安装Android SDK 12.0或更高版本。
接下来,选择**SDK工具**选项卡,并确保已安装**NDK**和**CMake**。
注意:只要版本比较新,精确版本并不重要,但我们目前使用的是NDK 26.1.10909125和CMake 3.22.1。随着后续NDK版本的发布,默认安装的NDK版本会发生变化。如果您需要安装特定版本的NDK,请按照Android Studio参考文档中关于安装NDK的说明操作,具体在“**安装特定版本的NDK**”部分。
检查所有必需的工具后,单击窗口底部的**应用**按钮进行安装。然后,您可以通过单击**确定**按钮关闭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驱动程序之前拦截和处理API调用的机制。应用程序可以在创建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后,应用程序可以从中创建一个逻辑设备。逻辑设备是特定于应用程序的物理设备的表示。它具有自己的状态和资源,并且独立于可能从同一物理设备创建的其他逻辑设备。
有不同类型的队列源自不同的**队列族**,每个队列族只允许一部分命令。例如,可能有一个队列族只允许处理计算命令,或者一个只允许处理与内存传输相关的命令。
VkPhysicalDevice可以枚举所有可用的**队列族**类型。我们这里只对图形队列感兴趣,但还可能有其他队列只支持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对象获取另一个图像才能再次渲染。
VkSwapchains 通常在应用程序启动时创建一次,并在结束时销毁。但是,可以在同一个应用程序中创建和销毁多个 VkSwapchains,例如,如果应用程序需要使用多个 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. 创建 Renderpass 和 Framebuffer
**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 代表可以用于附件(渲染目标)的实际图像的链接。通过指定渲染通道和图像视图集来创建 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. 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 分配的 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);
}
}
指定我们的统一缓冲区结构并创建统一缓冲区。记住使用 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。它连接到特定的队列族。
**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 中,所有渲染都发生在 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));
}
您可能还需要更新统一缓冲区,因为我们对正在渲染的所有顶点使用相同的变换矩阵。
// 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 库 是一个基于 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 图像库 将图像数据加载并解码到 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 的信息。采样器与描述符中的 VkImageViews 一起使用。
// 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
定义为顶点着色器的输出来完成的,我们直接用顶点的 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];
}
使用采样器和纹理的片段着色器。
// 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。