我們現在能爲每個頂點傳輸任意屬性到頂點着色器了,但是用全局變量怎麼樣呢?我們本章要轉移到3D圖形上,這要求Model-View-Projection矩陣,也就是MVP矩陣(模型-視口-投影矩陣)。 我們可以將它包括進來作爲頂點數據,但是這比較浪費內存,也要求我們在它的變換改變的時候更新頂點緩衝,而變換是很可能在每一幀都改變的。
Vulkan中正確處理該問題的方法是使用資源描述符。描述符是着色器能自由訪問緩衝和圖像等資源的一種方式。我們要建立一個緩衝,它包含了變換矩陣及讓頂點着色器通過描述符訪問它們。描述符用法由以下三部分組成:
管線創建階段指定一個描述符佈局;
從描述符池指定一個描述符集合;
在渲染階段構建描述符。
描述符佈局指定了將要被管線訪問的資源類型,就和渲染通道指定了將要訪問的附件的類型一樣。描述符集合指定了將要綁定到描述符的實際緩衝或者圖像資源,就和幀緩衝指定了實際圖像視圖來綁定渲染通道附件一樣。描述符集合爲繪製命令綁定,就和頂點緩衝及幀緩衝一樣。
描述符有很多類型,但是本章我們就用統一緩衝對象。以後的章節再看其他類型的描述符,但是基本的處理都是一樣的。假設我們有個C結構體如下,包含了我們想要頂點着色器擁有的數據:
struct UniformBufferObject {
glm::mat4 model;
glm::mat4 view;
glm::mat4 proj;
};
那麼我們可以將數據拷貝到VkBuffer,然後通過一個統一緩衝對象描述符從頂點着色器訪問如下:
layout(binding = 0) uniform UniformBufferObject {
mat4 model;
mat4 view;
mat4 proj;
} ubo;
void main() {
gl_Position = ubo.proj * ubo.view * ubo.model * vec4(inPosition, 0.0, 1.0);
fragColor = inColor;
}
我們將會每幀更新該MVP矩陣以讓矩形在3D模式轉動起來。
修改頂點着色器以包括統一緩衝對象,我這裏認爲你對MVP矩陣比較熟悉,否則就看第一章提到的資源學習。
#version 450
#extension GL_ARB_separate_shader_objects : enable
layout(binding = 0) uniform UniformBufferObject {
mat4 model;
mat4 view;
mat4 proj;
} ubo;
layout(location = 0) in vec2 inPosition;
layout(location = 1) in vec3 inColor;
layout(location = 0) out vec3 fragColor;
void main() {
gl_Position = ubo.proj * ubo.view * ubo.model * vec4(inPosition, 0.0, 1.0);
fragColor = inColor;
}
uniform、in和out的聲明順序沒有關係,對屬性來說,binding指令和location指令類似。我們將會在描述符佈局中引用該綁定。有gl_Position的行改成使用變換來計算最終在裁剪座標系中的位置。不像是2D三角形,裁剪座標最後的組件可能不是1,這將會導致轉換到最後的屏幕上的歸一化設備座標的時候要進行相除。這在透視投影中用作透視除法,對製作近處對象比遠處大的效果非常重要。
下一步是在C++側定義UBO,告訴Vulkan頂點着色器中的描述符信息:
struct UniformBufferObject {
glm::mat4 model;
glm::mat4 view;
glm::mat4 proj;
};
我們可以使用GLM中的數據類型嚴格匹配着色器中的定義。矩陣中的數據是和着色器所期望的那樣兼容二進制的,所以我們之後可以memcpy UniformBufferObject到BkBuffer。
我們要提供爲創建管線在着色器中使用的每個描述符綁定的細節信息,就和我們要爲每個頂點屬性和它的location索引做的工作一樣。我們將會設置一個新的函數來定義所有這些信息,該函數就是createDescriptorSetLayout。它應該在管線創建之前調用。
每個綁定都要通過VkDescriptorSetLayoutBinding描述:
void createDescriptorSetLayout() {
VkDescriptorSetLayoutBinding uboLayoutBinding = {};
uboLayoutBinding.binding = 0;
uboLayoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
uboLayoutBinding.descriptorCount = 1;
}
最開始兩個字段指定了着色器中使用的binding以及描述符類型,就是一個統一緩衝對象。着色器變量可以表示一組統一緩衝對象,descriptorCount指定了數組中的值的個數。比如,這個可以用於爲骨骼動畫中的骨骼的每個骨頭指定一個變換。我們的MVP變換是一個單一的統一緩衝對象,所以我們使用descriptorCount爲1。
uboLayoutBinding.stageFlags = VK_SHADER_STAGE_VERTEX_BIT;
我們也要指定引用哪個着色器階段。stageFlags字段可以是VkShaderStageFlagBits或者VK_SHADER_STAGE_ALL_GRAPHICS的組合。我們這裏僅僅引用來在頂點着色器的描述符。
uboLayoutBinding.pImmutableSamplers = nullptr; // optional
pImmutableSamplers字段只和圖像採樣有關的描述符有關,這裏就留默認值即可。
所有的描述符綁定都組合到單個VkDescriptorSetLayout對象,在pipelineLayout上定義一個類成員:
VkDescriptorSetLayout descriptorSetLayout;
VkPipelineLayout pipelineLayout;
我們可以用vkCreateDescriptorSetLayout創建了,該方法接收一個有一組綁定的VkDescriptorSetLayoutCreateInfo作爲參數:
VkDescriptorSetLayoutCreateInfo layoutInfo = {};
layoutInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;
layoutInfo.bindingCount = 1;
layoutInfo.pBindings = &uboLayoutBinding;
if (vkCreateDescriptorSetLayout(device, &layoutInfo, nullptr, &descriptorSetLayout) != VK_SUCCESS) {
throw std::runtime_error("failed to create descriptor set layout!");
}
我們需要在管線創建過程中指定描述符集合佈局,以告訴Vulkan着色器將會使用哪個描述符。描述符集合佈局在管線佈局對象中指定。修改VkPipelineLayoutCreateInfo來引用佈局對象:
VkPipelineLayoutCreateInfo pipelineLayoutInfo = {};
pipelineLayoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
pipelineLayoutInfo.setLayoutCount = 1;
pipelineLayoutInfo.pSetLayouts = &descriptorSetLayout;
你可能好奇,爲什麼可以指定多個描述符集合佈局,因爲單個已經包含了所有的綁定。我們以後會再看,那時候我們還會介紹描述符池和描述符集合。
在我們創建新的圖形管線的時候,描述符佈局應該就在旁邊,直到程序結束。
vkDestroyDescriptorSetLayout(device, descriptorSetLayout, nullptr);
這一行就放在清理交換鏈之後執行。
下一章我們會爲着色器指定含有統一緩衝對象的緩衝,但是我們要先創建緩衝。我們打算每一幀都先複製新的數據到統一緩衝。
我們應該有多個緩衝,因爲可能同時有很多幀都在準備中,而前一幀還在讀取的時候,我們不想在下一幀準備的時候就更新緩衝。我們可以每一幀或者每個交換鏈圖形都做一個統一緩衝。但是,由於我們需要從命令緩衝引用統一緩衝,我們選擇每一個交換鏈圖像都有一個統一緩衝的方式。
爲此,爲uniformBuffers和uniformBuffersMemory添加新的類成員,
std::vector<VkBuffer> uniformBuffers;
std::vector<VkDeviceMemory> uniformBuffersMemory;
類似的,創建一個新的方法createUniformBuffers,在createIndexBuffer之後調用來分配緩衝:
void createUniformBuffers() {
VkDeviceSize bufferSize = sizeof(UniformBufferObject);
uniformBuffers.resize(swapChainImages.size());
uniformBuffersMemory.resize(swapChainImages.size());
for (size_t i = 0; i < swapChainImages.size(); 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]);
}
}
我們要寫一個單獨的方法,每幀用一個新的變換更新統一緩衝,所以這裏不會有vkMapMemory。統一數據會被所有繪製命令使用,所以包含它的緩衝應該在我們停止渲染的時候才進行銷燬。由於它依賴於交換鏈圖像個數,這個個數可能會在重建之後改變,所以我們在清理交換鏈部分結尾處清理它:
for (size_t i = 0; i < swapChainImages.size(); i++) {
vkDestroyBuffer(device, uniformBuffers[i], nullptr);
vkFreeMemory(device, uniformBuffersMemory[i], nullptr);
}
這也意味着我們要在重建交換鏈的部分重建它:
createFramebuffers();
createUniformBuffers();
createCommandBuffers();
創建一個新的方法updateUniformBuffer,然後從drawFrame中調用,就在知道我們獲取的是哪個交換鏈圖像之後:
updateUniformBuffer(imageIndex);
VkSubmitInfo submitInfo = {};
submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
該方法會每幀生成一個新的變換讓幾何體轉動起來。我們要包含兩個新的頭文件:
#define GLM_FORCE_RADIANS
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <chrono>
頭文件glm/gtc/matrix_transform.hpp暴露了可以用於生成模型變換的方法,如glm::rotate,視圖變換如glm::lookAt以及投影變換如glm::perspective。GLM_FORCE_RADIANS對保證如glm::rotate之類的方法使用弧度作爲參數是很有必要的,避免了可能的混淆問題。
chrono標準庫頭文件暴露了做精準計時的方法。我們會使用該庫保證幾何體每秒旋轉90度,不管它是什麼幀率。
void updateUniformBuffer(uint32_t currentImage) {
static auto startTime = std::chrono::high_resolution_clock::now();
auto currentTime = std::chrono::high_resolution_clock::now();
float time = std::chrono::duration<float, std::chrono::seconds::period>(currentTime - startTime).count();
}
updateUniformBuffer方法開始的時候會計算從開始渲染起以秒爲單位的時間。
我們現在在同意緩衝對象中定義模型、視圖和投影變換。模型旋轉就是一個簡單的繞着Z軸根據時間變量的旋轉:
UniformBufferObject ubo = {};
ubo.model = glm::rotate(glm::mat4(1.0f), time*glm::radians(90.0f), glm::vec3(0.0f, 0.0f, 1.0f));
glm::rotate方法接收一個已存在的變換,旋轉角度以及旋轉軸作爲參數。glm::mat4(1.0f)構造器返回一個單位矩陣。使用time * glm::radians(90.0f)爲旋轉角度就滿足了每秒旋轉90度的目的。
ubo.view = glm::lookAt(glm::vec3(2.0f, 2.0f, 2.0f), glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f, 0.0f, 1.0f));
視圖變換部分我決定從45度角高度看該幾何體。glm::lookAt方法接收眼睛位置,中心點和向上的軸作爲參數。
ubo.proj = glm::perspective(glm::radians(45.0f),
swapChainExtent.width / (float)swapChainExtent.height, 0.1f, 10.0f);
我選擇使用45度垂直視場角作爲透視投影。其他參數時縱橫比,遠近視圖平面。使用當前交換鏈程度計算綜合比來考慮窗口調整大小後的寬高是有必要的。
ubo.proj[1][1] *= -1;
GLM原本是爲OpenGL設計的,裁剪座標系中它的Y座標是反向的。補償方式中最簡單的是翻轉投影矩陣中Y軸的大小因子的符號。如果你不這麼做,那麼圖像渲染後就是上下顛倒的。
所有變換都定義了,所以我們可以從統一緩衝對象拷貝數據到當前統一緩衝中了。這就和我們對頂點緩衝所做的一樣,除了沒有臨時緩衝:
void* data;
vkMapMemory(device, uniformBuffersMemory[currentImage], 0, sizeof(ubo), 0, &data);
memcpy(data, &ubo, sizeof(ubo));
vkUnmapMemory(device, uniformBuffersMemory[currentImage]);
這種方式使用統一緩衝對象將頻繁修改的值傳輸到着色器不是最高效的。更高效的一種方式將一小部分緩衝數據傳到着色器,也就是push constants。
後面我們會查看描述符集合,它會綁定VkBuffers到統一緩衝描述符,以便着色器能訪問變換數據。